{"openapi":"3.1.0","info":{"title":"Nemix ERP API","description":"Public OpenAPI spec for the Nemix ERP API.","version":"1.0.0"},"servers":[{"url":"/","description":"current host"}],"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Cookie-Sitzung aus der Anmeldung im Browser. Aufrufe aus dem Frontend brauchen zwingend `credentials: 'include'`, sonst wird das Cookie nicht mitgeschickt."},"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Mandantengebundener Schluessel mit dem Praefix `nemix_`, auch als `Authorization: Bearer nemix_…` akzeptiert. Der Mandant ergibt sich aus dem Schluessel und wird NICHT im Pfad oder Rumpf uebergeben. Seit 11.08.2026 gilt der Umfang des Schluessels auch fuers LESEN: ohne `<bereich>:read` (oder ein weiter reichendes Recht auf denselben Bereich) antwortet die API mit 403 und nennt im Feld `requiredScope`, was fehlt. Ein Schluessel ganz ohne Umfaenge darf alles lesen und nichts aendern."},"webhookSignature":{"type":"apiKey","in":"header","name":"X-Nemix-Signature","description":"HMAC-Signatur eingehender Webhooks. Die Pruefung liegt im jeweiligen Handler, nicht in der Auth-Middleware."}},"schemas":{}},"security":[{"sessionCookie":[]},{"apiKey":[]}],"tags":[{"name":"2fa","x-displayName":"2FA","description":"5 Endpunkte unter `/api/v1/2fa` — 1 lesend, 4 schreibend."},{"name":"Activities","description":"4 Endpunkte unter `/api/v1/crm/activities` — 1 lesend, 3 schreibend."},{"name":"Address Types","description":"4 Endpunkte unter `/api/v1/address-types` — 1 lesend, 3 schreibend."},{"name":"Anpassungen","description":"2 Endpunkte unter `/api/v1/customizations` — ausschliesslich lesend."},{"name":"Bau · Abrechnung","description":"5 Endpunkte unter `/api/v1/bau-abrechnung` — 2 lesend, 3 schreibend."},{"name":"Bau · Aufmaß","description":"6 Endpunkte unter `/api/v1/aufmass` — 2 lesend, 4 schreibend."},{"name":"Bau · LV","description":"7 Endpunkte unter `/api/v1/lv` — 2 lesend, 5 schreibend."},{"name":"Bau · Nachträge","description":"6 Endpunkte unter `/api/v1/nachtragsangebote` — 2 lesend, 4 schreibend."},{"name":"Bau · ZUGFeRD","description":"10 Endpunkte unter `/api/v1/zugferd` — 5 lesend, 5 schreibend."},{"name":"Belege","description":"3 Endpunkte — 1 lesend, 2 schreibend."},{"name":"Betrieb","description":"2 Endpunkte — ausschliesslich lesend."},{"name":"Bonus","description":"11 Endpunkte unter `/api/v1/bonus` — 4 lesend, 7 schreibend."},{"name":"Budget","description":"1 Endpunkt unter `/api/v1/projects` — ausschliesslich lesend."},{"name":"CRM","description":"62 Endpunkte — 23 lesend, 39 schreibend."},{"name":"CRM · Health","description":"2 Endpunkte unter `/api/v1/customers` — ausschliesslich lesend."},{"name":"CRM · Statement","description":"1 Endpunkt unter `/api/v1/customers` — ausschliesslich lesend."},{"name":"Calls","description":"2 Endpunkte unter `/api/v1/call-transcripts` — ausschliesslich schreibend."},{"name":"Campaigns","description":"6 Endpunkte unter `/api/v1/crm/campaigns` — 2 lesend, 4 schreibend."},{"name":"Comments","description":"3 Endpunkte unter `/api/v1/documents` — 1 lesend, 2 schreibend."},{"name":"Contact Categories","description":"4 Endpunkte unter `/api/v1/contact-categories` — 1 lesend, 3 schreibend."},{"name":"Contact Types","description":"4 Endpunkte unter `/api/v1/contact-types` — 1 lesend, 3 schreibend."},{"name":"CreditNotes","description":"9 Endpunkte unter `/api/v1/credit-notes` — 3 lesend, 6 schreibend."},{"name":"Customer Addresses","description":"5 Endpunkte unter `/api/v1/customers` — 1 lesend, 4 schreibend."},{"name":"Customer Bank Accounts","description":"5 Endpunkte unter `/api/v1/customers` — 1 lesend, 4 schreibend."},{"name":"Customer-Portal","description":"81 Endpunkte — 31 lesend, 50 schreibend."},{"name":"Datenqualitaet","description":"2 Endpunkte unter `/api/v1/data-quality/issues` — 1 lesend, 1 schreibend."},{"name":"Deliveries","description":"13 Endpunkte unter `/api/v1/deliveries` — 4 lesend, 9 schreibend."},{"name":"DocumentChain","description":"6 Endpunkte unter `/api/v1/document-chain` — 2 lesend, 4 schreibend."},{"name":"Documents · Sharing","description":"5 Endpunkte — 3 lesend, 2 schreibend."},{"name":"Dossier","description":"1 Endpunkt unter `/api/v1/projects` — ausschliesslich lesend."},{"name":"Eigene Agenten","description":"5 Endpunkte unter `/api/v1/custom-agents` — 2 lesend, 3 schreibend."},{"name":"Eigene Module","description":"5 Endpunkte unter `/api/v1/custom-modules` — 2 lesend, 3 schreibend."},{"name":"Eigene Seiten","description":"4 Endpunkte unter `/api/v1/custom-pages` — 2 lesend, 2 schreibend."},{"name":"EmailSync","description":"7 Endpunkte unter `/api/v1/crm/email-sync` — 2 lesend, 5 schreibend."},{"name":"EmailTemplates","description":"4 Endpunkte unter `/api/v1/email-templates` — 1 lesend, 3 schreibend."},{"name":"Empfehlungen","description":"3 Endpunkte unter `/api/v1/referrals` — 1 lesend, 2 schreibend."},{"name":"Finance","description":"14 Endpunkte — ausschliesslich lesend."},{"name":"GAEB-LV","description":"8 Endpunkte unter `/api/v1/gaeb-lv` — 3 lesend, 5 schreibend."},{"name":"Gantt","description":"1 Endpunkt unter `/api/v1/projects` — ausschliesslich lesend."},{"name":"Geo","description":"1 Endpunkt unter `/api/v1/geo/postal` — ausschliesslich lesend."},{"name":"HR","description":"38 Endpunkte — 21 lesend, 17 schreibend."},{"name":"Historie","description":"1 Endpunkt unter `/api/v1/change-history` — ausschliesslich lesend."},{"name":"Inventory","description":"76 Endpunkte — 32 lesend, 44 schreibend."},{"name":"KI","description":"4 Endpunkte — 2 lesend, 2 schreibend."},{"name":"KI-Gedaechtnis","description":"7 Endpunkte unter `/api/v1/ai-brain` — 3 lesend, 4 schreibend."},{"name":"Knowledge Base","description":"20 Endpunkte unter `/api/v1/ai` — 5 lesend, 15 schreibend."},{"name":"Konditionen","description":"7 Endpunkte unter `/api/v1/konditionen` — 4 lesend, 3 schreibend."},{"name":"Konnektoren","description":"5 Endpunkte unter `/api/v1/connectors` — 3 lesend, 2 schreibend."},{"name":"Leads","description":"20 Endpunkte — 6 lesend, 14 schreibend."},{"name":"Lieferanten","description":"12 Endpunkte unter `/api/v1/lieferanten-bewertung` — 5 lesend, 7 schreibend."},{"name":"MRP","description":"10 Endpunkte unter `/api/v1/mrp` — 4 lesend, 6 schreibend."},{"name":"Mahnwesen","description":"5 Endpunkte unter `/api/v1/dunning/config` — 2 lesend, 3 schreibend."},{"name":"Number Ranges","description":"4 Endpunkte unter `/api/v1/number-ranges` — 1 lesend, 3 schreibend."},{"name":"Operations","description":"4 Endpunkte unter `/api/v1/ai` — ausschliesslich lesend."},{"name":"Procurement","description":"1 Endpunkt unter `/api/v1/ai/vendor-risk` — ausschliesslich lesend."},{"name":"Projects","description":"38 Endpunkte — 15 lesend, 23 schreibend."},{"name":"Projects · Capacity","description":"1 Endpunkt unter `/api/v1/projects/capacity` — ausschliesslich lesend."},{"name":"Projekte","description":"4 Endpunkte — 1 lesend, 3 schreibend."},{"name":"Provisionen","description":"8 Endpunkte unter `/api/v1/provisionen` — 4 lesend, 4 schreibend."},{"name":"QM","description":"47 Endpunkte — 21 lesend, 26 schreibend."},{"name":"Quality","description":"1 Endpunkt unter `/api/v1/ai/quality-predictor` — ausschliesslich lesend."},{"name":"RAG","description":"5 Endpunkte unter `/api/v1/rag-collections` — 2 lesend, 3 schreibend."},{"name":"Rahmen","description":"15 Endpunkte unter `/api/v1/rahmen` — 7 lesend, 8 schreibend."},{"name":"RahmenAbrufe","description":"6 Endpunkte unter `/api/v1/rahmenauftraege` — 3 lesend, 3 schreibend."},{"name":"Recurring Invoices","description":"5 Endpunkte unter `/api/v1/recurring-invoices` — 1 lesend, 4 schreibend."},{"name":"Sales","description":"6 Endpunkte unter `/api/v1/ai` — ausschliesslich lesend."},{"name":"Sales · Forecasts","description":"6 Endpunkte unter `/api/v1/forecasts` — 3 lesend, 3 schreibend."},{"name":"Sales · Territories","description":"5 Endpunkte unter `/api/v1/territories` — 2 lesend, 3 schreibend."},{"name":"Scoring","description":"4 Endpunkte — 2 lesend, 2 schreibend."},{"name":"Segments","description":"6 Endpunkte unter `/api/v1/crm/segments` — 3 lesend, 3 schreibend."},{"name":"Tags","description":"9 Endpunkte — 2 lesend, 7 schreibend."},{"name":"Tasks","description":"24 Endpunkte unter `/api/v1/tasks` — 10 lesend, 14 schreibend."},{"name":"TimeTracking","description":"5 Endpunkte — 3 lesend, 2 schreibend."},{"name":"VAT Validation","description":"1 Endpunkt unter `/api/v1/vat/validate` — ausschliesslich lesend."},{"name":"Vendors","description":"1 Endpunkt unter `/api/v1/vendors/scorecard` — ausschliesslich lesend."},{"name":"Versions","description":"2 Endpunkte unter `/api/v1/documents` — 1 lesend, 1 schreibend."},{"name":"Zeiterfassung","description":"4 Endpunkte unter `/api/v1/time-entries` — 2 lesend, 2 schreibend."},{"name":"_internal","x-displayName":"Internal","description":"3 Endpunkte — 1 lesend, 2 schreibend."},{"name":"account-schedules","x-displayName":"Account Schedules","description":"6 Endpunkte unter `/api/v1/account-schedules` — 3 lesend, 3 schreibend."},{"name":"accounting","x-displayName":"Accounting","description":"23 Endpunkte unter `/api/v1/accounting` — 13 lesend, 10 schreibend."},{"name":"accounting-periods","x-displayName":"Accounting Periods","description":"4 Endpunkte unter `/api/v1/accounting/perioden` — 2 lesend, 2 schreibend."},{"name":"activity","x-displayName":"Activity","description":"3 Endpunkte unter `/api/v1/activity` — 1 lesend, 2 schreibend."},{"name":"admin","x-displayName":"Admin","description":"236 Endpunkte — 117 lesend, 119 schreibend."},{"name":"agent-plans","x-displayName":"Agent Plans","description":"7 Endpunkte unter `/api/v1/ai/agent` — 4 lesend, 3 schreibend."},{"name":"agent-templates","x-displayName":"Agent Templates","description":"6 Endpunkte unter `/api/v1/ai/agent/templates` — 2 lesend, 4 schreibend."},{"name":"ai","x-displayName":"AI","description":"188 Endpunkte — 92 lesend, 96 schreibend."},{"name":"ai-agent","x-displayName":"AI Agent","description":"5 Endpunkte unter `/api/v1/ai/agent` — 2 lesend, 3 schreibend."},{"name":"ai-data-builder","x-displayName":"AI Data Builder","description":"5 Endpunkte unter `/api/v1/ai/data-builder` — 1 lesend, 4 schreibend."},{"name":"ai-data-ops","x-displayName":"AI Data Ops","description":"12 Endpunkte unter `/api/v1/ai-data-ops` — ausschliesslich schreibend."},{"name":"ai-rls-builder","x-displayName":"AI RLS Builder","description":"3 Endpunkte unter `/api/v1/ai/rls-builder` — 1 lesend, 2 schreibend."},{"name":"ai-token-usage","x-displayName":"AI Token Usage","description":"1 Endpunkt unter `/api/v1/billing/usage/tokens/current-month` — ausschliesslich lesend."},{"name":"ai-user-budget","x-displayName":"AI User Budget","description":"2 Endpunkte unter `/api/v1/billing/users` — 1 lesend, 1 schreibend."},{"name":"analytics","x-displayName":"Analytics","description":"13 Endpunkte unter `/api/v1/analytics` — ausschliesslich lesend."},{"name":"anlagen","x-displayName":"Anlagen","description":"13 Endpunkte unter `/api/v1/anlagen` — 8 lesend, 5 schreibend."},{"name":"api-docs","x-displayName":"API Docs","description":"3 Endpunkte unter `/api-docs` — ausschliesslich lesend."},{"name":"api-keys","x-displayName":"API Keys","description":"3 Endpunkte unter `/api/v1/api-keys` — 1 lesend, 2 schreibend."},{"name":"audit","x-displayName":"Audit","description":"2 Endpunkte — ausschliesslich lesend."},{"name":"audit-log","x-displayName":"Audit Log","description":"7 Endpunkte unter `/api/v1/audit` — 6 lesend, 1 schreibend."},{"name":"auth","x-displayName":"Auth","description":"66 Endpunkte — 29 lesend, 37 schreibend."},{"name":"banking","x-displayName":"Banking","description":"11 Endpunkte unter `/api/v1/banking` — 5 lesend, 6 schreibend."},{"name":"beta-crm","x-displayName":"Beta CRM","description":"4 Endpunkte unter `/api/v1/beta-crm/leads` — 1 lesend, 3 schreibend."},{"name":"billing","x-displayName":"Billing","description":"23 Endpunkte — 9 lesend, 14 schreibend."},{"name":"billing-overage","x-displayName":"Billing Overage","description":"1 Endpunkt unter `/api/v1/billing/overage/live` — ausschliesslich lesend."},{"name":"bom","x-displayName":"BOM","description":"16 Endpunkte unter `/api/v1/bom` — 7 lesend, 9 schreibend."},{"name":"booking","x-displayName":"Booking","description":"1 Endpunkt unter `/api/v1/documents` — ausschliesslich schreibend."},{"name":"budgets","x-displayName":"Budgets","description":"5 Endpunkte unter `/api/v1/einkauf/budgets` — 1 lesend, 4 schreibend."},{"name":"build-docs","x-displayName":"Build Docs","description":"5 Endpunkte unter `/api/v1/ai-build-docs` — 2 lesend, 3 schreibend."},{"name":"bulk","x-displayName":"Bulk","description":"11 Endpunkte — ausschliesslich schreibend."},{"name":"calendar","x-displayName":"Calendar","description":"6 Endpunkte unter `/api/v1/calendar/events` — 3 lesend, 3 schreibend."},{"name":"catalog","x-displayName":"Catalog","description":"6 Endpunkte unter `/api/v1/einkauf` — 2 lesend, 4 schreibend."},{"name":"citations","x-displayName":"Citations","description":"1 Endpunkt unter `/api/v1/ai/citations` — ausschliesslich lesend."},{"name":"compliance","x-displayName":"Compliance","description":"5 Endpunkte — 4 lesend, 1 schreibend."},{"name":"contacts","x-displayName":"Contacts","description":"6 Endpunkte unter `/api/v1/contacts` — 2 lesend, 4 schreibend."},{"name":"contracts","x-displayName":"Contracts","description":"11 Endpunkte — 5 lesend, 6 schreibend."},{"name":"conversations","x-displayName":"Conversations","description":"6 Endpunkte unter `/api/v1/ai/conversations` — 2 lesend, 4 schreibend."},{"name":"costs","x-displayName":"Costs","description":"6 Endpunkte — ausschliesslich lesend."},{"name":"custom-agents","x-displayName":"Custom Agents","description":"2 Endpunkte unter `/api/v1/custom-agents` — 1 lesend, 1 schreibend."},{"name":"custom-entities","x-displayName":"Custom Entities","description":"9 Endpunkte unter `/api/v1/custom-entities` — 3 lesend, 6 schreibend."},{"name":"custom-fields","x-displayName":"Custom Fields","description":"9 Endpunkte unter `/api/v1/custom-fields` — 5 lesend, 4 schreibend."},{"name":"custom-fields-v2","x-displayName":"Custom Fields V2","description":"3 Endpunkte unter `/api/v1/custom-entities` — ausschliesslich schreibend."},{"name":"customers","x-displayName":"Customers","description":"44 Endpunkte — 18 lesend, 26 schreibend."},{"name":"customizing-introspect","x-displayName":"Customizing Introspect","description":"1 Endpunkt unter `/api/v1/customizing-introspect/overview` — ausschliesslich lesend."},{"name":"dashboard","x-displayName":"Dashboard","description":"9 Endpunkte unter `/api/v1/dashboard` — 5 lesend, 4 schreibend."},{"name":"dashboard-config","x-displayName":"Dashboard Config","description":"4 Endpunkte unter `/api/v1/dashboard-config` — 2 lesend, 2 schreibend."},{"name":"datev","x-displayName":"DATEV","description":"11 Endpunkte — 6 lesend, 5 schreibend."},{"name":"decision-tables","x-displayName":"Decision Tables","description":"10 Endpunkte unter `/api/v1/decision-tables` — 3 lesend, 7 schreibend."},{"name":"demo","x-displayName":"Demo","description":"3 Endpunkte unter `/api/v1/demo` — 1 lesend, 2 schreibend."},{"name":"developer","x-displayName":"Developer","description":"6 Endpunkte — 2 lesend, 4 schreibend."},{"name":"dimensions","x-displayName":"Dimensions","description":"14 Endpunkte unter `/api/v1/dimensions` — 5 lesend, 9 schreibend."},{"name":"dms","x-displayName":"DMS","description":"68 Endpunkte — 21 lesend, 47 schreibend."},{"name":"doc-templates","x-displayName":"Doc Templates","description":"6 Endpunkte unter `/api/v1/doc-templates` — 2 lesend, 4 schreibend."},{"name":"documents","x-displayName":"Documents","description":"48 Endpunkte — 14 lesend, 34 schreibend."},{"name":"e-rechnung","x-displayName":"E-Rechnung","description":"2 Endpunkte unter `/api/v1/einkauf/einvoice` — ausschliesslich schreibend."},{"name":"einkauf","x-displayName":"Einkauf","description":"34 Endpunkte — 12 lesend, 22 schreibend."},{"name":"elster","x-displayName":"ELSTER","description":"33 Endpunkte — 15 lesend, 18 schreibend."},{"name":"email","x-displayName":"Email","description":"2 Endpunkte — ausschliesslich schreibend."},{"name":"email-inbox","x-displayName":"Email Inbox","description":"5 Endpunkte — 2 lesend, 3 schreibend."},{"name":"email-tracking","x-displayName":"Email Tracking","description":"1 Endpunkt unter `/api/v1/email-tracking/pixel` — ausschliesslich lesend."},{"name":"embed","x-displayName":"Embed","description":"4 Endpunkte — 2 lesend, 2 schreibend."},{"name":"employees","x-displayName":"Employees","description":"12 Endpunkte unter `/api/v1/employees` — 5 lesend, 7 schreibend."},{"name":"entity-rules","x-displayName":"Entity Rules","description":"4 Endpunkte unter `/api/v1/entity-rules` — 1 lesend, 3 schreibend."},{"name":"exports","x-displayName":"Exports","description":"3 Endpunkte — 2 lesend, 1 schreibend."},{"name":"flags","x-displayName":"Flags","description":"8 Endpunkte — 4 lesend, 4 schreibend."},{"name":"folders","x-displayName":"Folders","description":"8 Endpunkte — 1 lesend, 7 schreibend."},{"name":"gaeb","x-displayName":"GAEB","description":"5 Endpunkte unter `/api/v1/gaeb` — 1 lesend, 4 schreibend."},{"name":"gdpr","x-displayName":"Gdpr","description":"12 Endpunkte unter `/api/v1/gdpr` — 5 lesend, 7 schreibend."},{"name":"gobd","x-displayName":"GoBD","description":"4 Endpunkte — ausschliesslich lesend."},{"name":"health","x-displayName":"Health","description":"41 Endpunkte — ausschliesslich lesend."},{"name":"history","x-displayName":"History","description":"2 Endpunkte — ausschliesslich lesend."},{"name":"immo","x-displayName":"Immo","description":"54 Endpunkte unter `/api/v1/immo` — 27 lesend, 27 schreibend."},{"name":"impersonate","x-displayName":"Impersonate","description":"4 Endpunkte — ausschliesslich schreibend."},{"name":"imports","x-displayName":"Imports","description":"17 Endpunkte — 2 lesend, 15 schreibend."},{"name":"inbox-addresses","x-displayName":"Inbox Addresses","description":"6 Endpunkte unter `/api/v1/inbox-addresses` — 3 lesend, 3 schreibend."},{"name":"industry-packs","x-displayName":"Industry Packs","description":"4 Endpunkte unter `/api/v1/industry-packs` — 2 lesend, 2 schreibend."},{"name":"instances","x-displayName":"Instances","description":"7 Endpunkte — 3 lesend, 4 schreibend."},{"name":"integrations","x-displayName":"Integrations","description":"26 Endpunkte unter `/api/v1/integrations` — 17 lesend, 9 schreibend."},{"name":"inventur","x-displayName":"Inventur","description":"12 Endpunkte unter `/api/v1/inventur` — 4 lesend, 8 schreibend."},{"name":"invoices","x-displayName":"Invoices","description":"38 Endpunkte — 18 lesend, 20 schreibend."},{"name":"journal-entries","x-displayName":"Journal Entries","description":"5 Endpunkte unter `/api/v1/journal-entries` — 3 lesend, 2 schreibend."},{"name":"kill-switch","x-displayName":"Kill Switch","description":"4 Endpunkte — 2 lesend, 2 schreibend."},{"name":"kostenrechnung","x-displayName":"Kostenrechnung","description":"18 Endpunkte unter `/api/v1/kostenrechnung` — 8 lesend, 10 schreibend."},{"name":"lager","x-displayName":"Lager","description":"5 Endpunkte unter `/api/v1/lager` — 4 lesend, 1 schreibend."},{"name":"layers","x-displayName":"Layers","description":"9 Endpunkte unter `/api/v1/layers` — 4 lesend, 5 schreibend."},{"name":"limits","x-displayName":"Limits","description":"4 Endpunkte — 2 lesend, 2 schreibend."},{"name":"list-configs","x-displayName":"List Configs","description":"2 Endpunkte unter `/api/v1/list-configs` — 1 lesend, 1 schreibend."},{"name":"lot-tracking","x-displayName":"Lot Tracking","description":"17 Endpunkte unter `/api/v1/lot-tracking` — 8 lesend, 9 schreibend."},{"name":"maengel","x-displayName":"Maengel","description":"9 Endpunkte — 5 lesend, 4 schreibend."},{"name":"manufacturing","x-displayName":"Manufacturing","description":"15 Endpunkte unter `/api/v1/manufacturing/work-orders` — 5 lesend, 10 schreibend."},{"name":"marketplace","x-displayName":"Marketplace","description":"6 Endpunkte unter `/api/v1/marketplace` — 3 lesend, 3 schreibend."},{"name":"matching","x-displayName":"Matching","description":"4 Endpunkte unter `/api/v1/einkauf` — 1 lesend, 3 schreibend."},{"name":"mcp","x-displayName":"MCP","description":"16 Endpunkte unter `/api/v1/mcp` — 8 lesend, 8 schreibend."},{"name":"mcp-tokens","x-displayName":"MCP Tokens","description":"3 Endpunkte unter `/api/v1/mcp/tokens` — 1 lesend, 2 schreibend."},{"name":"me","x-displayName":"Me","description":"6 Endpunkte unter `/api/v1/me` — 4 lesend, 2 schreibend."},{"name":"metrics","x-displayName":"Metrics","description":"3 Endpunkte — ausschliesslich lesend."},{"name":"modules","x-displayName":"Modules","description":"5 Endpunkte — 3 lesend, 2 schreibend."},{"name":"multi-tenant","x-displayName":"Multi Tenant","description":"4 Endpunkte unter `/api/v1/billing/tenants` — 1 lesend, 3 schreibend."},{"name":"n8n","x-displayName":"N8n","description":"5 Endpunkte unter `/api/v1/webhooks/n8n` — 2 lesend, 3 schreibend."},{"name":"notes","x-displayName":"Notes","description":"6 Endpunkte — 2 lesend, 4 schreibend."},{"name":"notifications","x-displayName":"Notifications","description":"4 Endpunkte unter `/api/v1/notifications` — 1 lesend, 3 schreibend."},{"name":"ocr","x-displayName":"OCR","description":"4 Endpunkte unter `/api/v1/ocr` — ausschliesslich schreibend."},{"name":"onboarding","x-displayName":"Onboarding","description":"13 Endpunkte — 5 lesend, 8 schreibend."},{"name":"orders","x-displayName":"Orders","description":"29 Endpunkte unter `/api/v1/orders` — 6 lesend, 23 schreibend."},{"name":"organizations","x-displayName":"Organizations","description":"40 Endpunkte — 22 lesend, 18 schreibend."},{"name":"payroll","x-displayName":"Payroll","description":"12 Endpunkte unter `/api/v1/payroll` — 6 lesend, 6 schreibend."},{"name":"permissions","x-displayName":"Permissions","description":"41 Endpunkte — 19 lesend, 22 schreibend."},{"name":"pipeline","x-displayName":"Pipeline","description":"13 Endpunkte unter `/api/v1/pipeline` — 4 lesend, 9 schreibend."},{"name":"platform","x-displayName":"Platform","description":"1 Endpunkt unter `/api/v1/capabilities` — ausschliesslich lesend."},{"name":"posting-groups","x-displayName":"Posting Groups","description":"17 Endpunkte unter `/api/v1/posting-groups` — 7 lesend, 10 schreibend."},{"name":"proactive-insights","x-displayName":"Proactive Insights","description":"2 Endpunkte unter `/api/v1/proactive-insights` — 1 lesend, 1 schreibend."},{"name":"production","x-displayName":"Production","description":"5 Endpunkte unter `/api/v1/production` — 2 lesend, 3 schreibend."},{"name":"prozess-charts","x-displayName":"Prozess Charts","description":"9 Endpunkte unter `/api/v1/prozess-charts` — 4 lesend, 5 schreibend."},{"name":"purchasing","x-displayName":"Purchasing","description":"21 Endpunkte — 11 lesend, 10 schreibend."},{"name":"push","x-displayName":"Push","description":"8 Endpunkte — 2 lesend, 6 schreibend."},{"name":"quotes","x-displayName":"Quotes","description":"20 Endpunkte unter `/api/v1/quotes` — 6 lesend, 14 schreibend."},{"name":"rag-query","x-displayName":"RAG Query","description":"1 Endpunkt unter `/api/v1/rag-query` — ausschliesslich schreibend."},{"name":"rag-wizard","x-displayName":"RAG Wizard","description":"8 Endpunkte unter `/api/v1/ai/rag` — 2 lesend, 6 schreibend."},{"name":"reports","x-displayName":"Reports","description":"3 Endpunkte unter `/api/v1/reports/reports` — 1 lesend, 2 schreibend."},{"name":"rma","x-displayName":"RMA","description":"8 Endpunkte unter `/api/v1/rma` — 3 lesend, 5 schreibend."},{"name":"rollouts","x-displayName":"Rollouts","description":"10 Endpunkte — 2 lesend, 8 schreibend."},{"name":"sandbox","x-displayName":"Sandbox","description":"5 Endpunkte — 2 lesend, 3 schreibend."},{"name":"saved-searches","x-displayName":"Saved Searches","description":"5 Endpunkte unter `/api/v1/saved-searches` — 2 lesend, 3 schreibend."},{"name":"saved-views","x-displayName":"Saved Views","description":"5 Endpunkte unter `/api/v1/saved-views` — 1 lesend, 4 schreibend."},{"name":"scorecard","x-displayName":"Scorecard","description":"1 Endpunkt unter `/api/v1/einkauf/lieferanten` — ausschliesslich lesend."},{"name":"search","x-displayName":"Search","description":"4 Endpunkte unter `/api/v1/search` — 2 lesend, 2 schreibend."},{"name":"sequence-audit","x-displayName":"Sequence Audit","description":"3 Endpunkte unter `/api/v1/sequence-audit` — 2 lesend, 1 schreibend."},{"name":"service-visits","x-displayName":"Service Visits","description":"5 Endpunkte unter `/api/v1/service-visits` — 2 lesend, 3 schreibend."},{"name":"settings","x-displayName":"Settings","description":"57 Endpunkte — 23 lesend, 34 schreibend."},{"name":"sidebar-prefs","x-displayName":"Sidebar Prefs","description":"3 Endpunkte unter `/api/v1/sidebar-prefs` — 1 lesend, 2 schreibend."},{"name":"signatures","x-displayName":"Signatures","description":"7 Endpunkte unter `/api/v1/signatures` — 2 lesend, 5 schreibend."},{"name":"spend","x-displayName":"Spend","description":"3 Endpunkte unter `/api/v1/einkauf/spend-analysis` — ausschliesslich lesend."},{"name":"stammdaten","x-displayName":"Stammdaten","description":"4 Endpunkte — 2 lesend, 2 schreibend."},{"name":"status","x-displayName":"Status","description":"8 Endpunkte — 2 lesend, 6 schreibend."},{"name":"stb-portal","x-displayName":"Stb Portal","description":"26 Endpunkte — 14 lesend, 12 schreibend."},{"name":"supplier","x-displayName":"Supplier","description":"12 Endpunkte — 6 lesend, 6 schreibend."},{"name":"support","x-displayName":"Support","description":"4 Endpunkte — 2 lesend, 2 schreibend."},{"name":"system","x-displayName":"System","description":"1 Endpunkt unter `/api/v1/system/status` — ausschliesslich lesend."},{"name":"task-display-prefs","x-displayName":"Task Display Prefs","description":"2 Endpunkte unter `/api/v1/task-display-prefs` — 1 lesend, 1 schreibend."},{"name":"telegram","x-displayName":"Telegram","description":"1 Endpunkt unter `/api/v1/telegram/webhook` — ausschliesslich schreibend."},{"name":"telemetry","x-displayName":"Telemetry","description":"1 Endpunkt unter `/api/v1/ai/agent/telemetry` — ausschliesslich lesend."},{"name":"templates","x-displayName":"Templates","description":"8 Endpunkte unter `/api/v1/ai/templates` — 2 lesend, 6 schreibend."},{"name":"tenant","x-displayName":"Tenant","description":"18 Endpunkte unter `/api/v1/tenant/ai` — 10 lesend, 8 schreibend."},{"name":"tenants","x-displayName":"Tenants","description":"62 Endpunkte — 30 lesend, 32 schreibend."},{"name":"theme","x-displayName":"Theme","description":"2 Endpunkte unter `/api/v1/theme` — 1 lesend, 1 schreibend."},{"name":"tickets","x-displayName":"Tickets","description":"9 Endpunkte unter `/api/v1/tickets` — 3 lesend, 6 schreibend."},{"name":"timesheets","x-displayName":"Timesheets","description":"8 Endpunkte unter `/api/v1/timesheets` — 4 lesend, 4 schreibend."},{"name":"ui-configs","x-displayName":"UI Configs","description":"2 Endpunkte unter `/api/v1/ui-configs` — 1 lesend, 1 schreibend."},{"name":"undo","x-displayName":"Undo","description":"3 Endpunkte unter `/api/v1/ai` — 1 lesend, 2 schreibend."},{"name":"uploads","x-displayName":"Uploads","description":"1 Endpunkt unter `/api/v1/uploads/presign` — ausschliesslich schreibend."},{"name":"usage","x-displayName":"Usage","description":"9 Endpunkte — 7 lesend, 2 schreibend."},{"name":"user-views","x-displayName":"User Views","description":"5 Endpunkte unter `/api/v1/user-views` — 1 lesend, 4 schreibend."},{"name":"users","x-displayName":"Users","description":"14 Endpunkte — 7 lesend, 7 schreibend."},{"name":"vision","x-displayName":"Vision","description":"1 Endpunkt unter `/api/v1/ai/vision` — ausschliesslich schreibend."},{"name":"voice","x-displayName":"Voice","description":"30 Endpunkte — 11 lesend, 19 schreibend."},{"name":"warehouse","x-displayName":"Warehouse","description":"5 Endpunkte unter `/api/v1/warehouse` — 2 lesend, 3 schreibend."},{"name":"webhooks","x-displayName":"Webhooks","description":"22 Endpunkte unter `/api/v1/webhooks` — 6 lesend, 16 schreibend."},{"name":"whatsapp","x-displayName":"Whatsapp","description":"5 Endpunkte unter `/api/v1/whatsapp` — 1 lesend, 4 schreibend."},{"name":"workflow-builder","x-displayName":"Workflow Builder","description":"2 Endpunkte unter `/api/v1/ai/workflow-builder` — ausschliesslich schreibend."},{"name":"workflows","x-displayName":"Workflows","description":"18 Endpunkte — 7 lesend, 11 schreibend."},{"name":"zugferd","x-displayName":"ZUGFeRD","description":"2 Endpunkte unter `/api/v1/documents` — 1 lesend, 1 schreibend."}],"paths":{"/health":{"get":{"responses":{"200":{"description":"Herkunftsauskunft ohne ?deep=1, sonst das Ergebnis der Tiefenpruefung","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"type":"string","const":"ok"},"service":{"type":"string","const":"nemix-erp-api"},"version":{"type":"string","description":"Commit dieses Containers aus GIT_COMMIT_SHA; \"unknown\" ohne CI-Build, keine feste Zahl"},"commit":{"type":"string","description":"Derselbe Wert wie version, additiv gefuehrt"},"builtAt":{"type":"string","description":"Bauzeitpunkt des Abbilds aus IMAGE_BUILD_DATE; \"unknown\" ohne CI-Build"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Antwort"},"uptime":{"type":"number","description":"Laufzeit des Prozesses in Sekunden"}},"required":["status","service","version","commit","builtAt","timestamp","uptime"]},{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn alle Einzelpruefungen durchliefen"},"duration_ms":{"type":"number","description":"Gesamtdauer aller Pruefungen in Millisekunden"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"s3":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"anthropic":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"meili":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"queue":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis","s3","anthropic","meili","queue"],"description":"Die sechs Abhaengigkeiten einzeln"}},"required":["ok","duration_ms","checks"]}]},"example":{"status":"ok","service":"nemix-erp-api","version":"string","commit":"string","builtAt":"string","timestamp":"2026-01-01T12:00:00.000Z","uptime":0}}}},"503":{"description":"Deep check failed (with fail-fast=1)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn alle Einzelpruefungen durchliefen"},"duration_ms":{"type":"number","description":"Gesamtdauer aller Pruefungen in Millisekunden"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"s3":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"anthropic":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"meili":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"queue":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis","s3","anthropic","meili","queue"],"description":"Die sechs Abhaengigkeiten einzeln"}},"required":["ok","duration_ms","checks"]}}}}},"operationId":"getHealth","tags":["health"],"parameters":[],"description":"Service health check. Ohne Parameter kommt eine Herkunftsauskunft ueber DIESEN Container (Commit aus GIT_COMMIT_SHA, Bauzeitpunkt, Laufzeit) — ohne jede Abhaengigkeitspruefung; dieser Fall ist IMMER 200. Mit `?deep=1` (oder `deep=true`) laufen stattdessen die sechs Abhaengigkeitspruefungen (db, redis, s3, anthropic, meili, queue) und der Rumpf hat eine ANDERE Form. Auch dann bleibt es 200 — erst zusaetzlich `?fail-fast=1` macht aus einer fehlgeschlagenen Tiefenpruefung ein 503.","summary":"Service health check","x-nemix-summary-source":"description:first-sentence","security":[]}},"/health/migrations":{"get":{"responses":{"200":{"description":"Alle Kern-Migrationen angewendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn keine Kern-Migration mehr offen ist"},"gesamt":{"type":"integer","minimum":0,"description":"Anzahl der Migrationen in der Registry (SOLL)"},"angewendet":{"type":"integer","minimum":0,"description":"Davon in public._root_migrations eingetragen"},"offen":{"type":"integer","minimum":0,"description":"Noch nicht eingetragene Migrationen"},"inDatenbankUnbekannt":{"type":"integer","minimum":0,"description":"Eintraege, die die Registry nicht kennt — Zeichen fuer ein Rueckwaertsdeploy"},"coreSchemaVersion":{"anyOf":[{"type":"string"},{"type":"number"}],"description":"Erwartete Version des Kern-Schemas"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"},"offeneMigrationen":{"type":"array","items":{"type":"string"},"description":"Die Namen der offenen Migrationen — NUR mit gueltigem METRICS_SCRAPE_TOKEN als Bearer"}},"required":["ok","gesamt","angewendet","offen","inDatenbankUnbekannt","coreSchemaVersion","timestamp"]},"example":{"ok":true,"gesamt":0,"angewendet":0,"offen":0,"inDatenbankUnbekannt":0,"coreSchemaVersion":"string","timestamp":"2026-01-01T12:00:00.000Z","offeneMigrationen":["string"]}}}},"503":{"description":"Migrationen offen ODER Datenbank/Tracking-Tabelle nicht lesbar","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn keine Kern-Migration mehr offen ist"},"gesamt":{"type":"integer","minimum":0,"description":"Anzahl der Migrationen in der Registry (SOLL)"},"angewendet":{"type":"integer","minimum":0,"description":"Davon in public._root_migrations eingetragen"},"offen":{"type":"integer","minimum":0,"description":"Noch nicht eingetragene Migrationen"},"inDatenbankUnbekannt":{"type":"integer","minimum":0,"description":"Eintraege, die die Registry nicht kennt — Zeichen fuer ein Rueckwaertsdeploy"},"coreSchemaVersion":{"anyOf":[{"type":"string"},{"type":"number"}],"description":"Erwartete Version des Kern-Schemas"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"},"offeneMigrationen":{"type":"array","items":{"type":"string"},"description":"Die Namen der offenen Migrationen — NUR mit gueltigem METRICS_SCRAPE_TOKEN als Bearer"}},"required":["ok","gesamt","angewendet","offen","inDatenbankUnbekannt","coreSchemaVersion","timestamp"]},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"grund":{"type":"string","enum":["datenbank_nicht_erreichbar","tracking_tabelle_fehlt","tracking_tabelle_nicht_lesbar"],"description":"Woran das Lesen scheiterte"},"hinweis":{"type":"string","description":"Naechster Schritt im Klartext; fehlt, wenn schon der Datenbank-Client fehlte"}},"required":["ok","grund"]}]}}}}},"operationId":"getHealthMigrations","tags":["health"],"parameters":[],"summary":"Migration state of the core schema: registry vs. applied","description":"Migrationsstand des Kern-Schemas: SOLL (Registry) gegen IST (public._root_migrations). Mit METRICS_SCRAPE_TOKEN zusaetzlich die Namen der offenen Migrationen.","security":[]}},"/health/ready":{"get":{"responses":{"200":{"description":"Ready — beide Bausteine antworten","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean","description":"true, wenn Datenbank UND Zwischenspeicher antworten"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis"],"description":"Die beiden Einzelpruefungen, je mit 1500 ms Zeitgrenze"}},"required":["ready","checks"]},"example":{"ready":true,"checks":{"db":{"ok":true,"ms":0,"error":"string"},"redis":{"ok":true,"ms":0,"error":"string"}}}}}},"503":{"description":"Not ready — mindestens ein Baustein antwortet nicht","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean","description":"true, wenn Datenbank UND Zwischenspeicher antworten"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis"],"description":"Die beiden Einzelpruefungen, je mit 1500 ms Zeitgrenze"}},"required":["ready","checks"]}}}}},"operationId":"getHealthReady","tags":["health"],"parameters":[],"description":"Readiness check — fragt Datenbank und Zwischenspeicher parallel mit je 1500 ms Zeitgrenze. `ready` ist nur true, wenn BEIDE antworten; ein fehlender Baustein gilt als \"not configured\" und macht die Antwort 503. Geprueft wird die Erreichbarkeit, NICHT der Migrationsstand — dafuer gibt es `/health/migrations`. Anders als `/health/live` ist diese Sonde also von aussen abhaengig.","summary":"Readiness check","x-nemix-summary-source":"description:first-sentence","security":[]}},"/health/live":{"get":{"responses":{"200":{"description":"Alive — der Prozess antwortet","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"live"},"pid":{"type":"integer","description":"Prozesskennung"},"uptime":{"type":"number","description":"Laufzeit des Prozesses in Sekunden"}},"required":["status","pid","uptime"]},"example":{"status":"live","pid":0,"uptime":0}}}}},"operationId":"getHealthLive","tags":["health"],"parameters":[],"description":"Liveness probe — beruehrt nichts ausserhalb des Prozesses: kein Datenbank-, kein Zwischenspeicher-Zugriff. Die Antwort nennt Prozesskennung und Laufzeit und ist immer 200, solange die Ereignisschleife ueberhaupt noch antwortet. Sie sagt NICHTS darueber, ob der Dienst arbeitsfaehig ist — dafuer `/health/ready`.","summary":"Liveness probe","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/health":{"get":{"responses":{"200":{"description":"Herkunftsauskunft ohne ?deep=1, sonst das Ergebnis der Tiefenpruefung","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"type":"string","const":"ok"},"service":{"type":"string","const":"nemix-erp-api"},"version":{"type":"string","description":"Commit dieses Containers aus GIT_COMMIT_SHA; \"unknown\" ohne CI-Build, keine feste Zahl"},"commit":{"type":"string","description":"Derselbe Wert wie version, additiv gefuehrt"},"builtAt":{"type":"string","description":"Bauzeitpunkt des Abbilds aus IMAGE_BUILD_DATE; \"unknown\" ohne CI-Build"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Antwort"},"uptime":{"type":"number","description":"Laufzeit des Prozesses in Sekunden"}},"required":["status","service","version","commit","builtAt","timestamp","uptime"]},{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn alle Einzelpruefungen durchliefen"},"duration_ms":{"type":"number","description":"Gesamtdauer aller Pruefungen in Millisekunden"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"s3":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"anthropic":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"meili":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"queue":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis","s3","anthropic","meili","queue"],"description":"Die sechs Abhaengigkeiten einzeln"}},"required":["ok","duration_ms","checks"]}]},"example":{"status":"ok","service":"nemix-erp-api","version":"string","commit":"string","builtAt":"string","timestamp":"2026-01-01T12:00:00.000Z","uptime":0}}}},"503":{"description":"Deep check failed (with fail-fast=1)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn alle Einzelpruefungen durchliefen"},"duration_ms":{"type":"number","description":"Gesamtdauer aller Pruefungen in Millisekunden"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"s3":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"anthropic":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"meili":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"queue":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis","s3","anthropic","meili","queue"],"description":"Die sechs Abhaengigkeiten einzeln"}},"required":["ok","duration_ms","checks"]}}}}},"operationId":"getApiHealth","tags":["health"],"parameters":[],"description":"Service health check. Ohne Parameter kommt eine Herkunftsauskunft ueber DIESEN Container (Commit aus GIT_COMMIT_SHA, Bauzeitpunkt, Laufzeit) — ohne jede Abhaengigkeitspruefung; dieser Fall ist IMMER 200. Mit `?deep=1` (oder `deep=true`) laufen stattdessen die sechs Abhaengigkeitspruefungen (db, redis, s3, anthropic, meili, queue) und der Rumpf hat eine ANDERE Form. Auch dann bleibt es 200 — erst zusaetzlich `?fail-fast=1` macht aus einer fehlgeschlagenen Tiefenpruefung ein 503.","summary":"Service health check","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/health/migrations":{"get":{"responses":{"200":{"description":"Alle Kern-Migrationen angewendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn keine Kern-Migration mehr offen ist"},"gesamt":{"type":"integer","minimum":0,"description":"Anzahl der Migrationen in der Registry (SOLL)"},"angewendet":{"type":"integer","minimum":0,"description":"Davon in public._root_migrations eingetragen"},"offen":{"type":"integer","minimum":0,"description":"Noch nicht eingetragene Migrationen"},"inDatenbankUnbekannt":{"type":"integer","minimum":0,"description":"Eintraege, die die Registry nicht kennt — Zeichen fuer ein Rueckwaertsdeploy"},"coreSchemaVersion":{"anyOf":[{"type":"string"},{"type":"number"}],"description":"Erwartete Version des Kern-Schemas"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"},"offeneMigrationen":{"type":"array","items":{"type":"string"},"description":"Die Namen der offenen Migrationen — NUR mit gueltigem METRICS_SCRAPE_TOKEN als Bearer"}},"required":["ok","gesamt","angewendet","offen","inDatenbankUnbekannt","coreSchemaVersion","timestamp"]},"example":{"ok":true,"gesamt":0,"angewendet":0,"offen":0,"inDatenbankUnbekannt":0,"coreSchemaVersion":"string","timestamp":"2026-01-01T12:00:00.000Z","offeneMigrationen":["string"]}}}},"503":{"description":"Migrationen offen ODER Datenbank/Tracking-Tabelle nicht lesbar","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn keine Kern-Migration mehr offen ist"},"gesamt":{"type":"integer","minimum":0,"description":"Anzahl der Migrationen in der Registry (SOLL)"},"angewendet":{"type":"integer","minimum":0,"description":"Davon in public._root_migrations eingetragen"},"offen":{"type":"integer","minimum":0,"description":"Noch nicht eingetragene Migrationen"},"inDatenbankUnbekannt":{"type":"integer","minimum":0,"description":"Eintraege, die die Registry nicht kennt — Zeichen fuer ein Rueckwaertsdeploy"},"coreSchemaVersion":{"anyOf":[{"type":"string"},{"type":"number"}],"description":"Erwartete Version des Kern-Schemas"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"},"offeneMigrationen":{"type":"array","items":{"type":"string"},"description":"Die Namen der offenen Migrationen — NUR mit gueltigem METRICS_SCRAPE_TOKEN als Bearer"}},"required":["ok","gesamt","angewendet","offen","inDatenbankUnbekannt","coreSchemaVersion","timestamp"]},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"grund":{"type":"string","enum":["datenbank_nicht_erreichbar","tracking_tabelle_fehlt","tracking_tabelle_nicht_lesbar"],"description":"Woran das Lesen scheiterte"},"hinweis":{"type":"string","description":"Naechster Schritt im Klartext; fehlt, wenn schon der Datenbank-Client fehlte"}},"required":["ok","grund"]}]}}}}},"operationId":"getApiHealthMigrations","tags":["health"],"parameters":[],"summary":"Migration state of the core schema: registry vs. applied","description":"Migrationsstand des Kern-Schemas: SOLL (Registry) gegen IST (public._root_migrations). Mit METRICS_SCRAPE_TOKEN zusaetzlich die Namen der offenen Migrationen.","security":[]}},"/api/health/ready":{"get":{"responses":{"200":{"description":"Ready — beide Bausteine antworten","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean","description":"true, wenn Datenbank UND Zwischenspeicher antworten"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis"],"description":"Die beiden Einzelpruefungen, je mit 1500 ms Zeitgrenze"}},"required":["ready","checks"]},"example":{"ready":true,"checks":{"db":{"ok":true,"ms":0,"error":"string"},"redis":{"ok":true,"ms":0,"error":"string"}}}}}},"503":{"description":"Not ready — mindestens ein Baustein antwortet nicht","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean","description":"true, wenn Datenbank UND Zwischenspeicher antworten"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis"],"description":"Die beiden Einzelpruefungen, je mit 1500 ms Zeitgrenze"}},"required":["ready","checks"]}}}}},"operationId":"getApiHealthReady","tags":["health"],"parameters":[],"description":"Readiness check — fragt Datenbank und Zwischenspeicher parallel mit je 1500 ms Zeitgrenze. `ready` ist nur true, wenn BEIDE antworten; ein fehlender Baustein gilt als \"not configured\" und macht die Antwort 503. Geprueft wird die Erreichbarkeit, NICHT der Migrationsstand — dafuer gibt es `/health/migrations`. Anders als `/health/live` ist diese Sonde also von aussen abhaengig.","summary":"Readiness check","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/health/live":{"get":{"responses":{"200":{"description":"Alive — der Prozess antwortet","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"live"},"pid":{"type":"integer","description":"Prozesskennung"},"uptime":{"type":"number","description":"Laufzeit des Prozesses in Sekunden"}},"required":["status","pid","uptime"]},"example":{"status":"live","pid":0,"uptime":0}}}}},"operationId":"getApiHealthLive","tags":["health"],"parameters":[],"description":"Liveness probe — beruehrt nichts ausserhalb des Prozesses: kein Datenbank-, kein Zwischenspeicher-Zugriff. Die Antwort nennt Prozesskennung und Laufzeit und ist immer 200, solange die Ereignisschleife ueberhaupt noch antwortet. Sie sagt NICHTS darueber, ob der Dienst arbeitsfaehig ist — dafuer `/health/ready`.","summary":"Liveness probe","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/v1/health":{"get":{"responses":{"200":{"description":"Herkunftsauskunft ohne ?deep=1, sonst das Ergebnis der Tiefenpruefung","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"type":"string","const":"ok"},"service":{"type":"string","const":"nemix-erp-api"},"version":{"type":"string","description":"Commit dieses Containers aus GIT_COMMIT_SHA; \"unknown\" ohne CI-Build, keine feste Zahl"},"commit":{"type":"string","description":"Derselbe Wert wie version, additiv gefuehrt"},"builtAt":{"type":"string","description":"Bauzeitpunkt des Abbilds aus IMAGE_BUILD_DATE; \"unknown\" ohne CI-Build"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Antwort"},"uptime":{"type":"number","description":"Laufzeit des Prozesses in Sekunden"}},"required":["status","service","version","commit","builtAt","timestamp","uptime"]},{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn alle Einzelpruefungen durchliefen"},"duration_ms":{"type":"number","description":"Gesamtdauer aller Pruefungen in Millisekunden"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"s3":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"anthropic":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"meili":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"queue":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis","s3","anthropic","meili","queue"],"description":"Die sechs Abhaengigkeiten einzeln"}},"required":["ok","duration_ms","checks"]}]},"example":{"status":"ok","service":"nemix-erp-api","version":"string","commit":"string","builtAt":"string","timestamp":"2026-01-01T12:00:00.000Z","uptime":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Deep check failed (with fail-fast=1)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn alle Einzelpruefungen durchliefen"},"duration_ms":{"type":"number","description":"Gesamtdauer aller Pruefungen in Millisekunden"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"s3":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"anthropic":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"meili":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"queue":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis","s3","anthropic","meili","queue"],"description":"Die sechs Abhaengigkeiten einzeln"}},"required":["ok","duration_ms","checks"]}}}}},"operationId":"getApiV1Health","tags":["health"],"parameters":[],"description":"Service health check. Ohne Parameter kommt eine Herkunftsauskunft ueber DIESEN Container (Commit aus GIT_COMMIT_SHA, Bauzeitpunkt, Laufzeit) — ohne jede Abhaengigkeitspruefung; dieser Fall ist IMMER 200. Mit `?deep=1` (oder `deep=true`) laufen stattdessen die sechs Abhaengigkeitspruefungen (db, redis, s3, anthropic, meili, queue) und der Rumpf hat eine ANDERE Form. Auch dann bleibt es 200 — erst zusaetzlich `?fail-fast=1` macht aus einer fehlgeschlagenen Tiefenpruefung ein 503.","summary":"Service health check","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/health/migrations":{"get":{"responses":{"200":{"description":"Alle Kern-Migrationen angewendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn keine Kern-Migration mehr offen ist"},"gesamt":{"type":"integer","minimum":0,"description":"Anzahl der Migrationen in der Registry (SOLL)"},"angewendet":{"type":"integer","minimum":0,"description":"Davon in public._root_migrations eingetragen"},"offen":{"type":"integer","minimum":0,"description":"Noch nicht eingetragene Migrationen"},"inDatenbankUnbekannt":{"type":"integer","minimum":0,"description":"Eintraege, die die Registry nicht kennt — Zeichen fuer ein Rueckwaertsdeploy"},"coreSchemaVersion":{"anyOf":[{"type":"string"},{"type":"number"}],"description":"Erwartete Version des Kern-Schemas"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"},"offeneMigrationen":{"type":"array","items":{"type":"string"},"description":"Die Namen der offenen Migrationen — NUR mit gueltigem METRICS_SCRAPE_TOKEN als Bearer"}},"required":["ok","gesamt","angewendet","offen","inDatenbankUnbekannt","coreSchemaVersion","timestamp"]},"example":{"ok":true,"gesamt":0,"angewendet":0,"offen":0,"inDatenbankUnbekannt":0,"coreSchemaVersion":"string","timestamp":"2026-01-01T12:00:00.000Z","offeneMigrationen":["string"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Migrationen offen ODER Datenbank/Tracking-Tabelle nicht lesbar","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn keine Kern-Migration mehr offen ist"},"gesamt":{"type":"integer","minimum":0,"description":"Anzahl der Migrationen in der Registry (SOLL)"},"angewendet":{"type":"integer","minimum":0,"description":"Davon in public._root_migrations eingetragen"},"offen":{"type":"integer","minimum":0,"description":"Noch nicht eingetragene Migrationen"},"inDatenbankUnbekannt":{"type":"integer","minimum":0,"description":"Eintraege, die die Registry nicht kennt — Zeichen fuer ein Rueckwaertsdeploy"},"coreSchemaVersion":{"anyOf":[{"type":"string"},{"type":"number"}],"description":"Erwartete Version des Kern-Schemas"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"},"offeneMigrationen":{"type":"array","items":{"type":"string"},"description":"Die Namen der offenen Migrationen — NUR mit gueltigem METRICS_SCRAPE_TOKEN als Bearer"}},"required":["ok","gesamt","angewendet","offen","inDatenbankUnbekannt","coreSchemaVersion","timestamp"]},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"grund":{"type":"string","enum":["datenbank_nicht_erreichbar","tracking_tabelle_fehlt","tracking_tabelle_nicht_lesbar"],"description":"Woran das Lesen scheiterte"},"hinweis":{"type":"string","description":"Naechster Schritt im Klartext; fehlt, wenn schon der Datenbank-Client fehlte"}},"required":["ok","grund"]}]}}}}},"operationId":"getApiV1HealthMigrations","tags":["health"],"parameters":[],"summary":"Migration state of the core schema: registry vs. applied","description":"Migrationsstand des Kern-Schemas: SOLL (Registry) gegen IST (public._root_migrations). Mit METRICS_SCRAPE_TOKEN zusaetzlich die Namen der offenen Migrationen."}},"/api/v1/health/ready":{"get":{"responses":{"200":{"description":"Ready — beide Bausteine antworten","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean","description":"true, wenn Datenbank UND Zwischenspeicher antworten"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis"],"description":"Die beiden Einzelpruefungen, je mit 1500 ms Zeitgrenze"}},"required":["ready","checks"]},"example":{"ready":true,"checks":{"db":{"ok":true,"ms":0,"error":"string"},"redis":{"ok":true,"ms":0,"error":"string"}}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Not ready — mindestens ein Baustein antwortet nicht","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean","description":"true, wenn Datenbank UND Zwischenspeicher antworten"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis"],"description":"Die beiden Einzelpruefungen, je mit 1500 ms Zeitgrenze"}},"required":["ready","checks"]}}}}},"operationId":"getApiV1HealthReady","tags":["health"],"parameters":[],"description":"Readiness check — fragt Datenbank und Zwischenspeicher parallel mit je 1500 ms Zeitgrenze. `ready` ist nur true, wenn BEIDE antworten; ein fehlender Baustein gilt als \"not configured\" und macht die Antwort 503. Geprueft wird die Erreichbarkeit, NICHT der Migrationsstand — dafuer gibt es `/health/migrations`. Anders als `/health/live` ist diese Sonde also von aussen abhaengig.","summary":"Readiness check","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/health/live":{"get":{"responses":{"200":{"description":"Alive — der Prozess antwortet","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"live"},"pid":{"type":"integer","description":"Prozesskennung"},"uptime":{"type":"number","description":"Laufzeit des Prozesses in Sekunden"}},"required":["status","pid","uptime"]},"example":{"status":"live","pid":0,"uptime":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1HealthLive","tags":["health"],"parameters":[],"description":"Liveness probe — beruehrt nichts ausserhalb des Prozesses: kein Datenbank-, kein Zwischenspeicher-Zugriff. Die Antwort nennt Prozesskennung und Laufzeit und ist immer 200, solange die Ereignisschleife ueberhaupt noch antwortet. Sie sagt NICHTS darueber, ob der Dienst arbeitsfaehig ist — dafuer `/health/ready`.","summary":"Liveness probe","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/_internal/sentry-test":{"get":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Disabled in production"},"500":{"description":"Intentional crash (Sentry will record this)"}},"operationId":"getApiV1_internalSentry-test","tags":["_internal"],"parameters":[],"description":"Throws an unhandled error so Sentry shows an issue. Dev-only — production returns 404.","summary":"Throws an unhandled error so Sentry shows an issue","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/_internal/sentry-test/capture":{"post":{"responses":{"200":{"description":"Captured (no real crash) — captureException() was called with a synthetic error.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"captured":{"type":"boolean","const":true}},"required":["ok","captured"]},"example":{"ok":true,"captured":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Disabled in production"}},"operationId":"postApiV1_internalSentry-testCapture","tags":["_internal"],"parameters":[],"description":"Prueft den Sentry-Meldeweg ohne echten Absturz. Der Handler ruft captureException() mit einem kuenstlichen Fehler auf, haengt Mandant, Nutzer und Request-Kennung an und antwortet mit 200; in Sentry erscheint ein Eintrag mit der Quelle `sentry-test`, sonst aendert sich nichts. Nur Super-Admins erreichen die Route, in Produktion antwortet sie immer 404.","summary":"Prueft den Sentry-Meldeweg ohne echten Absturz","x-nemix-summary-source":"description:first-sentence"}},"/health/auth":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getHealthAuth","tags":["health"],"parameters":[],"summary":"Prueft die Anmeldeschicht: Sitzungsspeicher und Schluessel erreichbar","description":"Prueft die Anmeldeschicht: erreicht der Dienst seinen Sitzungsspeicher und seine Schluessel? Sagt NICHT, ob ein bestimmter Anwender sich anmelden kann.","security":[]}},"/health/db":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getHealthDb","tags":["health"],"parameters":[],"description":"Prueft die Datenbank mit einer echten Abfrage, nicht mit einem Ping. Eine erreichbare Datenbank, die keine Antwort liefert, gilt hier als krank.","summary":"Prueft die Datenbank mit einer echten Abfrage, nicht mit einem Ping","x-nemix-summary-source":"description:first-sentence","security":[]}},"/health/redis":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getHealthRedis","tags":["health"],"parameters":[],"description":"Prueft den Zwischenspeicher. Faellt er aus, laeuft das System langsamer weiter — deshalb ist diese Sonde einzeln abfragbar und nicht Teil eines Sammelurteils.","summary":"Prueft den Zwischenspeicher","x-nemix-summary-source":"description:first-sentence","security":[]}},"/health/ai":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getHealthAi","tags":["health"],"parameters":[],"description":"Prueft, ob der KI-Anbieter erreichbar ist und ein Schluessel hinterlegt ist. Verbraucht kein Kontingent fuer eine echte Anfrage.","summary":"Prueft, ob der KI-Anbieter erreichbar ist und ein Schluessel hinterlegt ist","x-nemix-summary-source":"description:first-sentence","security":[]}},"/health/storage":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getHealthStorage","tags":["health"],"parameters":[],"summary":"Prueft den Dateispeicher","description":"Verlangt `S3_BUCKET` und listet dort EIN Objekt auf (`ListObjectsV2`, MaxKeys 1) — die Region kommt aus `AWS_DEFAULT_REGION`, ersatzweise `eu-central-1`. Damit ist geprueft, dass der Eimer erreichbar ist und die Zugangsdaten dafuer LESEN duerfen. Geschrieben wird nichts, ein Schreibrecht weist diese Sonde also NICHT nach. Fehlt `S3_BUCKET`, meldet sie `s3_bucket_missing`; ist das AWS-SDK nicht installiert, meldet sie Erfolg mit dem Zusatz \"config only\" — ohne den Eimer ueberhaupt angefasst zu haben.","security":[]}},"/health/queue":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getHealthQueue","tags":["health"],"parameters":[],"description":"Prueft die Warteschlange der Hintergrundarbeiten. Eine gesunde Antwort heisst: Auftraege koennen angenommen werden, nicht dass keine liegen bleiben.","summary":"Prueft die Warteschlange der Hintergrundarbeiten","x-nemix-summary-source":"description:first-sentence","security":[]}},"/health/all":{"get":{"responses":{"200":{"description":"`overall` ist `healthy` ODER `degraded`. Der Unterschied steht im Rumpf, nicht im Statuscode.","content":{"application/json":{"schema":{"type":"object","properties":{"overall":{"type":"string","enum":["healthy","degraded","unhealthy"],"description":"healthy = keine Sonde faellt aus, unhealthy = drei oder mehr fallen aus, degraded = alles dazwischen"},"probes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"description":"Die sechs Einzelsonden in der Reihenfolge db, redis, auth, ai, storage, queue"},"ts":{"type":"string","format":"date-time","description":"Zeitpunkt des Laufs"}},"required":["overall","probes","ts"]},"example":{"overall":"healthy","probes":[{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}],"ts":"2026-01-01T12:00:00.000Z"}}}},"503":{"description":"`overall` ist `unhealthy` — mindestens ein tragender Baustein faellt aus.","content":{"application/json":{"schema":{"type":"object","properties":{"overall":{"type":"string","enum":["healthy","degraded","unhealthy"],"description":"healthy = keine Sonde faellt aus, unhealthy = drei oder mehr fallen aus, degraded = alles dazwischen"},"probes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"description":"Die sechs Einzelsonden in der Reihenfolge db, redis, auth, ai, storage, queue"},"ts":{"type":"string","format":"date-time","description":"Zeitpunkt des Laufs"}},"required":["overall","probes","ts"]}}}}},"operationId":"getHealthAll","tags":["health"],"parameters":[],"description":"Fragt alle Sonden auf einmal und faellt EIN Urteil in `overall`. Achtung beim Statuscode: `degraded` gibt ebenfalls 200 — wer nur ihn liest, haelt ein angeschlagenes System fuer gesund. Nur `unhealthy` ergibt 503. Das ist Absicht: ein ausgefallener Zwischenspeicher soll keinen Alarm ausloesen, solange das System weiterarbeitet.","summary":"Fragt alle Sonden auf einmal und faellt EIN Urteil in `overall`","x-nemix-summary-source":"description:first-sentence","security":[]}},"/health/last-errors":{"get":{"responses":{"200":{"description":"Die Liste der letzten Fehler, moeglicherweise leer.","content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string","description":"Zeitpunkt der Meldung"},"err":{"type":"string","description":"Die Fehlermeldung als Text"},"ctx":{"type":"object","properties":{"requestId":{"type":"string","description":"Kennung der Anfrage"},"userId":{"type":"string","description":"Anwender, sofern bekannt"},"tenantId":{"type":"string","description":"Mandant, sofern bekannt"},"path":{"type":"string","description":"Angefragter Pfad"},"method":{"type":"string","description":"HTTP-Verfahren"},"apiKeyId":{"type":"string","description":"Verwendeter API-Schluessel, sofern einer im Spiel war"}},"description":"Umstaende der Meldung; jedes Feld kann fehlen"}},"required":["at","err","ctx"]},"description":"Der Ringspeicher, aelteste zuerst; hoechstens ERROR_RING_BUFFER_SIZE Eintraege"}},"required":["errors"]},"example":{"errors":[{"at":"string","err":"string","ctx":{"requestId":"string","userId":"string","tenantId":"string","path":"string","method":"string","apiKeyId":"string"}}]}}}},"401":{"description":"Kein oder falscher `DOCS_BEARER_TOKEN`. 401 und nicht 403 — es gibt hier keine Rolle, die mehr duerfte.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"hint":{"type":"string","description":"Hinweis, welches Merkmal fehlt"}},"required":["error","hint"]}}}}},"operationId":"getHealthLast-errors","tags":["health"],"parameters":[],"summary":"Die zuletzt gemeldeten Fehler aus dem Speicher dieses Prozesses","description":"Die zuletzt gemeldeten Fehler aus dem Speicher dieses Prozesses — nicht aus einer Datenbank. Nach einem Neustart ist die Liste leer, und das ist kein Befund. Geschuetzt durch `DOCS_BEARER_TOKEN` als Bearer, NICHT durch die Sitzung des Anwenders: die Sonde soll auch dann noch etwas sagen koennen, wenn die Anmeldung selbst der Grund des Ausfalls ist.","security":[]}},"/api/health/auth":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiHealthAuth","tags":["health"],"parameters":[],"summary":"Prueft die Anmeldeschicht: Sitzungsspeicher und Schluessel erreichbar","description":"Prueft die Anmeldeschicht: erreicht der Dienst seinen Sitzungsspeicher und seine Schluessel? Sagt NICHT, ob ein bestimmter Anwender sich anmelden kann.","security":[]}},"/api/health/db":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiHealthDb","tags":["health"],"parameters":[],"description":"Prueft die Datenbank mit einer echten Abfrage, nicht mit einem Ping. Eine erreichbare Datenbank, die keine Antwort liefert, gilt hier als krank.","summary":"Prueft die Datenbank mit einer echten Abfrage, nicht mit einem Ping","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/health/redis":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiHealthRedis","tags":["health"],"parameters":[],"description":"Prueft den Zwischenspeicher. Faellt er aus, laeuft das System langsamer weiter — deshalb ist diese Sonde einzeln abfragbar und nicht Teil eines Sammelurteils.","summary":"Prueft den Zwischenspeicher","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/health/ai":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiHealthAi","tags":["health"],"parameters":[],"description":"Prueft, ob der KI-Anbieter erreichbar ist und ein Schluessel hinterlegt ist. Verbraucht kein Kontingent fuer eine echte Anfrage.","summary":"Prueft, ob der KI-Anbieter erreichbar ist und ein Schluessel hinterlegt ist","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/health/storage":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiHealthStorage","tags":["health"],"parameters":[],"summary":"Prueft den Dateispeicher","description":"Verlangt `S3_BUCKET` und listet dort EIN Objekt auf (`ListObjectsV2`, MaxKeys 1) — die Region kommt aus `AWS_DEFAULT_REGION`, ersatzweise `eu-central-1`. Damit ist geprueft, dass der Eimer erreichbar ist und die Zugangsdaten dafuer LESEN duerfen. Geschrieben wird nichts, ein Schreibrecht weist diese Sonde also NICHT nach. Fehlt `S3_BUCKET`, meldet sie `s3_bucket_missing`; ist das AWS-SDK nicht installiert, meldet sie Erfolg mit dem Zusatz \"config only\" — ohne den Eimer ueberhaupt angefasst zu haben.","security":[]}},"/api/health/queue":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiHealthQueue","tags":["health"],"parameters":[],"description":"Prueft die Warteschlange der Hintergrundarbeiten. Eine gesunde Antwort heisst: Auftraege koennen angenommen werden, nicht dass keine liegen bleiben.","summary":"Prueft die Warteschlange der Hintergrundarbeiten","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/health/all":{"get":{"responses":{"200":{"description":"`overall` ist `healthy` ODER `degraded`. Der Unterschied steht im Rumpf, nicht im Statuscode.","content":{"application/json":{"schema":{"type":"object","properties":{"overall":{"type":"string","enum":["healthy","degraded","unhealthy"],"description":"healthy = keine Sonde faellt aus, unhealthy = drei oder mehr fallen aus, degraded = alles dazwischen"},"probes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"description":"Die sechs Einzelsonden in der Reihenfolge db, redis, auth, ai, storage, queue"},"ts":{"type":"string","format":"date-time","description":"Zeitpunkt des Laufs"}},"required":["overall","probes","ts"]},"example":{"overall":"healthy","probes":[{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}],"ts":"2026-01-01T12:00:00.000Z"}}}},"503":{"description":"`overall` ist `unhealthy` — mindestens ein tragender Baustein faellt aus.","content":{"application/json":{"schema":{"type":"object","properties":{"overall":{"type":"string","enum":["healthy","degraded","unhealthy"],"description":"healthy = keine Sonde faellt aus, unhealthy = drei oder mehr fallen aus, degraded = alles dazwischen"},"probes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"description":"Die sechs Einzelsonden in der Reihenfolge db, redis, auth, ai, storage, queue"},"ts":{"type":"string","format":"date-time","description":"Zeitpunkt des Laufs"}},"required":["overall","probes","ts"]}}}}},"operationId":"getApiHealthAll","tags":["health"],"parameters":[],"description":"Fragt alle Sonden auf einmal und faellt EIN Urteil in `overall`. Achtung beim Statuscode: `degraded` gibt ebenfalls 200 — wer nur ihn liest, haelt ein angeschlagenes System fuer gesund. Nur `unhealthy` ergibt 503. Das ist Absicht: ein ausgefallener Zwischenspeicher soll keinen Alarm ausloesen, solange das System weiterarbeitet.","summary":"Fragt alle Sonden auf einmal und faellt EIN Urteil in `overall`","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/health/last-errors":{"get":{"responses":{"200":{"description":"Die Liste der letzten Fehler, moeglicherweise leer.","content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string","description":"Zeitpunkt der Meldung"},"err":{"type":"string","description":"Die Fehlermeldung als Text"},"ctx":{"type":"object","properties":{"requestId":{"type":"string","description":"Kennung der Anfrage"},"userId":{"type":"string","description":"Anwender, sofern bekannt"},"tenantId":{"type":"string","description":"Mandant, sofern bekannt"},"path":{"type":"string","description":"Angefragter Pfad"},"method":{"type":"string","description":"HTTP-Verfahren"},"apiKeyId":{"type":"string","description":"Verwendeter API-Schluessel, sofern einer im Spiel war"}},"description":"Umstaende der Meldung; jedes Feld kann fehlen"}},"required":["at","err","ctx"]},"description":"Der Ringspeicher, aelteste zuerst; hoechstens ERROR_RING_BUFFER_SIZE Eintraege"}},"required":["errors"]},"example":{"errors":[{"at":"string","err":"string","ctx":{"requestId":"string","userId":"string","tenantId":"string","path":"string","method":"string","apiKeyId":"string"}}]}}}},"401":{"description":"Kein oder falscher `DOCS_BEARER_TOKEN`. 401 und nicht 403 — es gibt hier keine Rolle, die mehr duerfte.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"hint":{"type":"string","description":"Hinweis, welches Merkmal fehlt"}},"required":["error","hint"]}}}}},"operationId":"getApiHealthLast-errors","tags":["health"],"parameters":[],"summary":"Die zuletzt gemeldeten Fehler aus dem Speicher dieses Prozesses","description":"Die zuletzt gemeldeten Fehler aus dem Speicher dieses Prozesses — nicht aus einer Datenbank. Nach einem Neustart ist die Liste leer, und das ist kein Befund. Geschuetzt durch `DOCS_BEARER_TOKEN` als Bearer, NICHT durch die Sitzung des Anwenders: die Sonde soll auch dann noch etwas sagen koennen, wenn die Anmeldung selbst der Grund des Ausfalls ist.","security":[]}},"/api/v1/health/auth":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiV1HealthAuth","tags":["health"],"parameters":[],"summary":"Prueft die Anmeldeschicht: Sitzungsspeicher und Schluessel erreichbar","description":"Prueft die Anmeldeschicht: erreicht der Dienst seinen Sitzungsspeicher und seine Schluessel? Sagt NICHT, ob ein bestimmter Anwender sich anmelden kann."}},"/api/v1/health/db":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiV1HealthDb","tags":["health"],"parameters":[],"description":"Prueft die Datenbank mit einer echten Abfrage, nicht mit einem Ping. Eine erreichbare Datenbank, die keine Antwort liefert, gilt hier als krank.","summary":"Prueft die Datenbank mit einer echten Abfrage, nicht mit einem Ping","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/health/redis":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiV1HealthRedis","tags":["health"],"parameters":[],"description":"Prueft den Zwischenspeicher. Faellt er aus, laeuft das System langsamer weiter — deshalb ist diese Sonde einzeln abfragbar und nicht Teil eines Sammelurteils.","summary":"Prueft den Zwischenspeicher","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/health/ai":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiV1HealthAi","tags":["health"],"parameters":[],"description":"Prueft, ob der KI-Anbieter erreichbar ist und ein Schluessel hinterlegt ist. Verbraucht kein Kontingent fuer eine echte Anfrage.","summary":"Prueft, ob der KI-Anbieter erreichbar ist und ein Schluessel hinterlegt ist","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/health/storage":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiV1HealthStorage","tags":["health"],"parameters":[],"summary":"Prueft den Dateispeicher","description":"Verlangt `S3_BUCKET` und listet dort EIN Objekt auf (`ListObjectsV2`, MaxKeys 1) — die Region kommt aus `AWS_DEFAULT_REGION`, ersatzweise `eu-central-1`. Damit ist geprueft, dass der Eimer erreichbar ist und die Zugangsdaten dafuer LESEN duerfen. Geschrieben wird nichts, ein Schreibrecht weist diese Sonde also NICHT nach. Fehlt `S3_BUCKET`, meldet sie `s3_bucket_missing`; ist das AWS-SDK nicht installiert, meldet sie Erfolg mit dem Zusatz \"config only\" — ohne den Eimer ueberhaupt angefasst zu haben."}},"/api/v1/health/queue":{"get":{"responses":{"200":{"description":"Der Baustein antwortet. Einzelheiten im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"example":{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Der Baustein antwortet nicht oder zu langsam. Der Rumpf sagt, woran es lag — die Sonde raet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]}}}}},"operationId":"getApiV1HealthQueue","tags":["health"],"parameters":[],"description":"Prueft die Warteschlange der Hintergrundarbeiten. Eine gesunde Antwort heisst: Auftraege koennen angenommen werden, nicht dass keine liegen bleiben.","summary":"Prueft die Warteschlange der Hintergrundarbeiten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/health/all":{"get":{"responses":{"200":{"description":"`overall` ist `healthy` ODER `degraded`. Der Unterschied steht im Rumpf, nicht im Statuscode.","content":{"application/json":{"schema":{"type":"object","properties":{"overall":{"type":"string","enum":["healthy","degraded","unhealthy"],"description":"healthy = keine Sonde faellt aus, unhealthy = drei oder mehr fallen aus, degraded = alles dazwischen"},"probes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"description":"Die sechs Einzelsonden in der Reihenfolge db, redis, auth, ai, storage, queue"},"ts":{"type":"string","format":"date-time","description":"Zeitpunkt des Laufs"}},"required":["overall","probes","ts"]},"example":{"overall":"healthy","probes":[{"name":"string","ok":true,"latency_ms":0,"details":"string","error":"string"}],"ts":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"`overall` ist `unhealthy` — mindestens ein tragender Baustein faellt aus.","content":{"application/json":{"schema":{"type":"object","properties":{"overall":{"type":"string","enum":["healthy","degraded","unhealthy"],"description":"healthy = keine Sonde faellt aus, unhealthy = drei oder mehr fallen aus, degraded = alles dazwischen"},"probes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Der geprueste Baustein: db, redis, auth, ai, storage oder queue"},"ok":{"type":"boolean","description":"true, wenn die Pruefung ohne Fehler durchlief"},"latency_ms":{"type":"integer","minimum":0,"description":"Dauer der Pruefung in Millisekunden"},"details":{"type":"string","description":"Klartext-Befund bei Erfolg, z. B. \"set/get/del ok\"; fehlt im Fehlerfall"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"redis_url_missing\"; fehlt im Erfolgsfall"}},"required":["name","ok","latency_ms"]},"description":"Die sechs Einzelsonden in der Reihenfolge db, redis, auth, ai, storage, queue"},"ts":{"type":"string","format":"date-time","description":"Zeitpunkt des Laufs"}},"required":["overall","probes","ts"]}}}}},"operationId":"getApiV1HealthAll","tags":["health"],"parameters":[],"description":"Fragt alle Sonden auf einmal und faellt EIN Urteil in `overall`. Achtung beim Statuscode: `degraded` gibt ebenfalls 200 — wer nur ihn liest, haelt ein angeschlagenes System fuer gesund. Nur `unhealthy` ergibt 503. Das ist Absicht: ein ausgefallener Zwischenspeicher soll keinen Alarm ausloesen, solange das System weiterarbeitet.","summary":"Fragt alle Sonden auf einmal und faellt EIN Urteil in `overall`","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/health/last-errors":{"get":{"responses":{"200":{"description":"Die Liste der letzten Fehler, moeglicherweise leer.","content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string","description":"Zeitpunkt der Meldung"},"err":{"type":"string","description":"Die Fehlermeldung als Text"},"ctx":{"type":"object","properties":{"requestId":{"type":"string","description":"Kennung der Anfrage"},"userId":{"type":"string","description":"Anwender, sofern bekannt"},"tenantId":{"type":"string","description":"Mandant, sofern bekannt"},"path":{"type":"string","description":"Angefragter Pfad"},"method":{"type":"string","description":"HTTP-Verfahren"},"apiKeyId":{"type":"string","description":"Verwendeter API-Schluessel, sofern einer im Spiel war"}},"description":"Umstaende der Meldung; jedes Feld kann fehlen"}},"required":["at","err","ctx"]},"description":"Der Ringspeicher, aelteste zuerst; hoechstens ERROR_RING_BUFFER_SIZE Eintraege"}},"required":["errors"]},"example":{"errors":[{"at":"string","err":"string","ctx":{"requestId":"string","userId":"string","tenantId":"string","path":"string","method":"string","apiKeyId":"string"}}]}}}},"401":{"description":"Kein oder falscher `DOCS_BEARER_TOKEN`. 401 und nicht 403 — es gibt hier keine Rolle, die mehr duerfte.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"hint":{"type":"string","description":"Hinweis, welches Merkmal fehlt"}},"required":["error","hint"]}}}}},"operationId":"getApiV1HealthLast-errors","tags":["health"],"parameters":[],"summary":"Die zuletzt gemeldeten Fehler aus dem Speicher dieses Prozesses","description":"Die zuletzt gemeldeten Fehler aus dem Speicher dieses Prozesses — nicht aus einer Datenbank. Nach einem Neustart ist die Liste leer, und das ist kein Befund. Geschuetzt durch `DOCS_BEARER_TOKEN` als Bearer, NICHT durch die Sitzung des Anwenders: die Sonde soll auch dann noch etwas sagen koennen, wenn die Anmeldung selbst der Grund des Ausfalls ist."}},"/health/schema-drift":{"get":{"responses":{"200":{"description":"Vergleich gelaufen. `summary.total` = 0 heisst: keine Abweichung gefunden. Das ist kein Beweis fuer Schema-Gesundheit, sondern nur fuer die geprueften Tabellen — was die Liste nicht kennt, kann sie nicht vermissen. Je Fund nennt `kind`, ob eine ganze Tabelle oder eine einzelne Spalte fehlt; `column` steht nur bei `missing_column`.","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"object","properties":{"tables_missing":{"type":"integer"},"columns_missing":{"type":"integer"},"total":{"type":"integer"}},"required":["tables_missing","columns_missing","total"]},"drift":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["missing_table","missing_column"]},"schema":{"type":"string"},"table":{"type":"string"},"column":{"type":"string"}},"required":["kind","schema","table"]}}},"required":["summary","drift"]},"example":{"summary":{"tables_missing":0,"columns_missing":0,"total":0},"drift":[{"kind":"missing_table","schema":"string","table":"string","column":"string"}]}}}},"401":{"description":"Nur in Produktion: `Authorization: Bearer $DOCS_BEARER_TOKEN` fehlt oder passt nicht. Ist `DOCS_BEARER_TOKEN` gar nicht gesetzt, ist die Route in Produktion fuer niemanden erreichbar. Auf dev und staging entfaellt diese Pruefung ganz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"]}}}},"500":{"description":"Der Vergleich konnte nicht durchgefuehrt werden; der Rumpf traegt die Fehlermeldung. Auch eine fehlende Datenbankverbindung endet hier als 500, nicht als 503 — dann steht `database unavailable` in `error`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}},"operationId":"getHealthSchema-drift","tags":["health"],"parameters":[],"summary":"Vergleicht das erwartete Datenbankschema mit dem tatsaechlichen","description":"Vergleicht das erwartete Datenbankschema mit dem, was in der Datenbank wirklich steht, und meldet die Abweichungen. Geprueft werden alle Mandanten-Schemata (`tenant_%`, ohne Test- und Trigger-Schemata) sowie `public`; gemeldet wird je Fund, ob eine ganze TABELLE fehlt oder eine einzelne SPALTE. Rein lesend: die Route liest nur `information_schema` und legt nichts an, aendert nichts, repariert nichts. Zurueck kommen eine Zaehlung (`summary`) und die Einzelfunde (`drift`) — nur Schema- und Tabellennamen, keine Mandantendaten. Die Erwartung stammt aus der Handliste in dieser Datei, vereinigt mit den Spalten, die das ORM deklariert.","security":[]}},"/api/health/schema-drift":{"get":{"responses":{"200":{"description":"Vergleich gelaufen. `summary.total` = 0 heisst: keine Abweichung gefunden. Das ist kein Beweis fuer Schema-Gesundheit, sondern nur fuer die geprueften Tabellen — was die Liste nicht kennt, kann sie nicht vermissen. Je Fund nennt `kind`, ob eine ganze Tabelle oder eine einzelne Spalte fehlt; `column` steht nur bei `missing_column`.","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"object","properties":{"tables_missing":{"type":"integer"},"columns_missing":{"type":"integer"},"total":{"type":"integer"}},"required":["tables_missing","columns_missing","total"]},"drift":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["missing_table","missing_column"]},"schema":{"type":"string"},"table":{"type":"string"},"column":{"type":"string"}},"required":["kind","schema","table"]}}},"required":["summary","drift"]},"example":{"summary":{"tables_missing":0,"columns_missing":0,"total":0},"drift":[{"kind":"missing_table","schema":"string","table":"string","column":"string"}]}}}},"401":{"description":"Nur in Produktion: `Authorization: Bearer $DOCS_BEARER_TOKEN` fehlt oder passt nicht. Ist `DOCS_BEARER_TOKEN` gar nicht gesetzt, ist die Route in Produktion fuer niemanden erreichbar. Auf dev und staging entfaellt diese Pruefung ganz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"]}}}},"500":{"description":"Der Vergleich konnte nicht durchgefuehrt werden; der Rumpf traegt die Fehlermeldung. Auch eine fehlende Datenbankverbindung endet hier als 500, nicht als 503 — dann steht `database unavailable` in `error`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiHealthSchema-drift","tags":["health"],"parameters":[],"summary":"Vergleicht das erwartete Datenbankschema mit dem tatsaechlichen","description":"Vergleicht das erwartete Datenbankschema mit dem, was in der Datenbank wirklich steht, und meldet die Abweichungen. Geprueft werden alle Mandanten-Schemata (`tenant_%`, ohne Test- und Trigger-Schemata) sowie `public`; gemeldet wird je Fund, ob eine ganze TABELLE fehlt oder eine einzelne SPALTE. Rein lesend: die Route liest nur `information_schema` und legt nichts an, aendert nichts, repariert nichts. Zurueck kommen eine Zaehlung (`summary`) und die Einzelfunde (`drift`) — nur Schema- und Tabellennamen, keine Mandantendaten. Die Erwartung stammt aus der Handliste in dieser Datei, vereinigt mit den Spalten, die das ORM deklariert.","security":[]}},"/api/v1/health/schema-drift":{"get":{"responses":{"200":{"description":"Vergleich gelaufen. `summary.total` = 0 heisst: keine Abweichung gefunden. Das ist kein Beweis fuer Schema-Gesundheit, sondern nur fuer die geprueften Tabellen — was die Liste nicht kennt, kann sie nicht vermissen. Je Fund nennt `kind`, ob eine ganze Tabelle oder eine einzelne Spalte fehlt; `column` steht nur bei `missing_column`.","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"object","properties":{"tables_missing":{"type":"integer"},"columns_missing":{"type":"integer"},"total":{"type":"integer"}},"required":["tables_missing","columns_missing","total"]},"drift":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["missing_table","missing_column"]},"schema":{"type":"string"},"table":{"type":"string"},"column":{"type":"string"}},"required":["kind","schema","table"]}}},"required":["summary","drift"]},"example":{"summary":{"tables_missing":0,"columns_missing":0,"total":0},"drift":[{"kind":"missing_table","schema":"string","table":"string","column":"string"}]}}}},"401":{"description":"Nur in Produktion: `Authorization: Bearer $DOCS_BEARER_TOKEN` fehlt oder passt nicht. Ist `DOCS_BEARER_TOKEN` gar nicht gesetzt, ist die Route in Produktion fuer niemanden erreichbar. Auf dev und staging entfaellt diese Pruefung ganz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"]}}}},"500":{"description":"Der Vergleich konnte nicht durchgefuehrt werden; der Rumpf traegt die Fehlermeldung. Auch eine fehlende Datenbankverbindung endet hier als 500, nicht als 503 — dann steht `database unavailable` in `error`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1HealthSchema-drift","tags":["health"],"parameters":[],"summary":"Vergleicht das erwartete Datenbankschema mit dem tatsaechlichen","description":"Vergleicht das erwartete Datenbankschema mit dem, was in der Datenbank wirklich steht, und meldet die Abweichungen. Geprueft werden alle Mandanten-Schemata (`tenant_%`, ohne Test- und Trigger-Schemata) sowie `public`; gemeldet wird je Fund, ob eine ganze TABELLE fehlt oder eine einzelne SPALTE. Rein lesend: die Route liest nur `information_schema` und legt nichts an, aendert nichts, repariert nichts. Zurueck kommen eine Zaehlung (`summary`) und die Einzelfunde (`drift`) — nur Schema- und Tabellennamen, keine Mandantendaten. Die Erwartung stammt aus der Handliste in dieser Datei, vereinigt mit den Spalten, die das ORM deklariert."}},"/api/live":{"get":{"responses":{"200":{"description":"Process alive — der einzige Ausgang dieses Handlers. Er beruehrt nichts ausserhalb des Prozesses und kann daher gar nicht anders enden.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"live"},"pid":{"type":"integer","description":"Prozesskennung"},"uptime":{"type":"number","description":"Laufzeit des Prozesses in Sekunden"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Antwort"}},"required":["status","pid","uptime","timestamp"],"description":"Rumpf von GET /api/live"},"example":{"status":"live","pid":0,"uptime":0,"timestamp":"2026-01-01T12:00:00.000Z"}}}}},"operationId":"getApiLive","tags":["health"],"parameters":[],"summary":"Liveness probe for the container — direct API port only","description":"Liveness probe — returns 200 if the Hono process is responsive. NOTE: reachable on the API process directly (ALB target group, container health check). It is NOT proxied through the web domain, where it answers 404 — see apps/web/next.config.ts for the list of forwarded prefixes.","security":[]}},"/api/ready":{"get":{"responses":{"200":{"description":"Ready — Datenbank UND Zwischenspeicher haben innerhalb von je 1500 ms geantwortet.","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean","description":"true, wenn Datenbank UND Zwischenspeicher antworten"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis"],"description":"Die beiden Einzelpruefungen, je mit 1500 ms Zeitgrenze"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"}},"required":["ready","checks","timestamp"],"description":"Rumpf von GET /api/ready — bei 200 und bei 503 derselbe Feldbestand"},"example":{"ready":true,"checks":{"db":{"ok":true,"ms":0,"error":"string"},"redis":{"ok":true,"ms":0,"error":"string"}},"timestamp":"2026-01-01T12:00:00.000Z"}}}},"503":{"description":"Not ready — mindestens einer der beiden Bausteine antwortet nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean","description":"true, wenn Datenbank UND Zwischenspeicher antworten"},"checks":{"type":"object","properties":{"db":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"},"redis":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Pruefung durchlief"},"ms":{"type":"number","description":"Dauer in Millisekunden; bei Zeitueberschreitung die Zeitgrenze selbst"},"error":{"type":"string","description":"Grund des Fehlschlags, z. B. \"timeout\" oder \"not configured\"; fehlt im Erfolgsfall"}},"required":["ok","ms"],"additionalProperties":true,"description":"Einzelne Pruefung; je nach Baustein koennen weitere Felder dazukommen"}},"required":["db","redis"],"description":"Die beiden Einzelpruefungen, je mit 1500 ms Zeitgrenze"},"timestamp":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"}},"required":["ready","checks","timestamp"],"description":"Rumpf von GET /api/ready — bei 200 und bei 503 derselbe Feldbestand"}}}}},"operationId":"getApiReady","tags":["health"],"parameters":[],"summary":"Readiness probe for the container — direct API port only","description":"Readiness probe — returns 200 if DB + Redis are reachable, 503 otherwise. NOTE: reachable on the API process directly (ALB target group, container health check). It is NOT proxied through the web domain, where it answers 404 — see apps/web/next.config.ts for the list of forwarded prefixes.","security":[]}},"/status":{"get":{"responses":{"200":{"description":"Status payload","content":{"application/json":{"schema":{"type":"object","properties":{"overall":{"type":"string","enum":["operational","degraded","down","unknown"]},"components":{"type":"array","items":{"type":"object","properties":{"definition":{"type":"object","properties":{"id":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]},"name":{"type":"string"},"description":{"type":"string"},"check":{"type":"string","enum":["http","db","redis","aws-cloudfront","aws-ses"]},"target":{"type":"string"},"degradedAboveMs":{"type":"number"},"timeoutMs":{"type":"number"},"group":{"type":"string","enum":["core","edge","async"]}},"required":["id","name","description","check","target","degradedAboveMs","timeoutMs","group"]},"current":{"type":"string","enum":["operational","degraded","down","unknown"]},"latencyMs":{"type":["number","null"]},"uptime90d":{"type":"object","properties":{"componentId":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]},"uptimePercent":{"type":"number"},"windowSeconds":{"type":"number"},"downSeconds":{"type":"number"},"degradedSeconds":{"type":"number"}},"required":["componentId","uptimePercent","windowSeconds","downSeconds","degradedSeconds"]}},"required":["definition","current","latencyMs","uptime90d"]}},"incidents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"impact":{"type":"string","enum":["minor","major","critical"]},"affectedComponents":{"type":"array","items":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]}},"currentState":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"updates":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"message":{"type":"string"},"postedAt":{"type":"string"}},"required":["state","message","postedAt"]}},"resolvedAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","title","impact","affectedComponents","currentState","updates","resolvedAt","createdAt"]}},"generatedAt":{"type":"string"}},"required":["overall","components","incidents","generatedAt"]},"example":{"overall":"operational","components":[{"definition":{"id":"api","name":"string","description":"string","check":"http","target":"string","degradedAboveMs":0,"timeoutMs":0,"group":"core"},"current":"operational","latencyMs":0,"uptime90d":{"componentId":"api","uptimePercent":0,"windowSeconds":0,"downSeconds":0,"degradedSeconds":0}}],"incidents":[{"id":"string","title":"string","impact":"minor","affectedComponents":["api"],"currentState":"investigating","updates":[{"state":"investigating","message":"string","postedAt":"string"}],"resolvedAt":"string","createdAt":"string"}],"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getStatus","tags":["status"],"parameters":[],"summary":"Public status page payload — components, incidents, and uptime","description":"Ohne Anmeldung abrufbar und mandantenunabhaengig: die Zahlen gelten fuer die Plattform, nicht fuer einen einzelnen Mandanten. Fuer jede fest hinterlegte Komponente wird der Zustand aus dem JUENGSTEN Messpunkt der letzten 90 Tage abgeleitet — gibt es keinen, lautet er \"unknown\", was nicht „in Ordnung\" heiszt. `uptime90d` fasst dasselbe Fenster zusammen. Dazu kommen die Vorfaelle der letzten 30 Tage, offene wie bereits geschlossene. `overall` ist die hoechste Schwere ueber alle Komponenten. Nichts wird gemessen: der Aufruf liest nur, was die Sonden zuvor eingeliefert haben."}},"/status/snapshots":{"post":{"responses":{"200":{"description":"Snapshot accepted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"snapshot":{"type":"object","properties":{"componentId":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]},"status":{"type":"string","enum":["operational","degraded","down","unknown"]},"latencyMs":{"type":["number","null"]},"observedAt":{"type":"string"},"message":{"type":"string"}},"required":["componentId","status","latencyMs","observedAt","message"]}},"required":["ok","snapshot"]},"example":{"ok":true,"snapshot":{"componentId":"api","status":"operational","latencyMs":0,"observedAt":"string","message":"string"}}}}},"401":{"description":"Invalid token"},"422":{"description":"Invalid body"},"503":{"description":"Einliefern ist abgeschaltet — kein Token hinterlegt"}},"operationId":"postStatusSnapshots","tags":["status"],"parameters":[],"summary":"Submit a probe snapshot — protected by X-Status-Token shared secret","description":"Haengt EINEN Messpunkt an die Zeitreihe einer Komponente an; ein bestehender wird nie ersetzt, und es gibt keinen Weg, einen wieder zu entfernen. Pflicht sind `componentId` und `status`; `latencyMs` faellt auf null zurueck, `observedAt` auf die aktuelle Zeit und `message` auf einen leeren Text — ein Messpunkt ohne Zeitstempel gilt also als jetzt beobachtet. Die Werte werden NICHT gegen die Liste der bekannten Komponenten und Zustaende geprueft. Die Antwort gibt den abgelegten Messpunkt samt Vorgabewerten zurueck. Der Aufruf verlangt die Kopfzeile X-Status-Token; ist serverseitig kein Token hinterlegt, sind Schreib-zugriffe gesperrt (503) statt offen."}},"/status/incidents":{"post":{"responses":{"200":{"description":"Incident created","content":{"application/json":{"schema":{"type":"object","properties":{"incident":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"impact":{"type":"string","enum":["minor","major","critical"]},"affectedComponents":{"type":"array","items":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]}},"currentState":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"updates":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"message":{"type":"string"},"postedAt":{"type":"string"}},"required":["state","message","postedAt"]}},"resolvedAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","title","impact","affectedComponents","currentState","updates","resolvedAt","createdAt"]}},"required":["incident"]},"example":{"incident":{"id":"string","title":"string","impact":"minor","affectedComponents":["api"],"currentState":"investigating","updates":[{"state":"investigating","message":"string","postedAt":"string"}],"resolvedAt":"string","createdAt":"string"}}}}},"401":{"description":"Invalid token"},"422":{"description":"Invalid body"},"503":{"description":"Einliefern ist abgeschaltet — kein Token hinterlegt"}},"operationId":"postStatusIncidents","tags":["status"],"parameters":[],"summary":"Open a new incident (admin/ingest token required)","description":"Legt einen Vorfall an, der sofort oeffentlich auf der Statusseite erscheint. Die Kennung `id` gibt der AUFRUFER vor — sie ist zugleich die Adresse fuer spaetere Fortschreibungen. Pflicht sind `id` und `title`; `impact` faellt auf \"minor\" zurueck, `affectedComponents` auf eine leere Liste und `initialMessage` auf einen leeren Text. Der Vorfall startet im Zustand \"investigating\" mit genau einem Eintrag im Verlauf; weiter geht es nur ueber POST /:id/updates, und es gibt keinen Weg, einen Vorfall wieder zu loeschen. Der Aufruf verlangt die Kopfzeile X-Status-Token; ist serverseitig kein Token hinterlegt, sind Schreibzugriffe gesperrt (503)."}},"/status/incidents/{id}/updates":{"post":{"responses":{"200":{"description":"Update appended","content":{"application/json":{"schema":{"type":"object","properties":{"incident":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"impact":{"type":"string","enum":["minor","major","critical"]},"affectedComponents":{"type":"array","items":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]}},"currentState":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"updates":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"message":{"type":"string"},"postedAt":{"type":"string"}},"required":["state","message","postedAt"]}},"resolvedAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","title","impact","affectedComponents","currentState","updates","resolvedAt","createdAt"]}},"required":["incident"]},"example":{"incident":{"id":"string","title":"string","impact":"minor","affectedComponents":["api"],"currentState":"investigating","updates":[{"state":"investigating","message":"string","postedAt":"string"}],"resolvedAt":"string","createdAt":"string"}}}}},"401":{"description":"Invalid token"},"422":{"description":"Invalid body"},"503":{"description":"Einliefern ist abgeschaltet — kein Token hinterlegt"}},"operationId":"postStatusIncidentsByIdUpdates","tags":["status"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Append an update to an existing incident (admin/ingest token required)","description":"Haengt einen Eintrag an den Verlauf eines bestehenden Vorfalls und setzt damit dessen aktuellen Zustand. Pflicht ist `state` (investigating, identified, monitoring, resolved), `message` faellt auf einen leeren Text zurueck. Der Zustand darf nur VORWAERTS oder auf sich selbst wechseln — ein Rueckschritt und jede Aenderung an einem bereits geschlossenen Vorfall werden abgelehnt. Beim ersten Wechsel nach \"resolved\" wird der Abschlusszeitpunkt gesetzt; bestehende Eintraege bleiben unveraendert und lassen sich nicht entfernen. Der Aufruf verlangt die Kopfzeile X-Status-Token; ist serverseitig kein Token hinterlegt, sind Schreibzugriffe gesperrt (503)."}},"/api/status":{"get":{"responses":{"200":{"description":"Status payload","content":{"application/json":{"schema":{"type":"object","properties":{"overall":{"type":"string","enum":["operational","degraded","down","unknown"]},"components":{"type":"array","items":{"type":"object","properties":{"definition":{"type":"object","properties":{"id":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]},"name":{"type":"string"},"description":{"type":"string"},"check":{"type":"string","enum":["http","db","redis","aws-cloudfront","aws-ses"]},"target":{"type":"string"},"degradedAboveMs":{"type":"number"},"timeoutMs":{"type":"number"},"group":{"type":"string","enum":["core","edge","async"]}},"required":["id","name","description","check","target","degradedAboveMs","timeoutMs","group"]},"current":{"type":"string","enum":["operational","degraded","down","unknown"]},"latencyMs":{"type":["number","null"]},"uptime90d":{"type":"object","properties":{"componentId":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]},"uptimePercent":{"type":"number"},"windowSeconds":{"type":"number"},"downSeconds":{"type":"number"},"degradedSeconds":{"type":"number"}},"required":["componentId","uptimePercent","windowSeconds","downSeconds","degradedSeconds"]}},"required":["definition","current","latencyMs","uptime90d"]}},"incidents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"impact":{"type":"string","enum":["minor","major","critical"]},"affectedComponents":{"type":"array","items":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]}},"currentState":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"updates":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"message":{"type":"string"},"postedAt":{"type":"string"}},"required":["state","message","postedAt"]}},"resolvedAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","title","impact","affectedComponents","currentState","updates","resolvedAt","createdAt"]}},"generatedAt":{"type":"string"}},"required":["overall","components","incidents","generatedAt"]},"example":{"overall":"operational","components":[{"definition":{"id":"api","name":"string","description":"string","check":"http","target":"string","degradedAboveMs":0,"timeoutMs":0,"group":"core"},"current":"operational","latencyMs":0,"uptime90d":{"componentId":"api","uptimePercent":0,"windowSeconds":0,"downSeconds":0,"degradedSeconds":0}}],"incidents":[{"id":"string","title":"string","impact":"minor","affectedComponents":["api"],"currentState":"investigating","updates":[{"state":"investigating","message":"string","postedAt":"string"}],"resolvedAt":"string","createdAt":"string"}],"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiStatus","tags":["status"],"parameters":[],"summary":"Public status page payload — components, incidents, and uptime","description":"Ohne Anmeldung abrufbar und mandantenunabhaengig: die Zahlen gelten fuer die Plattform, nicht fuer einen einzelnen Mandanten. Fuer jede fest hinterlegte Komponente wird der Zustand aus dem JUENGSTEN Messpunkt der letzten 90 Tage abgeleitet — gibt es keinen, lautet er \"unknown\", was nicht „in Ordnung\" heiszt. `uptime90d` fasst dasselbe Fenster zusammen. Dazu kommen die Vorfaelle der letzten 30 Tage, offene wie bereits geschlossene. `overall` ist die hoechste Schwere ueber alle Komponenten. Nichts wird gemessen: der Aufruf liest nur, was die Sonden zuvor eingeliefert haben."}},"/api/status/snapshots":{"post":{"responses":{"200":{"description":"Snapshot accepted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"snapshot":{"type":"object","properties":{"componentId":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]},"status":{"type":"string","enum":["operational","degraded","down","unknown"]},"latencyMs":{"type":["number","null"]},"observedAt":{"type":"string"},"message":{"type":"string"}},"required":["componentId","status","latencyMs","observedAt","message"]}},"required":["ok","snapshot"]},"example":{"ok":true,"snapshot":{"componentId":"api","status":"operational","latencyMs":0,"observedAt":"string","message":"string"}}}}},"401":{"description":"Invalid token"},"422":{"description":"Invalid body"},"503":{"description":"Einliefern ist abgeschaltet — kein Token hinterlegt"}},"operationId":"postApiStatusSnapshots","tags":["status"],"parameters":[],"summary":"Submit a probe snapshot — protected by X-Status-Token shared secret","description":"Haengt EINEN Messpunkt an die Zeitreihe einer Komponente an; ein bestehender wird nie ersetzt, und es gibt keinen Weg, einen wieder zu entfernen. Pflicht sind `componentId` und `status`; `latencyMs` faellt auf null zurueck, `observedAt` auf die aktuelle Zeit und `message` auf einen leeren Text — ein Messpunkt ohne Zeitstempel gilt also als jetzt beobachtet. Die Werte werden NICHT gegen die Liste der bekannten Komponenten und Zustaende geprueft. Die Antwort gibt den abgelegten Messpunkt samt Vorgabewerten zurueck. Der Aufruf verlangt die Kopfzeile X-Status-Token; ist serverseitig kein Token hinterlegt, sind Schreib-zugriffe gesperrt (503) statt offen."}},"/api/status/incidents":{"post":{"responses":{"200":{"description":"Incident created","content":{"application/json":{"schema":{"type":"object","properties":{"incident":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"impact":{"type":"string","enum":["minor","major","critical"]},"affectedComponents":{"type":"array","items":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]}},"currentState":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"updates":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"message":{"type":"string"},"postedAt":{"type":"string"}},"required":["state","message","postedAt"]}},"resolvedAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","title","impact","affectedComponents","currentState","updates","resolvedAt","createdAt"]}},"required":["incident"]},"example":{"incident":{"id":"string","title":"string","impact":"minor","affectedComponents":["api"],"currentState":"investigating","updates":[{"state":"investigating","message":"string","postedAt":"string"}],"resolvedAt":"string","createdAt":"string"}}}}},"401":{"description":"Invalid token"},"422":{"description":"Invalid body"},"503":{"description":"Einliefern ist abgeschaltet — kein Token hinterlegt"}},"operationId":"postApiStatusIncidents","tags":["status"],"parameters":[],"summary":"Open a new incident (admin/ingest token required)","description":"Legt einen Vorfall an, der sofort oeffentlich auf der Statusseite erscheint. Die Kennung `id` gibt der AUFRUFER vor — sie ist zugleich die Adresse fuer spaetere Fortschreibungen. Pflicht sind `id` und `title`; `impact` faellt auf \"minor\" zurueck, `affectedComponents` auf eine leere Liste und `initialMessage` auf einen leeren Text. Der Vorfall startet im Zustand \"investigating\" mit genau einem Eintrag im Verlauf; weiter geht es nur ueber POST /:id/updates, und es gibt keinen Weg, einen Vorfall wieder zu loeschen. Der Aufruf verlangt die Kopfzeile X-Status-Token; ist serverseitig kein Token hinterlegt, sind Schreibzugriffe gesperrt (503)."}},"/api/status/incidents/{id}/updates":{"post":{"responses":{"200":{"description":"Update appended","content":{"application/json":{"schema":{"type":"object","properties":{"incident":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"impact":{"type":"string","enum":["minor","major","critical"]},"affectedComponents":{"type":"array","items":{"type":"string","enum":["api","web","worker","aurora","redis","cloudfront","ses"]}},"currentState":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"updates":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"message":{"type":"string"},"postedAt":{"type":"string"}},"required":["state","message","postedAt"]}},"resolvedAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","title","impact","affectedComponents","currentState","updates","resolvedAt","createdAt"]}},"required":["incident"]},"example":{"incident":{"id":"string","title":"string","impact":"minor","affectedComponents":["api"],"currentState":"investigating","updates":[{"state":"investigating","message":"string","postedAt":"string"}],"resolvedAt":"string","createdAt":"string"}}}}},"401":{"description":"Invalid token"},"422":{"description":"Invalid body"},"503":{"description":"Einliefern ist abgeschaltet — kein Token hinterlegt"}},"operationId":"postApiStatusIncidentsByIdUpdates","tags":["status"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Append an update to an existing incident (admin/ingest token required)","description":"Haengt einen Eintrag an den Verlauf eines bestehenden Vorfalls und setzt damit dessen aktuellen Zustand. Pflicht ist `state` (investigating, identified, monitoring, resolved), `message` faellt auf einen leeren Text zurueck. Der Zustand darf nur VORWAERTS oder auf sich selbst wechseln — ein Rueckschritt und jede Aenderung an einem bereits geschlossenen Vorfall werden abgelehnt. Beim ersten Wechsel nach \"resolved\" wird der Abschlusszeitpunkt gesetzt; bestehende Eintraege bleiben unveraendert und lassen sich nicht entfernen. Der Aufruf verlangt die Kopfzeile X-Status-Token; ist serverseitig kein Token hinterlegt, sind Schreibzugriffe gesperrt (503)."}},"/api/public/shares/{token}":{"get":{"responses":{"200":{"description":"Metadaten der Freigabe. `remainingDownloads` ist null, wenn kein Limit gesetzt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"filename":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":["number","null"]},"passwordProtected":{"type":"boolean"},"remainingDownloads":{"type":["integer","null"]}},"required":["filename","mimeType","sizeBytes","passwordProtected","remainingDownloads"]},"example":{"filename":"string","mimeType":"string","sizeBytes":0,"passwordProtected":true,"remainingDownloads":0}}}},"401":{"description":"Der Link ist passwortgeschützt und es kam kein `password` mit (`password required`)"},"403":{"description":"Falsches Passwort (`wrong password`)"},"404":{"description":"Token fehlt oder ist kürzer als 16 Zeichen (`invalid share token`), oder er existiert in keinem Mandanten-Schema (`share not found`)"},"410":{"description":"Freigabe widerrufen (`share revoked`) oder abgelaufen (`share expired`). Beides ist endgültig: kein Endpunkt setzt `revoked_at` oder `expires_at` zurück, der Link wird nicht wieder gültig. Es braucht eine neue Freigabe."},"429":{"description":"Download-Kontingent aufgebraucht (`download limit reached`)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiPublicSharesByToken","tags":["Documents · Sharing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"token","required":true}],"summary":"Public share metadata (no auth)","description":"Liefert die Metadaten hinter einem Freigabe-Token: Dateiname, MIME-Typ, Größe und verbleibende Downloads. Ohne Anmeldung — der Mandant wird über den Token aufgelöst, nicht über ein Cookie. Ist der Link passwortgeschützt, muss das Passwort als Query-Parameter `password` mitkommen. Dieser Aufruf zählt NICHT auf das Download-Kontingent; nur GET /:token/download erhöht den Zähler. Die Datei selbst liefert ebenfalls GET /:token/download."}},"/api/public/shares/{token}/download":{"get":{"responses":{"200":{"description":"Die Datei als Bytes, kein JSON. Der gesendete Content-Type ist der MIME-Typ des Belegs (`mimeType` aus GET /:token); `application/octet-stream` steht hier stellvertretend fuer die Bytes.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Der Link ist passwortgeschützt und es kam kein `password` mit (`password required`)"},"403":{"description":"Falsches Passwort (`wrong password`)"},"404":{"description":"Token fehlt oder ist kürzer als 16 Zeichen (`invalid share token`), er existiert in keinem Mandanten-Schema (`share not found`), oder der Beleg hat keinen Speicherschlüssel (`file not available`)"},"410":{"description":"Freigabe widerrufen (`share revoked`) oder abgelaufen (`share expired`). Beides ist endgültig: kein Endpunkt setzt `revoked_at` oder `expires_at` zurück, der Link wird nicht wieder gültig. Es braucht eine neue Freigabe."},"429":{"description":"Download-Kontingent aufgebraucht (`download limit reached`)"},"502":{"description":"Der Speicher lieferte die Datei nicht (`download failed`). `download_count` bleibt unverändert — der Zähler steigt erst nach erfolgreichem Abruf, ein Fehlversuch verbraucht kein Kontingent."},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiPublicSharesByTokenDownload","tags":["Documents · Sharing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"token","required":true}],"summary":"Public share download (no auth)","description":"Liefert die Datei hinter einem Freigabe-Token als Anhang (Content-Disposition: attachment, Cache-Control: private, no-store). Ohne Anmeldung — der Mandant wird über den Token aufgelöst. Ist der Link passwortgeschützt, muss das Passwort als Query-Parameter `password` mitkommen. Jeder erfolgreiche Abruf erhöht `download_count` um 1 und kann den Link damit aufbrauchen, sobald `max_downloads` erreicht ist; dieser Zähler lässt sich über keinen Endpunkt zurücksetzen."}},"/api/public/invoices/{token}":{"get":{"responses":{"200":{"description":"Die Rechnung als PDF. Kein JSON-Rumpf — der Körper sind die PDF-Bytes, ausgeliefert mit `Content-Type: application/pdf`, `Content-Disposition: inline; filename=\"Rechnung-<Nr>.pdf\"` und `Cache-Control: private, no-store`.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Bewusst ununterscheidbar für fünf verschiedene Ursachen: Token ist keine UUID, Datenbank nicht erreichbar, kein Eintrag zum Token, aufgelöster Schema-Name besteht die Musterprüfung nicht, Rechnung nicht (mehr) vorhanden. Der Rumpf ist immer `{ \"error\": \"not_found\" }` — wer rät, erfährt nichts über den Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"500":{"description":"Der PDF-Aufbau ist gescheitert. Rumpf `{ \"error\": \"server_error\" }`, der Grund steht nur im Serverprotokoll.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"server_error"}},"required":["error"]}}}}},"operationId":"getApiPublicInvoicesByToken","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"token","required":true}],"summary":"Rechnung als PDF über einen Freigabe-Token (ohne Anmeldung)","description":"Gibt die Rechnung als PDF-Datenstrom aus — KEIN JSON. Erfolgsantwort: `Content-Type: application/pdf`, `Content-Disposition: inline; filename=\"Rechnung-<Nr>.pdf\"`, `Cache-Control: private, no-store`. Nur die Fehlerfälle antworten mit JSON.\n\nWER DAS AUFRUFT: Endkunden, keine Mitarbeiter. Die Web-Seite `/r/[token]` bettet diese Adresse in ein `<object>` ein; der Link geht mit dem Rechnungsversand raus.\n\nWIE DER ZUGRIFF GESCHÜTZT IST (nachgesehen, nicht angenommen): der Router hängt in `index.ts` direkt an `app` und damit VOR der `api`-Unter-App, die `authMiddleware` und `tenantMiddleware` trägt. Er läuft deshalb ohne jede Sitzungsprüfung und steht folgerichtig auch nicht in `PUBLIC_PATHS` — diese Liste sieht ihn nie. Der Ausweis IST der Token im Pfad: eine UUID, die `ensureInvoiceViewLink()` (invoices.ts) beim Versand vergibt und in `public.invoice_public_links` auf genau EIN Paar (tenant_slug, invoice_id) abbildet. Kein Schema-Durchlauf, kein Raten über fremde Mandanten.\n\nABLAUF UND WIDERRUF, ausdrücklich: der Token läuft NICHT ab. `public.invoice_public_links` führt nur token, tenant_slug, invoice_id, created_at — keine Frist, kein Widerrufs-Kennzeichen. Er wird über mehrere Versände hinweg absichtlich wiederverwendet, damit der Link stabil bleibt. Entzogen wird der Zugriff einzig dadurch, dass die Rechnung gelöscht wird: der PDF-Aufbau liest über `findById` mit `deleted_at IS NULL`, danach antwortet dieselbe Adresse mit 404.\n\nZUM 401 IN DIESER SPEZIFIKATION: den kann diese Route nicht liefern. Er wird von `applyUnauthorizedResponse` (lib/api-doku/security.ts) nachträglich angehängt, weil die Funktion „öffentlich\" allein an `isPublicPath()` misst. Dieser Pfad steht dort nicht — er ist nicht über die Liste offen, sondern weil er vor der `api`-Unter-App hängt und die Auth-Middleware ihn nie sieht. Gemessen 30.08.2026; wer sich darauf verlässt, wartet auf ein 401, das nie kommt."}},"/metrics":{"get":{"responses":{"200":{"description":"Prometheus metrics"},"401":{"description":"Unauthorized — missing or invalid METRICS_SCRAPE_TOKEN"}},"operationId":"getMetrics","tags":["metrics"],"parameters":[],"description":"Prometheus scrape endpoint — bearer-token protected. Returns text exposition format.","summary":"Prometheus scrape endpoint — bearer-token protected","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/v1/metrics/prom":{"get":{"responses":{"200":{"description":"Prometheus text exposition format (version 0.0.4), NOT JSON. Covers only the in-process API counters — latency percentiles, requests per second, request and error totals since process start, error rate as a 0..1 ratio, and uptime. Database, Redis and queue stats are NOT part of this endpoint; those live in `GET /api/v1/metrics`. Sent with `Cache-Control: no-cache`.","content":{"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden — a plain-text `forbidden`, not a JSON error. Fail-closed: without `METRICS_SCRAPE_TOKEN` in the environment the endpoint is locked in EVERY environment, development included.","content":{"text/plain":{"schema":{"type":"string"}}}}},"operationId":"getApiV1MetricsProm","tags":["metrics"],"parameters":[],"description":"Prometheus scrape endpoint — bearer-token protected. Returns text exposition format.","summary":"Prometheus scrape endpoint — bearer-token protected","x-nemix-summary-source":"description:first-sentence"}},"/auth/status":{"get":{"responses":{"200":{"description":"Status payload","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ready","demo-mode"]},"database":{"type":"boolean"},"auth":{"type":"boolean"},"message":{"type":"string"}},"required":["status","database","auth","message"],"additionalProperties":false},"example":{"status":"ready","database":true,"auth":true,"message":"string"}}}}},"operationId":"getAuthStatus","tags":["auth"],"parameters":[],"summary":"Status des Anmeldesystems","description":"Auth subsystem status — whether Better-Auth + DB are ready or running in demo mode.","security":[]}},"/auth/social-providers":{"get":{"responses":{"200":{"description":"Provider availability map","content":{"application/json":{"schema":{"type":"object","properties":{"google":{"type":"boolean"},"microsoft":{"type":"boolean"},"setupDocsUrl":{"type":"string"}},"required":["google","microsoft","setupDocsUrl"],"additionalProperties":false},"example":{"google":true,"microsoft":true,"setupDocsUrl":"string"}}}}},"operationId":"getAuthSocial-providers","tags":["auth"],"parameters":[],"summary":"Verfuegbare Anmeldeanbieter","description":"Returns which social OAuth providers are configured on this instance.","security":[]}},"/auth/check-slug":{"get":{"responses":{"200":{"description":"Availability result","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean"},"reason":{"type":"string","enum":["reserved","taken","error"]},"demo":{"type":"boolean"}},"required":["available"],"additionalProperties":false},"example":{"available":true,"reason":"reserved","demo":true}}}},"400":{"description":"Parameter `slug` fehlt oder verletzt das Format ^[a-z0-9-]{3,56}$"},"500":{"description":"Pruefung fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean"},"reason":{"type":"string","enum":["reserved","taken","error"]},"demo":{"type":"boolean"}},"required":["available"],"additionalProperties":false}}}}},"operationId":"getAuthCheck-slug","tags":["auth"],"parameters":[{"in":"query","name":"slug","schema":{"type":"string","pattern":"^[a-z0-9-]+$","minLength":3,"maxLength":56},"required":true}],"summary":"Mandantenkuerzel auf Verfuegbarkeit pruefen","description":"Check if a tenant slug is available for registration. Der Parameter `slug` ist PFLICHT — ohne ihn antwortet die Route mit 400. Der Vertragstest vom 06.08.2026 hat genau das gemeldet, weil die Pflicht in der Spezifikation nicht erkennbar war.","security":[]}},"/auth/register-tenant":{"post":{"responses":{"201":{"description":"Tenant created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"tenant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"layerId":{"type":["string","null"]}},"required":["id","slug","name","layerId"],"additionalProperties":false},"user":{"type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"emailVerificationRequired":{"type":"boolean","const":true}},"required":["email","name","emailVerificationRequired"],"additionalProperties":false},"nextStep":{"type":"string","const":"verify-email"},"signUpResult":{"type":["object","null"],"properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}},"required":["success","message","tenant","user","nextStep","signUpResult"],"additionalProperties":false},"example":{"success":true,"message":"string","tenant":{"id":"string","slug":"string","name":"string","layerId":"string"},"user":{"email":"string","name":"string","emailVerificationRequired":true},"nextStep":"verify-email","signUpResult":{"ok":true}}}}},"400":{"description":"Slug ist reserviert, oder der Rumpf verletzt das Pruefschema"},"403":{"description":"Self-service registration is closed on this deployment"},"409":{"description":"Slug bereits vergeben"},"500":{"description":"Anlage fehlgeschlagen — Schema und Mandantenzeile werden zurueckgerollt"},"503":{"description":"Auth unavailable"}},"operationId":"postAuthRegister-tenant","tags":["auth"],"parameters":[],"summary":"Mandant anlegen (oeffentlich)","description":"Register a new tenant with primary admin user. Sends a welcome email. Answers 403 REGISTRATION_CLOSED unless NEMIX_REGISTRATION_OPEN=true — see lib/registration-lock.ts.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantSlug":{"type":"string","pattern":"^[a-z0-9-]+$","minLength":3,"maxLength":56},"tenantName":{"type":"string","minLength":2,"maxLength":255},"industry":{"type":"string","enum":["bau","handel","dienstleistung","fertigung"]},"adminEmail":{"type":"string","format":"email"},"adminPassword":{"allOf":[{"type":"string","pattern":"[A-Z]","minLength":8,"maxLength":128},{"type":"string","pattern":"[0-9]"}]},"adminName":{"type":"string","minLength":2},"acceptTerms":{"type":"boolean","const":true},"planId":{"type":"string","format":"uuid"},"sitzland":{"type":"string","pattern":"^[A-Z]{2}$"},"profil":{"type":"object","properties":{"branchen":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":200,"default":[]},"groesse":{"type":"string","maxLength":32,"default":""},"laender":{"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"maxItems":200,"default":[]},"sprachen":{"type":"array","items":{"type":"string","minLength":2,"maxLength":8},"maxItems":200,"default":[]}}}},"required":["tenantSlug","tenantName","industry","adminEmail","adminPassword","adminName","acceptTerms","sitzland"]}}}},"security":[]}},"/auth/login":{"post":{"responses":{"200":{"description":"Session created"},"401":{"description":"Invalid credentials"}},"operationId":"postAuthLogin","tags":["auth"],"parameters":[],"summary":"Anmelden mit E-Mail und Passwort","description":"Sign in with email + password. Forwards to Better-Auth /sign-in/email.","security":[]}},"/auth/register":{"post":{"responses":{"201":{"description":"User created"},"400":{"description":"Invalid body"},"403":{"description":"Self-service registration is closed on this deployment"},"409":{"description":"Email already in use"}},"operationId":"postAuthRegister","tags":["auth"],"parameters":[],"summary":"Registrieren — legt Nutzer und Mandant an","description":"Register a new user with email + password. Auto-creates a tenant so the user can write immediately. Answers 403 REGISTRATION_CLOSED unless NEMIX_REGISTRATION_OPEN=true — see lib/registration-lock.ts.","security":[]}},"/auth/repair-orphaned-users":{"post":{"responses":{"200":{"description":"Repair result — eine Klartextzeile je bearbeitetem Nutzer","content":{"application/json":{"schema":{"type":"object","properties":{"repaired":{"type":"array","items":{"type":"string"},"description":"Je Nutzer eine Klartextzeile, auch bei Fehlern"},"count":{"type":"integer","description":"Laenge von `repaired`, nicht die Zahl der Erfolge"}},"required":["repaired","count"],"additionalProperties":false},"example":{"repaired":["string"],"count":0}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postAuthRepair-orphaned-users","tags":["auth"],"parameters":[],"summary":"Wartung: Nutzer ohne Mandant reparieren (nur dev)","description":"Create a tenant for every user with tenant_id=NULL or role=user. Dev-only, Bootstrap-Secret required.","security":[]}},"/auth/refresh":{"get":{"responses":{"200":{"description":"Session payload"}},"operationId":"getAuthRefresh","tags":["auth"],"parameters":[],"summary":"Aktuelle Sitzung lesen bzw. erneuern","description":"Refresh / read the current session. Forwards to Better-Auth /get-session.","security":[]},"post":{"responses":{"200":{"description":"Session payload"}},"operationId":"postAuthRefresh","tags":["auth"],"parameters":[],"summary":"Aktuelle Sitzung lesen bzw. erneuern","description":"Refresh / read the current session. Forwards to Better-Auth /get-session.","security":[]}},"/auth/fix-duplicate-users":{"post":{"responses":{"200":{"description":"Duplicate users fixed. `success: true` heisst „durchgelaufen\", nicht „alles gelungen\" — Fehler einzelner Schritte stehen als Zeile in `results`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"results":{"type":"array","items":{"type":"string"}}},"required":["success","results"],"additionalProperties":false},"example":{"success":true,"results":["string"]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postAuthFix-duplicate-users","tags":["auth"],"parameters":[],"summary":"Wartung: doppelte Nutzer zusammenfuehren (nur dev)","description":"Clean up duplicate user entries created by re-signups. Dev-only, X-Bootstrap-Secret required.","security":[]}},"/auth/fix-tenant-owner-roles":{"post":{"responses":{"200":{"description":"Fixed tenant owner roles — eine Klartextzeile je betroffenem Eigentuemer","content":{"application/json":{"schema":{"type":"object","properties":{"fixed":{"type":"array","items":{"type":"string"},"description":"Je betroffenem Eigentuemer eine Klartextzeile"},"count":{"type":"integer","description":"Laenge von `fixed`, nicht die Zahl der Erfolge"}},"required":["fixed","count"],"additionalProperties":false},"example":{"fixed":["string"],"count":0}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postAuthFix-tenant-owner-roles","tags":["auth"],"parameters":[],"summary":"Wartung: Mandanten-Eigentuemer auf admin heben (nur dev)","description":"Promote new-tenant owners with role=user to admin. Dev-only, X-Bootstrap-Secret required.","security":[]}},"/auth/debug-users":{"get":{"responses":{"200":{"description":"User state. Die Zeilen gehen roh hinaus (snake_case) und enthalten den ANFANG des gespeicherten Passwort-Hashes — deshalb ist der Endpunkt ausserhalb der Entwicklung zu.","content":{"application/json":{"schema":{"type":"object","properties":{"userState":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string"},"users":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"email_verified":{"type":"boolean"},"tenant_id":{"type":["string","null"]},"role":{"type":["string","null"]}},"required":["id","email","email_verified","tenant_id","role"],"additionalProperties":false}},"accounts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"provider_id":{"type":"string"},"account_id":{"type":"string"},"password":{"type":["string","null"],"description":"Erste 20 Zeichen des Hashes"},"hasPassword":{"type":"boolean"},"passwordPrefix":{"type":["string","null"],"description":"Erste 15 Zeichen plus \"...\""}},"required":["id","provider_id","account_id","password","hasPassword","passwordPrefix"],"additionalProperties":false}}},"required":["email","users","accounts"],"additionalProperties":false}}},"required":["userState"],"additionalProperties":false},"example":{"userState":[{"email":"string","users":[{"id":"string","email":"string","email_verified":true,"tenant_id":"string","role":"string"}],"accounts":[{"id":"string","provider_id":"string","account_id":"string","password":"string","hasPassword":true,"passwordPrefix":"string"}]}]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getAuthDebug-users","tags":["auth"],"parameters":[],"summary":"Diagnose: Zustand der Bootstrap-Nutzer (nur dev)","description":"Debug: show user + account state for bootstrap users. Dev-only, X-Bootstrap-Secret required.","security":[]}},"/auth/debug-tenants":{"get":{"responses":{"200":{"description":"Tenant list — die 50 zuletzt angelegten Mandanten mit dem Erstnutzer, dazu bis zu 20 Nutzer ohne Mandant. Die Zeilen gehen roh hinaus (snake_case).","content":{"application/json":{"schema":{"type":"object","properties":{"tenants":{"type":"array","items":{"type":"object","properties":{"tenant_id":{"type":"string"},"tenant_name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"},"owner_email":{"type":["string","null"]},"owner_role":{"type":["string","null"]},"owner_tenant_id":{"type":["string","null"]},"owner_email_verified":{"type":["boolean","null"]},"user_count":{"type":"string","description":"Anzahl als Zeichenkette — COUNT(*)::text"}},"required":["tenant_id","tenant_name","slug","status","created_at","owner_email","owner_role","owner_tenant_id","owner_email_verified","user_count"],"additionalProperties":false},"description":"Die 50 zuletzt angelegten Mandanten, neueste zuerst"},"orphanedUsers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":["string","null"]},"tenant_id":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","email","role","tenant_id","created_at"],"additionalProperties":false},"description":"Hoechstens 20 Nutzer ohne Mandant oder mit Rolle `user`"}},"required":["tenants","orphanedUsers"],"additionalProperties":false},"example":{"tenants":[{"tenant_id":"string","tenant_name":"string","slug":"string","status":"string","created_at":"string","owner_email":"string","owner_role":"string","owner_tenant_id":"string","owner_email_verified":true,"user_count":"string"}],"orphanedUsers":[{"id":"string","email":"string","role":"string","tenant_id":"string","created_at":"string"}]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getAuthDebug-tenants","tags":["auth"],"parameters":[],"summary":"Diagnose: alle Mandanten mit Rolle des Erstnutzers (nur dev)","description":"Debug: list all tenants and their first user role. Dev-only, Bootstrap-Secret required.","security":[]}},"/auth/bootstrap-users":{"post":{"responses":{"200":{"description":"Users created/seeded. `success: true` heisst „durchgelaufen\", nicht „alles gelungen\" — Fehler einzelner Schritte stehen als Zeile in `results`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"results":{"type":"array","items":{"type":"string"}}},"required":["success","results"],"additionalProperties":false},"example":{"success":true,"results":["string"]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"500":{"description":"Kein Startpasswort: weder im Rumpf noch in DEV_BOOTSTRAP_PASSWORD"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postAuthBootstrap-users","tags":["auth"],"parameters":[],"summary":"Standardnutzer fuer einen frischen Mandanten anlegen","description":"Bootstrap default users for a fresh tenant (idempotent, demo seeding).","security":[]}},"/auth/sessions":{"get":{"responses":{"200":{"description":"Liste aktiver Sessions","content":{"application/json":{"schema":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"device":{"type":"string"},"browser":{"type":"string"},"ipRegion":{"type":["string","null"]},"lastActiveAt":{},"current":{"type":"boolean"}},"required":["id","device","browser","ipRegion","current"],"additionalProperties":false}}},"required":["sessions"],"additionalProperties":false},"example":{"sessions":[{"id":"string","device":"string","browser":"string","ipRegion":"string","current":true}]}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht verfuegbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getAuthSessions","tags":["auth"],"parameters":[],"summary":"Aktive Sitzungen auflisten","description":"Listet aktive Sessions des Users.","security":[]},"delete":{"responses":{"200":{"description":"Sessions beendet — `revoked` nennt die Anzahl","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"revoked":{"type":"integer"}},"required":["ok","revoked"],"additionalProperties":false},"example":{"ok":true,"revoked":0}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteAuthSessions","tags":["auth"],"parameters":[],"summary":"Alle anderen Sitzungen beenden","description":"Beendet alle Sessions außer der aktuellen.","security":[]}},"/auth/sessions/{id}":{"delete":{"responses":{"200":{"description":"Session beendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","id"],"additionalProperties":false},"example":{"ok":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Session nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteAuthSessionsById","tags":["auth"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelne Sitzung beenden","description":"Beendet eine einzelne Session.","security":[]}},"/auth/login-history":{"get":{"responses":{"200":{"description":"Login-Events (nur erfolgreiche), maximal 200","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"at":{},"ip":{"type":["string","null"]},"userAgent":{"type":["string","null"]},"status":{"type":"string","const":"success"}},"required":["id","ip","userAgent","status"],"additionalProperties":false}}},"required":["events"],"additionalProperties":false},"example":{"events":[{"id":"string","ip":"string","userAgent":"string","status":"success"}]}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getAuthLogin-history","tags":["auth"],"parameters":[],"summary":"Anmeldungen der letzten 30 Tage","description":"Login-Historie der letzten 30 Tage. ACHTUNG fuer Integratoren: die Liste wird aus bestehenden Sitzungen abgeleitet — eine Sitzung entsteht erst NACH erfolgreicher Anmeldung. FEHLVERSUCHE STEHEN NICHT DRIN, `status` ist deshalb immer `success`. Wer ein vollstaendiges Anmeldeprotokoll braucht, kann diese Route dafuer nicht nehmen.","security":[]}},"/auth/change-password":{"post":{"responses":{"200":{"description":"Passwort geaendert. Alle uebrigen Sitzungen des Nutzers werden dabei beendet (`revokeOtherSessions`); `result` reicht die Antwort von Better-Auth durch.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"result":{"description":"Rueckgabe von Better-Auth, oder null — Form nicht zugesagt"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Validation failed"},"401":{"description":"Nicht authentifiziert oder current password falsch"},"503":{"description":"Auth-Service nicht verfuegbar"}},"operationId":"postAuthChange-password","tags":["auth"],"parameters":[],"summary":"Passwort aendern","description":"Aendert das Passwort des aktuellen Users (Fallback-Route fuer Better-Auth).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentPassword":{"type":"string","minLength":1},"newPassword":{"allOf":[{"type":"string","pattern":"[A-Z]","minLength":8,"maxLength":128},{"type":"string","pattern":"[0-9]"}]}},"required":["currentPassword","newPassword"]}}}},"security":[]}},"/auth/revoke-all-sessions":{"post":{"responses":{"200":{"description":"Alle Sessions beendet — `revoked` nennt die Anzahl","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"revoked":{"type":"integer"}},"required":["ok","revoked"],"additionalProperties":false},"example":{"ok":true,"revoked":0}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postAuthRevoke-all-sessions","tags":["auth"],"parameters":[],"summary":"Alle Sitzungen beenden — auch die eigene","description":"Beendet ALLE Sessions des Users (inkl. der aktuellen).","security":[]}},"/auth/me":{"get":{"responses":{"200":{"description":"User-Profil","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"name":{"type":["string","null"]},"image":{"type":["string","null"]},"tenantId":{"type":["string","null"]},"createdAt":{}},"required":["id","email","name","image","tenantId"],"additionalProperties":false},"example":{"id":"string","email":"string","name":"string","image":"string","tenantId":"string"}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Nutzer nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getAuthMe","tags":["auth"],"parameters":[],"summary":"Profil des angemeldeten Nutzers","description":"Gibt das Profil des aktuell eingeloggten Users zurück.","security":[]}},"/api/auth/status":{"get":{"responses":{"200":{"description":"Status payload","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ready","demo-mode"]},"database":{"type":"boolean"},"auth":{"type":"boolean"},"message":{"type":"string"}},"required":["status","database","auth","message"],"additionalProperties":false},"example":{"status":"ready","database":true,"auth":true,"message":"string"}}}}},"operationId":"getApiAuthStatus","tags":["auth"],"parameters":[],"summary":"Status des Anmeldesystems","description":"Auth subsystem status — whether Better-Auth + DB are ready or running in demo mode.","security":[]}},"/api/auth/social-providers":{"get":{"responses":{"200":{"description":"Provider availability map","content":{"application/json":{"schema":{"type":"object","properties":{"google":{"type":"boolean"},"microsoft":{"type":"boolean"},"setupDocsUrl":{"type":"string"}},"required":["google","microsoft","setupDocsUrl"],"additionalProperties":false},"example":{"google":true,"microsoft":true,"setupDocsUrl":"string"}}}}},"operationId":"getApiAuthSocial-providers","tags":["auth"],"parameters":[],"summary":"Verfuegbare Anmeldeanbieter","description":"Returns which social OAuth providers are configured on this instance.","security":[]}},"/api/auth/check-slug":{"get":{"responses":{"200":{"description":"Availability result","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean"},"reason":{"type":"string","enum":["reserved","taken","error"]},"demo":{"type":"boolean"}},"required":["available"],"additionalProperties":false},"example":{"available":true,"reason":"reserved","demo":true}}}},"400":{"description":"Parameter `slug` fehlt oder verletzt das Format ^[a-z0-9-]{3,56}$"},"500":{"description":"Pruefung fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean"},"reason":{"type":"string","enum":["reserved","taken","error"]},"demo":{"type":"boolean"}},"required":["available"],"additionalProperties":false}}}}},"operationId":"getApiAuthCheck-slug","tags":["auth"],"parameters":[{"in":"query","name":"slug","schema":{"type":"string","pattern":"^[a-z0-9-]+$","minLength":3,"maxLength":56},"required":true}],"summary":"Mandantenkuerzel auf Verfuegbarkeit pruefen","description":"Check if a tenant slug is available for registration. Der Parameter `slug` ist PFLICHT — ohne ihn antwortet die Route mit 400. Der Vertragstest vom 06.08.2026 hat genau das gemeldet, weil die Pflicht in der Spezifikation nicht erkennbar war.","security":[]}},"/api/auth/register-tenant":{"post":{"responses":{"201":{"description":"Tenant created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"tenant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"layerId":{"type":["string","null"]}},"required":["id","slug","name","layerId"],"additionalProperties":false},"user":{"type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"emailVerificationRequired":{"type":"boolean","const":true}},"required":["email","name","emailVerificationRequired"],"additionalProperties":false},"nextStep":{"type":"string","const":"verify-email"},"signUpResult":{"type":["object","null"],"properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}},"required":["success","message","tenant","user","nextStep","signUpResult"],"additionalProperties":false},"example":{"success":true,"message":"string","tenant":{"id":"string","slug":"string","name":"string","layerId":"string"},"user":{"email":"string","name":"string","emailVerificationRequired":true},"nextStep":"verify-email","signUpResult":{"ok":true}}}}},"400":{"description":"Slug ist reserviert, oder der Rumpf verletzt das Pruefschema"},"403":{"description":"Self-service registration is closed on this deployment"},"409":{"description":"Slug bereits vergeben"},"500":{"description":"Anlage fehlgeschlagen — Schema und Mandantenzeile werden zurueckgerollt"},"503":{"description":"Auth unavailable"}},"operationId":"postApiAuthRegister-tenant","tags":["auth"],"parameters":[],"summary":"Mandant anlegen (oeffentlich)","description":"Register a new tenant with primary admin user. Sends a welcome email. Answers 403 REGISTRATION_CLOSED unless NEMIX_REGISTRATION_OPEN=true — see lib/registration-lock.ts.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantSlug":{"type":"string","pattern":"^[a-z0-9-]+$","minLength":3,"maxLength":56},"tenantName":{"type":"string","minLength":2,"maxLength":255},"industry":{"type":"string","enum":["bau","handel","dienstleistung","fertigung"]},"adminEmail":{"type":"string","format":"email"},"adminPassword":{"allOf":[{"type":"string","pattern":"[A-Z]","minLength":8,"maxLength":128},{"type":"string","pattern":"[0-9]"}]},"adminName":{"type":"string","minLength":2},"acceptTerms":{"type":"boolean","const":true},"planId":{"type":"string","format":"uuid"},"sitzland":{"type":"string","pattern":"^[A-Z]{2}$"},"profil":{"type":"object","properties":{"branchen":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":200,"default":[]},"groesse":{"type":"string","maxLength":32,"default":""},"laender":{"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"maxItems":200,"default":[]},"sprachen":{"type":"array","items":{"type":"string","minLength":2,"maxLength":8},"maxItems":200,"default":[]}}}},"required":["tenantSlug","tenantName","industry","adminEmail","adminPassword","adminName","acceptTerms","sitzland"]}}}},"security":[]}},"/api/auth/login":{"post":{"responses":{"200":{"description":"Session created"},"401":{"description":"Invalid credentials"}},"operationId":"postApiAuthLogin","tags":["auth"],"parameters":[],"summary":"Anmelden mit E-Mail und Passwort","description":"Sign in with email + password. Forwards to Better-Auth /sign-in/email.","security":[]}},"/api/auth/register":{"post":{"responses":{"201":{"description":"User created"},"400":{"description":"Invalid body"},"403":{"description":"Self-service registration is closed on this deployment"},"409":{"description":"Email already in use"}},"operationId":"postApiAuthRegister","tags":["auth"],"parameters":[],"summary":"Registrieren — legt Nutzer und Mandant an","description":"Register a new user with email + password. Auto-creates a tenant so the user can write immediately. Answers 403 REGISTRATION_CLOSED unless NEMIX_REGISTRATION_OPEN=true — see lib/registration-lock.ts.","security":[]}},"/api/auth/repair-orphaned-users":{"post":{"responses":{"200":{"description":"Repair result — eine Klartextzeile je bearbeitetem Nutzer","content":{"application/json":{"schema":{"type":"object","properties":{"repaired":{"type":"array","items":{"type":"string"},"description":"Je Nutzer eine Klartextzeile, auch bei Fehlern"},"count":{"type":"integer","description":"Laenge von `repaired`, nicht die Zahl der Erfolge"}},"required":["repaired","count"],"additionalProperties":false},"example":{"repaired":["string"],"count":0}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiAuthRepair-orphaned-users","tags":["auth"],"parameters":[],"summary":"Wartung: Nutzer ohne Mandant reparieren (nur dev)","description":"Create a tenant for every user with tenant_id=NULL or role=user. Dev-only, Bootstrap-Secret required.","security":[]}},"/api/auth/refresh":{"get":{"responses":{"200":{"description":"Session payload"}},"operationId":"getApiAuthRefresh","tags":["auth"],"parameters":[],"summary":"Aktuelle Sitzung lesen bzw. erneuern","description":"Refresh / read the current session. Forwards to Better-Auth /get-session.","security":[]},"post":{"responses":{"200":{"description":"Session payload"}},"operationId":"postApiAuthRefresh","tags":["auth"],"parameters":[],"summary":"Aktuelle Sitzung lesen bzw. erneuern","description":"Refresh / read the current session. Forwards to Better-Auth /get-session.","security":[]}},"/api/auth/fix-duplicate-users":{"post":{"responses":{"200":{"description":"Duplicate users fixed. `success: true` heisst „durchgelaufen\", nicht „alles gelungen\" — Fehler einzelner Schritte stehen als Zeile in `results`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"results":{"type":"array","items":{"type":"string"}}},"required":["success","results"],"additionalProperties":false},"example":{"success":true,"results":["string"]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiAuthFix-duplicate-users","tags":["auth"],"parameters":[],"summary":"Wartung: doppelte Nutzer zusammenfuehren (nur dev)","description":"Clean up duplicate user entries created by re-signups. Dev-only, X-Bootstrap-Secret required.","security":[]}},"/api/auth/fix-tenant-owner-roles":{"post":{"responses":{"200":{"description":"Fixed tenant owner roles — eine Klartextzeile je betroffenem Eigentuemer","content":{"application/json":{"schema":{"type":"object","properties":{"fixed":{"type":"array","items":{"type":"string"},"description":"Je betroffenem Eigentuemer eine Klartextzeile"},"count":{"type":"integer","description":"Laenge von `fixed`, nicht die Zahl der Erfolge"}},"required":["fixed","count"],"additionalProperties":false},"example":{"fixed":["string"],"count":0}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiAuthFix-tenant-owner-roles","tags":["auth"],"parameters":[],"summary":"Wartung: Mandanten-Eigentuemer auf admin heben (nur dev)","description":"Promote new-tenant owners with role=user to admin. Dev-only, X-Bootstrap-Secret required.","security":[]}},"/api/auth/debug-users":{"get":{"responses":{"200":{"description":"User state. Die Zeilen gehen roh hinaus (snake_case) und enthalten den ANFANG des gespeicherten Passwort-Hashes — deshalb ist der Endpunkt ausserhalb der Entwicklung zu.","content":{"application/json":{"schema":{"type":"object","properties":{"userState":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string"},"users":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"email_verified":{"type":"boolean"},"tenant_id":{"type":["string","null"]},"role":{"type":["string","null"]}},"required":["id","email","email_verified","tenant_id","role"],"additionalProperties":false}},"accounts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"provider_id":{"type":"string"},"account_id":{"type":"string"},"password":{"type":["string","null"],"description":"Erste 20 Zeichen des Hashes"},"hasPassword":{"type":"boolean"},"passwordPrefix":{"type":["string","null"],"description":"Erste 15 Zeichen plus \"...\""}},"required":["id","provider_id","account_id","password","hasPassword","passwordPrefix"],"additionalProperties":false}}},"required":["email","users","accounts"],"additionalProperties":false}}},"required":["userState"],"additionalProperties":false},"example":{"userState":[{"email":"string","users":[{"id":"string","email":"string","email_verified":true,"tenant_id":"string","role":"string"}],"accounts":[{"id":"string","provider_id":"string","account_id":"string","password":"string","hasPassword":true,"passwordPrefix":"string"}]}]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiAuthDebug-users","tags":["auth"],"parameters":[],"summary":"Diagnose: Zustand der Bootstrap-Nutzer (nur dev)","description":"Debug: show user + account state for bootstrap users. Dev-only, X-Bootstrap-Secret required.","security":[]}},"/api/auth/debug-tenants":{"get":{"responses":{"200":{"description":"Tenant list — die 50 zuletzt angelegten Mandanten mit dem Erstnutzer, dazu bis zu 20 Nutzer ohne Mandant. Die Zeilen gehen roh hinaus (snake_case).","content":{"application/json":{"schema":{"type":"object","properties":{"tenants":{"type":"array","items":{"type":"object","properties":{"tenant_id":{"type":"string"},"tenant_name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"},"owner_email":{"type":["string","null"]},"owner_role":{"type":["string","null"]},"owner_tenant_id":{"type":["string","null"]},"owner_email_verified":{"type":["boolean","null"]},"user_count":{"type":"string","description":"Anzahl als Zeichenkette — COUNT(*)::text"}},"required":["tenant_id","tenant_name","slug","status","created_at","owner_email","owner_role","owner_tenant_id","owner_email_verified","user_count"],"additionalProperties":false},"description":"Die 50 zuletzt angelegten Mandanten, neueste zuerst"},"orphanedUsers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":["string","null"]},"tenant_id":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","email","role","tenant_id","created_at"],"additionalProperties":false},"description":"Hoechstens 20 Nutzer ohne Mandant oder mit Rolle `user`"}},"required":["tenants","orphanedUsers"],"additionalProperties":false},"example":{"tenants":[{"tenant_id":"string","tenant_name":"string","slug":"string","status":"string","created_at":"string","owner_email":"string","owner_role":"string","owner_tenant_id":"string","owner_email_verified":true,"user_count":"string"}],"orphanedUsers":[{"id":"string","email":"string","role":"string","tenant_id":"string","created_at":"string"}]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiAuthDebug-tenants","tags":["auth"],"parameters":[],"summary":"Diagnose: alle Mandanten mit Rolle des Erstnutzers (nur dev)","description":"Debug: list all tenants and their first user role. Dev-only, Bootstrap-Secret required.","security":[]}},"/api/auth/bootstrap-users":{"post":{"responses":{"200":{"description":"Users created/seeded. `success: true` heisst „durchgelaufen\", nicht „alles gelungen\" — Fehler einzelner Schritte stehen als Zeile in `results`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"results":{"type":"array","items":{"type":"string"}}},"required":["success","results"],"additionalProperties":false},"example":{"success":true,"results":["string"]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"500":{"description":"Kein Startpasswort: weder im Rumpf noch in DEV_BOOTSTRAP_PASSWORD"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiAuthBootstrap-users","tags":["auth"],"parameters":[],"summary":"Standardnutzer fuer einen frischen Mandanten anlegen","description":"Bootstrap default users for a fresh tenant (idempotent, demo seeding).","security":[]}},"/api/auth/sessions":{"get":{"responses":{"200":{"description":"Liste aktiver Sessions","content":{"application/json":{"schema":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"device":{"type":"string"},"browser":{"type":"string"},"ipRegion":{"type":["string","null"]},"lastActiveAt":{},"current":{"type":"boolean"}},"required":["id","device","browser","ipRegion","current"],"additionalProperties":false}}},"required":["sessions"],"additionalProperties":false},"example":{"sessions":[{"id":"string","device":"string","browser":"string","ipRegion":"string","current":true}]}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht verfuegbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiAuthSessions","tags":["auth"],"parameters":[],"summary":"Aktive Sitzungen auflisten","description":"Listet aktive Sessions des Users.","security":[]},"delete":{"responses":{"200":{"description":"Sessions beendet — `revoked` nennt die Anzahl","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"revoked":{"type":"integer"}},"required":["ok","revoked"],"additionalProperties":false},"example":{"ok":true,"revoked":0}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiAuthSessions","tags":["auth"],"parameters":[],"summary":"Alle anderen Sitzungen beenden","description":"Beendet alle Sessions außer der aktuellen.","security":[]}},"/api/auth/sessions/{id}":{"delete":{"responses":{"200":{"description":"Session beendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","id"],"additionalProperties":false},"example":{"ok":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Session nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiAuthSessionsById","tags":["auth"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelne Sitzung beenden","description":"Beendet eine einzelne Session.","security":[]}},"/api/auth/login-history":{"get":{"responses":{"200":{"description":"Login-Events (nur erfolgreiche), maximal 200","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"at":{},"ip":{"type":["string","null"]},"userAgent":{"type":["string","null"]},"status":{"type":"string","const":"success"}},"required":["id","ip","userAgent","status"],"additionalProperties":false}}},"required":["events"],"additionalProperties":false},"example":{"events":[{"id":"string","ip":"string","userAgent":"string","status":"success"}]}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiAuthLogin-history","tags":["auth"],"parameters":[],"summary":"Anmeldungen der letzten 30 Tage","description":"Login-Historie der letzten 30 Tage. ACHTUNG fuer Integratoren: die Liste wird aus bestehenden Sitzungen abgeleitet — eine Sitzung entsteht erst NACH erfolgreicher Anmeldung. FEHLVERSUCHE STEHEN NICHT DRIN, `status` ist deshalb immer `success`. Wer ein vollstaendiges Anmeldeprotokoll braucht, kann diese Route dafuer nicht nehmen.","security":[]}},"/api/auth/change-password":{"post":{"responses":{"200":{"description":"Passwort geaendert. Alle uebrigen Sitzungen des Nutzers werden dabei beendet (`revokeOtherSessions`); `result` reicht die Antwort von Better-Auth durch.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"result":{"description":"Rueckgabe von Better-Auth, oder null — Form nicht zugesagt"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Validation failed"},"401":{"description":"Nicht authentifiziert oder current password falsch"},"503":{"description":"Auth-Service nicht verfuegbar"}},"operationId":"postApiAuthChange-password","tags":["auth"],"parameters":[],"summary":"Passwort aendern","description":"Aendert das Passwort des aktuellen Users (Fallback-Route fuer Better-Auth).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentPassword":{"type":"string","minLength":1},"newPassword":{"allOf":[{"type":"string","pattern":"[A-Z]","minLength":8,"maxLength":128},{"type":"string","pattern":"[0-9]"}]}},"required":["currentPassword","newPassword"]}}}},"security":[]}},"/api/auth/revoke-all-sessions":{"post":{"responses":{"200":{"description":"Alle Sessions beendet — `revoked` nennt die Anzahl","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"revoked":{"type":"integer"}},"required":["ok","revoked"],"additionalProperties":false},"example":{"ok":true,"revoked":0}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiAuthRevoke-all-sessions","tags":["auth"],"parameters":[],"summary":"Alle Sitzungen beenden — auch die eigene","description":"Beendet ALLE Sessions des Users (inkl. der aktuellen).","security":[]}},"/api/auth/me":{"get":{"responses":{"200":{"description":"User-Profil","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"name":{"type":["string","null"]},"image":{"type":["string","null"]},"tenantId":{"type":["string","null"]},"createdAt":{}},"required":["id","email","name","image","tenantId"],"additionalProperties":false},"example":{"id":"string","email":"string","name":"string","image":"string","tenantId":"string"}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Nutzer nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiAuthMe","tags":["auth"],"parameters":[],"summary":"Profil des angemeldeten Nutzers","description":"Gibt das Profil des aktuell eingeloggten Users zurück.","security":[]}},"/api/v1/auth/status":{"get":{"responses":{"200":{"description":"Status payload","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ready","demo-mode"]},"database":{"type":"boolean"},"auth":{"type":"boolean"},"message":{"type":"string"}},"required":["status","database","auth","message"],"additionalProperties":false},"example":{"status":"ready","database":true,"auth":true,"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AuthStatus","tags":["auth"],"parameters":[],"summary":"Status des Anmeldesystems","description":"Auth subsystem status — whether Better-Auth + DB are ready or running in demo mode."}},"/api/v1/auth/social-providers":{"get":{"responses":{"200":{"description":"Provider availability map","content":{"application/json":{"schema":{"type":"object","properties":{"google":{"type":"boolean"},"microsoft":{"type":"boolean"},"setupDocsUrl":{"type":"string"}},"required":["google","microsoft","setupDocsUrl"],"additionalProperties":false},"example":{"google":true,"microsoft":true,"setupDocsUrl":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AuthSocial-providers","tags":["auth"],"parameters":[],"summary":"Verfuegbare Anmeldeanbieter","description":"Returns which social OAuth providers are configured on this instance."}},"/api/v1/auth/check-slug":{"get":{"responses":{"200":{"description":"Availability result","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean"},"reason":{"type":"string","enum":["reserved","taken","error"]},"demo":{"type":"boolean"}},"required":["available"],"additionalProperties":false},"example":{"available":true,"reason":"reserved","demo":true}}}},"400":{"description":"Parameter `slug` fehlt oder verletzt das Format ^[a-z0-9-]{3,56}$"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Pruefung fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean"},"reason":{"type":"string","enum":["reserved","taken","error"]},"demo":{"type":"boolean"}},"required":["available"],"additionalProperties":false}}}}},"operationId":"getApiV1AuthCheck-slug","tags":["auth"],"parameters":[{"in":"query","name":"slug","schema":{"type":"string","pattern":"^[a-z0-9-]+$","minLength":3,"maxLength":56},"required":true}],"summary":"Mandantenkuerzel auf Verfuegbarkeit pruefen","description":"Check if a tenant slug is available for registration. Der Parameter `slug` ist PFLICHT — ohne ihn antwortet die Route mit 400. Der Vertragstest vom 06.08.2026 hat genau das gemeldet, weil die Pflicht in der Spezifikation nicht erkennbar war."}},"/api/v1/auth/register-tenant":{"post":{"responses":{"201":{"description":"Tenant created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"tenant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"layerId":{"type":["string","null"]}},"required":["id","slug","name","layerId"],"additionalProperties":false},"user":{"type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"emailVerificationRequired":{"type":"boolean","const":true}},"required":["email","name","emailVerificationRequired"],"additionalProperties":false},"nextStep":{"type":"string","const":"verify-email"},"signUpResult":{"type":["object","null"],"properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}},"required":["success","message","tenant","user","nextStep","signUpResult"],"additionalProperties":false},"example":{"success":true,"message":"string","tenant":{"id":"string","slug":"string","name":"string","layerId":"string"},"user":{"email":"string","name":"string","emailVerificationRequired":true},"nextStep":"verify-email","signUpResult":{"ok":true}}}}},"400":{"description":"Slug ist reserviert, oder der Rumpf verletzt das Pruefschema"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Self-service registration is closed on this deployment"},"409":{"description":"Slug bereits vergeben"},"500":{"description":"Anlage fehlgeschlagen — Schema und Mandantenzeile werden zurueckgerollt"},"503":{"description":"Auth unavailable"}},"operationId":"postApiV1AuthRegister-tenant","tags":["auth"],"parameters":[],"summary":"Mandant anlegen (oeffentlich)","description":"Register a new tenant with primary admin user. Sends a welcome email. Answers 403 REGISTRATION_CLOSED unless NEMIX_REGISTRATION_OPEN=true — see lib/registration-lock.ts.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantSlug":{"type":"string","pattern":"^[a-z0-9-]+$","minLength":3,"maxLength":56},"tenantName":{"type":"string","minLength":2,"maxLength":255},"industry":{"type":"string","enum":["bau","handel","dienstleistung","fertigung"]},"adminEmail":{"type":"string","format":"email"},"adminPassword":{"allOf":[{"type":"string","pattern":"[A-Z]","minLength":8,"maxLength":128},{"type":"string","pattern":"[0-9]"}]},"adminName":{"type":"string","minLength":2},"acceptTerms":{"type":"boolean","const":true},"planId":{"type":"string","format":"uuid"},"sitzland":{"type":"string","pattern":"^[A-Z]{2}$"},"profil":{"type":"object","properties":{"branchen":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":200,"default":[]},"groesse":{"type":"string","maxLength":32,"default":""},"laender":{"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"maxItems":200,"default":[]},"sprachen":{"type":"array","items":{"type":"string","minLength":2,"maxLength":8},"maxItems":200,"default":[]}}}},"required":["tenantSlug","tenantName","industry","adminEmail","adminPassword","adminName","acceptTerms","sitzland"]}}}}}},"/api/v1/auth/login":{"post":{"responses":{"200":{"description":"Session created"},"401":{"description":"Invalid credentials"}},"operationId":"postApiV1AuthLogin","tags":["auth"],"parameters":[],"summary":"Anmelden mit E-Mail und Passwort","description":"Sign in with email + password. Forwards to Better-Auth /sign-in/email.","security":[]}},"/api/v1/auth/register":{"post":{"responses":{"201":{"description":"User created"},"400":{"description":"Invalid body"},"403":{"description":"Self-service registration is closed on this deployment"},"409":{"description":"Email already in use"}},"operationId":"postApiV1AuthRegister","tags":["auth"],"parameters":[],"summary":"Registrieren — legt Nutzer und Mandant an","description":"Register a new user with email + password. Auto-creates a tenant so the user can write immediately. Answers 403 REGISTRATION_CLOSED unless NEMIX_REGISTRATION_OPEN=true — see lib/registration-lock.ts.","security":[]}},"/api/v1/auth/repair-orphaned-users":{"post":{"responses":{"200":{"description":"Repair result — eine Klartextzeile je bearbeitetem Nutzer","content":{"application/json":{"schema":{"type":"object","properties":{"repaired":{"type":"array","items":{"type":"string"},"description":"Je Nutzer eine Klartextzeile, auch bei Fehlern"},"count":{"type":"integer","description":"Laenge von `repaired`, nicht die Zahl der Erfolge"}},"required":["repaired","count"],"additionalProperties":false},"example":{"repaired":["string"],"count":0}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1AuthRepair-orphaned-users","tags":["auth"],"parameters":[],"summary":"Wartung: Nutzer ohne Mandant reparieren (nur dev)","description":"Create a tenant for every user with tenant_id=NULL or role=user. Dev-only, Bootstrap-Secret required."}},"/api/v1/auth/refresh":{"get":{"responses":{"200":{"description":"Session payload"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AuthRefresh","tags":["auth"],"parameters":[],"summary":"Aktuelle Sitzung lesen bzw. erneuern","description":"Refresh / read the current session. Forwards to Better-Auth /get-session."},"post":{"responses":{"200":{"description":"Session payload"}},"operationId":"postApiV1AuthRefresh","tags":["auth"],"parameters":[],"summary":"Aktuelle Sitzung lesen bzw. erneuern","description":"Refresh / read the current session. Forwards to Better-Auth /get-session.","security":[]}},"/api/v1/auth/fix-duplicate-users":{"post":{"responses":{"200":{"description":"Duplicate users fixed. `success: true` heisst „durchgelaufen\", nicht „alles gelungen\" — Fehler einzelner Schritte stehen als Zeile in `results`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"results":{"type":"array","items":{"type":"string"}}},"required":["success","results"],"additionalProperties":false},"example":{"success":true,"results":["string"]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1AuthFix-duplicate-users","tags":["auth"],"parameters":[],"summary":"Wartung: doppelte Nutzer zusammenfuehren (nur dev)","description":"Clean up duplicate user entries created by re-signups. Dev-only, X-Bootstrap-Secret required."}},"/api/v1/auth/fix-tenant-owner-roles":{"post":{"responses":{"200":{"description":"Fixed tenant owner roles — eine Klartextzeile je betroffenem Eigentuemer","content":{"application/json":{"schema":{"type":"object","properties":{"fixed":{"type":"array","items":{"type":"string"},"description":"Je betroffenem Eigentuemer eine Klartextzeile"},"count":{"type":"integer","description":"Laenge von `fixed`, nicht die Zahl der Erfolge"}},"required":["fixed","count"],"additionalProperties":false},"example":{"fixed":["string"],"count":0}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1AuthFix-tenant-owner-roles","tags":["auth"],"parameters":[],"summary":"Wartung: Mandanten-Eigentuemer auf admin heben (nur dev)","description":"Promote new-tenant owners with role=user to admin. Dev-only, X-Bootstrap-Secret required."}},"/api/v1/auth/debug-users":{"get":{"responses":{"200":{"description":"User state. Die Zeilen gehen roh hinaus (snake_case) und enthalten den ANFANG des gespeicherten Passwort-Hashes — deshalb ist der Endpunkt ausserhalb der Entwicklung zu.","content":{"application/json":{"schema":{"type":"object","properties":{"userState":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string"},"users":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"email_verified":{"type":"boolean"},"tenant_id":{"type":["string","null"]},"role":{"type":["string","null"]}},"required":["id","email","email_verified","tenant_id","role"],"additionalProperties":false}},"accounts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"provider_id":{"type":"string"},"account_id":{"type":"string"},"password":{"type":["string","null"],"description":"Erste 20 Zeichen des Hashes"},"hasPassword":{"type":"boolean"},"passwordPrefix":{"type":["string","null"],"description":"Erste 15 Zeichen plus \"...\""}},"required":["id","provider_id","account_id","password","hasPassword","passwordPrefix"],"additionalProperties":false}}},"required":["email","users","accounts"],"additionalProperties":false}}},"required":["userState"],"additionalProperties":false},"example":{"userState":[{"email":"string","users":[{"id":"string","email":"string","email_verified":true,"tenant_id":"string","role":"string"}],"accounts":[{"id":"string","provider_id":"string","account_id":"string","password":"string","hasPassword":true,"passwordPrefix":"string"}]}]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1AuthDebug-users","tags":["auth"],"parameters":[],"summary":"Diagnose: Zustand der Bootstrap-Nutzer (nur dev)","description":"Debug: show user + account state for bootstrap users. Dev-only, X-Bootstrap-Secret required."}},"/api/v1/auth/debug-tenants":{"get":{"responses":{"200":{"description":"Tenant list — die 50 zuletzt angelegten Mandanten mit dem Erstnutzer, dazu bis zu 20 Nutzer ohne Mandant. Die Zeilen gehen roh hinaus (snake_case).","content":{"application/json":{"schema":{"type":"object","properties":{"tenants":{"type":"array","items":{"type":"object","properties":{"tenant_id":{"type":"string"},"tenant_name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"},"owner_email":{"type":["string","null"]},"owner_role":{"type":["string","null"]},"owner_tenant_id":{"type":["string","null"]},"owner_email_verified":{"type":["boolean","null"]},"user_count":{"type":"string","description":"Anzahl als Zeichenkette — COUNT(*)::text"}},"required":["tenant_id","tenant_name","slug","status","created_at","owner_email","owner_role","owner_tenant_id","owner_email_verified","user_count"],"additionalProperties":false},"description":"Die 50 zuletzt angelegten Mandanten, neueste zuerst"},"orphanedUsers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":["string","null"]},"tenant_id":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","email","role","tenant_id","created_at"],"additionalProperties":false},"description":"Hoechstens 20 Nutzer ohne Mandant oder mit Rolle `user`"}},"required":["tenants","orphanedUsers"],"additionalProperties":false},"example":{"tenants":[{"tenant_id":"string","tenant_name":"string","slug":"string","status":"string","created_at":"string","owner_email":"string","owner_role":"string","owner_tenant_id":"string","owner_email_verified":true,"user_count":"string"}],"orphanedUsers":[{"id":"string","email":"string","role":"string","tenant_id":"string","created_at":"string"}]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1AuthDebug-tenants","tags":["auth"],"parameters":[],"summary":"Diagnose: alle Mandanten mit Rolle des Erstnutzers (nur dev)","description":"Debug: list all tenants and their first user role. Dev-only, Bootstrap-Secret required."}},"/api/v1/auth/bootstrap-users":{"post":{"responses":{"200":{"description":"Users created/seeded. `success: true` heisst „durchgelaufen\", nicht „alles gelungen\" — Fehler einzelner Schritte stehen als Zeile in `results`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"results":{"type":"array","items":{"type":"string"}}},"required":["success","results"],"additionalProperties":false},"example":{"success":true,"results":["string"]}}}},"401":{"description":"X-Bootstrap-Secret fehlt oder stimmt nicht"},"403":{"description":"Ausserhalb der Entwicklung abgeschaltet"},"500":{"description":"Kein Startpasswort: weder im Rumpf noch in DEV_BOOTSTRAP_PASSWORD"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1AuthBootstrap-users","tags":["auth"],"parameters":[],"summary":"Standardnutzer fuer einen frischen Mandanten anlegen","description":"Bootstrap default users for a fresh tenant (idempotent, demo seeding)."}},"/api/v1/auth/sessions":{"get":{"responses":{"200":{"description":"Liste aktiver Sessions","content":{"application/json":{"schema":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"device":{"type":"string"},"browser":{"type":"string"},"ipRegion":{"type":["string","null"]},"lastActiveAt":{},"current":{"type":"boolean"}},"required":["id","device","browser","ipRegion","current"],"additionalProperties":false}}},"required":["sessions"],"additionalProperties":false},"example":{"sessions":[{"id":"string","device":"string","browser":"string","ipRegion":"string","current":true}]}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht verfuegbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AuthSessions","tags":["auth"],"parameters":[],"summary":"Aktive Sitzungen auflisten","description":"Listet aktive Sessions des Users."},"delete":{"responses":{"200":{"description":"Sessions beendet — `revoked` nennt die Anzahl","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"revoked":{"type":"integer"}},"required":["ok","revoked"],"additionalProperties":false},"example":{"ok":true,"revoked":0}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1AuthSessions","tags":["auth"],"parameters":[],"summary":"Alle anderen Sitzungen beenden","description":"Beendet alle Sessions außer der aktuellen."}},"/api/v1/auth/sessions/{id}":{"delete":{"responses":{"200":{"description":"Session beendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","id"],"additionalProperties":false},"example":{"ok":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Session nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1AuthSessionsById","tags":["auth"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelne Sitzung beenden","description":"Beendet eine einzelne Session."}},"/api/v1/auth/login-history":{"get":{"responses":{"200":{"description":"Login-Events (nur erfolgreiche), maximal 200","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"at":{},"ip":{"type":["string","null"]},"userAgent":{"type":["string","null"]},"status":{"type":"string","const":"success"}},"required":["id","ip","userAgent","status"],"additionalProperties":false}}},"required":["events"],"additionalProperties":false},"example":{"events":[{"id":"string","ip":"string","userAgent":"string","status":"success"}]}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AuthLogin-history","tags":["auth"],"parameters":[],"summary":"Anmeldungen der letzten 30 Tage","description":"Login-Historie der letzten 30 Tage. ACHTUNG fuer Integratoren: die Liste wird aus bestehenden Sitzungen abgeleitet — eine Sitzung entsteht erst NACH erfolgreicher Anmeldung. FEHLVERSUCHE STEHEN NICHT DRIN, `status` ist deshalb immer `success`. Wer ein vollstaendiges Anmeldeprotokoll braucht, kann diese Route dafuer nicht nehmen."}},"/api/v1/auth/change-password":{"post":{"responses":{"200":{"description":"Passwort geaendert. Alle uebrigen Sitzungen des Nutzers werden dabei beendet (`revokeOtherSessions`); `result` reicht die Antwort von Better-Auth durch.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"result":{"description":"Rueckgabe von Better-Auth, oder null — Form nicht zugesagt"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Validation failed"},"401":{"description":"Nicht authentifiziert oder current password falsch"},"503":{"description":"Auth-Service nicht verfuegbar"}},"operationId":"postApiV1AuthChange-password","tags":["auth"],"parameters":[],"summary":"Passwort aendern","description":"Aendert das Passwort des aktuellen Users (Fallback-Route fuer Better-Auth).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentPassword":{"type":"string","minLength":1},"newPassword":{"allOf":[{"type":"string","pattern":"[A-Z]","minLength":8,"maxLength":128},{"type":"string","pattern":"[0-9]"}]}},"required":["currentPassword","newPassword"]}}}}}},"/api/v1/auth/revoke-all-sessions":{"post":{"responses":{"200":{"description":"Alle Sessions beendet — `revoked` nennt die Anzahl","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"revoked":{"type":"integer"}},"required":["ok","revoked"],"additionalProperties":false},"example":{"ok":true,"revoked":0}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AuthRevoke-all-sessions","tags":["auth"],"parameters":[],"summary":"Alle Sitzungen beenden — auch die eigene","description":"Beendet ALLE Sessions des Users (inkl. der aktuellen)."}},"/api/v1/auth/me":{"get":{"responses":{"200":{"description":"User-Profil","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"name":{"type":["string","null"]},"image":{"type":["string","null"]},"tenantId":{"type":["string","null"]},"createdAt":{}},"required":["id","email","name","image","tenantId"],"additionalProperties":false},"example":{"id":"string","email":"string","name":"string","image":"string","tenantId":"string"}}}},"401":{"description":"Nicht authentifiziert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Nutzer nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AuthMe","tags":["auth"],"parameters":[],"summary":"Profil des angemeldeten Nutzers","description":"Gibt das Profil des aktuell eingeloggten Users zurück."}},"/api/v1/webhooks/in/email-bounce":{"post":{"responses":{"200":{"description":"Angenommen. `customersFlagged` kann 0 sein, auch wenn die Verarbeitung scheiterte","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"bouncesProcessed":{"type":"integer","minimum":0,"description":"Anzahl der Empfaenger aus der Meldung; 0 wenn die Meldung keine enthielt"},"customersFlagged":{"type":"integer","minimum":0,"description":"Anzahl der ueber ALLE Mandanten markierten Kontakte; 0 auch dann, wenn nur die Datenbank fehlte"}},"required":["ok","bouncesProcessed","customersFlagged"]},"example":{"ok":true,"bouncesProcessed":0,"customersFlagged":0}}}},"400":{"description":"Rumpf kein gueltiges JSON oder nicht die erwartete Form","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}},"401":{"description":"Signatur fehlt, ist falsch oder es ist kein Geheimnis eingerichtet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}}},"operationId":"postApiV1WebhooksInEmail-bounce","tags":["webhooks"],"parameters":[],"description":"Nimmt Unzustellbarkeitsmeldungen von SES oder Mailgun entgegen. Der Rumpf wird ROH gegen eine HMAC-Signatur geprueft (Geheimnis aus `WEBHOOK_BOUNCE_SECRET`, ersatzweise `WEBHOOK_INBOUND_SECRET`); fehlendes, falsches oder gar nicht eingerichtetes Geheimnis ergeben dasselbe 401 OHNE jede Verarbeitung.\n\nDanach werden die betroffenen Adressen ueber ALLE aktiven Mandanten hinweg gesucht und dort markiert; bei einer dauerhaften Unzustellbarkeit entsteht zusaetzlich ein Posteingangs-Hinweis je betroffenem Mandanten. Diese Route ist also bewusst NICHT auf einen Mandanten begrenzt.\n\nSie antwortet auch dann mit 200 und `ok: true`, wenn die Datenbank fehlt oder die Mandantenliste nicht lesbar ist — `customersFlagged` bleibt dann 0. Das ist Absicht: ein 4xx oder 5xx wuerde den Absender endlos wiederholen lassen. Aus einer 200 laesst sich deshalb NICHT schliessen, dass etwas markiert wurde.","summary":"Nimmt Unzustellbarkeitsmeldungen von SES oder Mailgun entgegen","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/v1/webhooks/in/ses-inbound":{"post":{"responses":{"200":{"description":"Bestaetigt, ignoriert, verarbeitet ODER fehlgeschlagen — der Unterschied steht im Rumpf","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"confirmed":{"type":"boolean","const":true,"description":"Die SNS-Anmeldung wurde bestaetigt"}},"required":["ok","confirmed"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"ignored":{"type":"string","description":"Der unbekannte Nachrichtentyp, ersatzweise \"unknown\""}},"required":["ok","ignored"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"result":{"type":"object","properties":{"ok":{"type":"boolean","description":"Ob die Mail wirklich importiert wurde"},"reason":{"type":"string","description":"Grund im Klartext — auch bei ok=true belegt"},"tenantId":{"type":"string","description":"Zugeordneter Mandant, sofern einer gefunden wurde"},"imported":{"type":"integer","description":"Anzahl importierter Belege"}},"required":["ok","reason"],"description":"Ergebnis des Imports. ACHTUNG: `result.ok` kann false sein, obwohl der Aufruf 200 ist"}},"required":["ok","result"]},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","const":"processing_error","description":"Die Verarbeitung warf — bewusst mit 200, damit SNS nicht endlos wiederholt"}},"required":["ok","error"]}]},"example":{"ok":true,"confirmed":true}}}},"400":{"description":"SNS-Umschlag oder eingebettete Nachricht kein gueltiges JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}},"401":{"description":"Falsches Token oder abweichender TopicArn","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}},"503":{"description":"`not_configured` — SES_INBOUND_SECRET ist nicht gesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}}},"operationId":"postApiV1WebhooksInSes-inbound","tags":["webhooks"],"parameters":[],"summary":"SES-Eingangsmails per SNS entgegennehmen und zu Belegen machen","description":"Nimmt SNS-Meldungen der SES-Eingangsregel entgegen und macht aus den Mails Belege. Abgesichert ueber ein gemeinsames Geheimnis (`?token=` oder Kopfzeile `X-SES-Token`, ENV `SES_INBOUND_SECRET`) und wahlweise einen Abgleich des TopicArn — ist kein Geheimnis gesetzt, lehnt die Route mit 503 ab, ein falsches ergibt 401. Die X.509-Signatur von SNS wird NICHT geprueft.\n\nEine `SubscriptionConfirmation` wird selbsttaetig bestaetigt. Erkannt werden zwei Formate: der uebliche SNS-Umschlag und die Rohzustellung ohne Umschlag; ein unbekannter Typ wird mit 200 und `ignored` verworfen.\n\nANTWORTET FAST IMMER MIT 200 — auch wenn die Verarbeitung scheitert (`ok: false`, `error: processing_error`) oder die Mail nicht importiert wurde (`result.ok: false` mit Grund). Das ist Absicht, damit SNS nicht endlos wiederholt. Der Statuscode taugt hier also NICHT als Erfolgsnachweis; massgeblich ist `result.ok`.","security":[]}},"/api/v1/webhooks/in/whatsapp-in":{"post":{"responses":{"200":{"description":"Eintrag angelegt; `classification` null wenn die Einordnung scheiterte","content":{"application/json":{"schema":{"type":"object","properties":{"received":{"type":"boolean","const":true},"postfach_id":{"type":["string","null"],"description":"Kennung des angelegten Postfach-Eintrags; null wenn keine zurueckkam"},"classification":{"type":["object","null"],"properties":{"kategorie":{"type":"string","description":"Erkannte Kategorie der Nachricht"},"confidence":{"type":"number","description":"Sicherheit der Erkennung"}},"required":["kategorie","confidence"],"description":"Ergebnis der Einordnung; null wenn sie scheiterte — der Eintrag entsteht trotzdem"}},"required":["received","postfach_id","classification"]},"example":{"received":true,"postfach_id":"string","classification":{"kategorie":"string","confidence":0}}}}},"400":{"description":"`invalid_payload` oder `tenant_unresolved`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}},"401":{"description":"Signatur fehlt oder ist falsch","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}},"503":{"description":"`database_unavailable`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}}},"operationId":"postApiV1WebhooksInWhatsapp-in","tags":["webhooks"],"parameters":[],"summary":"Eingehende WhatsApp-Nachricht ins Immobilien-Postfach legen","description":"Nimmt eingehende WhatsApp-Nachrichten im Twilio-Format entgegen und legt sie als Eintrag im Immobilien-Postfach ab (Kanal `whatsapp`, Status `neu`). Der Rumpf wird ROH gegen eine HMAC-Signatur geprueft (Geheimnis aus `WEBHOOK_WHATSAPP_IN_SECRET`, ersatzweise `WEBHOOK_INBOUND_SECRET`).\n\nDer Mandant kommt NICHT aus einer Sitzung, sondern aus der Kopfzeile `X-Nemix-Tenant-Id` oder dem Abfrageparameter `tenantSlug`; fehlt beides, kommt 400 mit `tenant_unresolved`. Das Twilio-Praefix `whatsapp:` wird vom Absender entfernt, ein mitgeschicktes `MediaUrl0` als Anhang vermerkt (die Datei selbst wird nicht geholt).\n\nDie KI-Einordnung ist nachrangig: scheitert sie, entsteht der Eintrag trotzdem und `classification` ist null.\n\nDerselbe Handler haengt an ZWEI Pfaden: `/api/v1/webhooks/in/whatsapp-in` und `/api/v1/webhooks/whatsapp-in`.","security":[]}},"/api/v1/webhooks/in/{slug}":{"post":{"responses":{"202":{"description":"Angenommen und an den Verteiler gegeben — auch wenn dieser scheiterte","content":{"application/json":{"schema":{"type":"object","properties":{"accepted":{"type":"boolean","const":true,"description":"Immer true — die Annahme sagt nichts ueber die Verarbeitung"},"slug":{"type":"string","description":"Der angesprochene Eingang"},"eventType":{"type":"string","description":"Ereignisart, die der Verteiler daraus gebildet hat"},"receivedAt":{"type":"string","description":"Zeitpunkt des Eingangs"}},"required":["accepted","slug","eventType","receivedAt"]},"example":{"accepted":true,"slug":"string","eventType":"string","receivedAt":"string"}}}},"401":{"description":"Signatur, Einmalwert, Zeitstempel oder Mandant nicht in Ordnung — ununterscheidbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}},"404":{"description":"Slug entspricht nicht dem erlaubten Muster","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}}},"operationId":"postApiV1WebhooksInBySlug","tags":["webhooks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"description":"Allgemeiner Eingang fuer fremde Systeme. Der Rumpf wird ROH gelesen und gegen eine HMAC-Signatur geprueft: noetig sind `X-Nemix-Tenant-Id` sowie die Kopfzeilen fuer Signatur, Einmalwert und Zeitstempel. FEHLT eines davon, ist der Mandant unbekannt, hat er kein Geheimnis oder stimmt die Signatur nicht, kommt IMMER dasselbe 401 — absichtlich ununterscheidbar, damit sich keine gueltigen Mandanten-Kennungen abfragen lassen. Ein Einmalwert wird nur einmal angenommen (Wiedereinspielschutz). Ein Slug, der nicht dem Kleinbuchstaben-Bindestrich-Muster entspricht, ergibt 404.\n\nNach bestandener Pruefung wird das Ereignis an den Verteiler gegeben und mit 202 quittiert — NICHT mit 200. Scheitert der Verteiler, bleibt es trotzdem bei 202 und `accepted: true`; der Fehler steht nur im Serverprotokoll. Eine erfolgreiche Antwort belegt also die Annahme, nicht die Verarbeitung.","summary":"Allgemeiner Eingang fuer fremde Systeme","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/v1/webhooks/whatsapp-in":{"post":{"responses":{"200":{"description":"Eintrag angelegt; `classification` null wenn die Einordnung scheiterte","content":{"application/json":{"schema":{"type":"object","properties":{"received":{"type":"boolean","const":true},"postfach_id":{"type":["string","null"],"description":"Kennung des angelegten Postfach-Eintrags; null wenn keine zurueckkam"},"classification":{"type":["object","null"],"properties":{"kategorie":{"type":"string","description":"Erkannte Kategorie der Nachricht"},"confidence":{"type":"number","description":"Sicherheit der Erkennung"}},"required":["kategorie","confidence"],"description":"Ergebnis der Einordnung; null wenn sie scheiterte — der Eintrag entsteht trotzdem"}},"required":["received","postfach_id","classification"]},"example":{"received":true,"postfach_id":"string","classification":{"kategorie":"string","confidence":0}}}}},"400":{"description":"`invalid_payload` oder `tenant_unresolved`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}},"401":{"description":"Signatur fehlt oder ist falsch","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}},"503":{"description":"`database_unavailable`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"reason":{"type":"string","description":"Genauerer Grund, sofern einer genannt wird"},"hint":{"type":"string","description":"Hinweis, was fehlt"}},"required":["error"]}}}}},"operationId":"postApiV1WebhooksWhatsapp-in","tags":["webhooks"],"parameters":[],"summary":"Eingehende WhatsApp-Nachricht ins Immobilien-Postfach legen","description":"Nimmt eingehende WhatsApp-Nachrichten im Twilio-Format entgegen und legt sie als Eintrag im Immobilien-Postfach ab (Kanal `whatsapp`, Status `neu`). Der Rumpf wird ROH gegen eine HMAC-Signatur geprueft (Geheimnis aus `WEBHOOK_WHATSAPP_IN_SECRET`, ersatzweise `WEBHOOK_INBOUND_SECRET`).\n\nDer Mandant kommt NICHT aus einer Sitzung, sondern aus der Kopfzeile `X-Nemix-Tenant-Id` oder dem Abfrageparameter `tenantSlug`; fehlt beides, kommt 400 mit `tenant_unresolved`. Das Twilio-Praefix `whatsapp:` wird vom Absender entfernt, ein mitgeschicktes `MediaUrl0` als Anhang vermerkt (die Datei selbst wird nicht geholt).\n\nDie KI-Einordnung ist nachrangig: scheitert sie, entsteht der Eintrag trotzdem und `classification` ist null.\n\nDerselbe Handler haengt an ZWEI Pfaden: `/api/v1/webhooks/in/whatsapp-in` und `/api/v1/webhooks/whatsapp-in`.","security":[]}},"/api/v1/webhooks/voice/callback":{"post":{"responses":{"200":{"description":"Angenommen. Absichtlich AUCH dann, wenn nichts geschrieben wurde — der Anbieter soll nicht endlos wiederholen. Der Rumpf sagt, was passiert ist: `{ ok: true, updated: true }` bei einem Treffer, sonst `{ ok: true, ignored: \"no_matching_call\" | \"no_call_id\" | \"invalid_json\" }`. Auch ein Datenbankfehler beim Nachtragen endet hier als `no_matching_call` — die Schreibfunktion fängt ihn selbst ab und meldet nur „nicht aktualisiert\".","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Im 200-Fall immer true — die Annahme, nicht der Erfolg"},"updated":{"type":"boolean","const":true,"description":"Nur beim Treffer: der Eintrag in public.voice_call_logs wurde nachgetragen"},"ignored":{"type":"string","enum":["invalid_json","no_call_id","no_matching_call"],"description":"Nur wenn NICHTS geschrieben wurde. `invalid_json`: der Rumpf war nicht lesbar. `no_call_id`: keine anbieter-eindeutige Call-ID im Rumpf. `no_matching_call`: kein passender Eintrag gefunden — hierunter faellt auch ein Datenbankfehler beim Nachtragen, weil die Schreibfunktion ihn selbst abfaengt und nur „nicht aktualisiert\" meldet."}},"required":["ok"]},"example":{"ok":true,"updated":true,"ignored":"invalid_json"}}}},"401":{"description":"Token fehlt, hat eine andere Länge oder stimmt nicht überein. Rumpf `{ ok: false, error: \"unauthorized\" }`."},"503":{"description":"`VOICE_CALLBACK_SECRET` ist auf diesem Host nicht gesetzt. Die Route nimmt dann gar nichts an: `{ ok: false, error: \"not_configured\" }`."}},"operationId":"postApiV1WebhooksVoiceCallback","tags":["voice","webhooks"],"parameters":[],"summary":"Rücklauf des Sprach-Anbieters nach einem KI-Anruf","description":"Nimmt den Abschlussbericht eines Anrufs entgegen (ElevenLabs post-call oder Twilio status-callback) und trägt Status, Dauer und Kurzergebnis in den passenden Eintrag in `public.voice_call_logs` nach. Zugeordnet wird über die anbieter-eindeutige Call-ID (`conversation_id`, `external_call_id`, `CallSid` oder `call_sid`).\n\nWIE DER AUFRUFER GEPRÜFT WIRD: gemeinsames Geheimnis, KEINE HMAC-Signatur. Der Aufrufer schickt es als `?token=` oder im Kopf `X-Voice-Token`; verglichen wird gegen `VOICE_CALLBACK_SECRET` mit `timingSafeEqual` nach vorheriger Längenprüfung. Damit hängt die Echtheit allein am Besitz des Geheimnisses — der Rumpf ist NICHT signiert und wird nicht auf Unversehrtheit geprüft. Passt das Token nicht, endet der Aufruf mit 401, bevor der Rumpf überhaupt gelesen wird. Ist die Variable gar nicht gesetzt, antwortet die Route mit 503 statt offen zu stehen. Der Pfad ist in `PUBLIC_PATHS` freigegeben (POST /api/v1/webhooks/voice/*), läuft also ohne Nemix-Sitzung.\n\nWORTLAUT WIRD VERWORFEN: ein mitgeschicktes Transkript, Nachrichtenverläufe sowie Aufzeichnungs- und Ton-Adressen werden vor dem Schreiben rekursiv aus dem Ergebnis entfernt (siehe `stripWortlaut` und den Abschnitt „Wortlaut\" im Dateikopf). Das Ergebnis bleibt ein Vorschlag; es wird nichts automatisch in Notizen oder Leads geschrieben.","security":[]}},"/api/v1/email-tracking/pixel/{tenantId}/{eventId}":{"get":{"responses":{"200":{"description":"1x1 GIF (transparent)","content":{"image/gif":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Email-trackingPixelByTenantIdByEventId","tags":["email-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true},{"schema":{"type":"string"},"in":"path","name":"eventId","required":true}],"summary":"Zaehlpixel: liefert ein 1x1-GIF und protokolliert die Mail-Oeffnung","description":"Liefert ein transparentes 1x1-GIF und schreibt dabei als Nebenwirkung eine Zeile nach `public.email_tracking_events` — Mandant und Ereigniskennung aus dem Pfad, `doc_type` und `doc_id` aus der Query, dazu User-Agent und aufgeloeste IP. Der Pfad ist bewusst oeffentlich, weil im Postfach des Empfaengers keine Sitzung existiert; es wird nicht entdoppelt, jeder Abruf ist eine eigene Zeile. Die Antwort ist immer 200 mit dem Bild — auch bei unbrauchbaren Kennungen (dann entfaellt nur der Eintrag) oder bei einem Datenbankfehler. `Cache-Control: no-store` haelt Bildzwischenspeicher davon ab, weitere Oeffnungen zu verschlucken."}},"/api/v1/vat/validate":{"get":{"responses":{"200":{"description":"Ergebnis der Pruefung. Auch der Nicht-Erfolg kommt hier an: `status` unterscheidet bestaetigt, verneint und nicht pruefbar.","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"status":{"type":"string","enum":["valid","invalid","unavailable"]},"countryCode":{"type":"string"},"vatNumber":{"type":"string"},"requestDate":{"type":"string"},"companyName":{"type":["string","null"]},"companyAddress":{"type":["string","null"]},"companyNameMatch":{"type":"boolean"},"companyAddressMatch":{"type":"boolean"},"requestIdentifier":{"type":["string","null"]},"error":{"type":"string"}},"required":["valid","status","countryCode","vatNumber","requestDate","companyName","companyAddress","requestIdentifier"],"additionalProperties":false},"example":{"valid":true,"status":"valid","countryCode":"string","vatNumber":"string","requestDate":"string","companyName":"string","companyAddress":"string","companyNameMatch":true,"companyAddressMatch":true,"requestIdentifier":"string","error":"string"}}}},"400":{"description":"`vatId` fehlt, ist kuerzer als 8 oder laenger als 15 Zeichen, oder beginnt nicht mit zwei Buchstaben."}},"operationId":"getApiV1VatValidate","tags":["VAT Validation"],"parameters":[{"in":"query","name":"vatId","schema":{"type":"string","minLength":8,"maxLength":15},"required":true},{"in":"query","name":"companyName","schema":{"type":"string","maxLength":200},"required":false},{"in":"query","name":"companyAddress","schema":{"type":"string","maxLength":300},"required":false},{"in":"query","name":"requesterVatId","schema":{"type":"string","maxLength":15},"required":false},{"in":"query","name":"companyStreet","schema":{"type":"string","maxLength":200},"required":false},{"in":"query","name":"companyPostcode","schema":{"type":"string","maxLength":20},"required":false},{"in":"query","name":"companyCity","schema":{"type":"string","maxLength":100},"required":false}],"summary":"Validate an EU VAT-ID online via the official VIES service (qualified when requesterVatId is supplied)","description":"Prueft eine EU-Umsatzsteuer-Identifikationsnummer online beim\noffiziellen VIES-Dienst der Kommission. Wird `requesterVatId`\nmitgegeben, wird daraus eine QUALIFIZIERTE Bestaetigungsanfrage nach\n§ 18e UStG, und die Antwort traegt die Konsultationsnummer\n(`requestIdentifier`).\n\nES GIBT AUSSER DEM 400 KEINEN FEHLERCODE. Zeitgrenze, Netzfehler,\nHTTP-Fehler der Gegenstelle, nicht erreichbarer Mitgliedstaat, ein\nLaendercode ausserhalb der EU: alles kommt als 200. Der Zustand steht\nim Rumpf, nicht im Statuscode.\n\n`status` HAT DREI WERTE, UND `valid` HAT NUR ZWEI.\n\n· `valid`       — VIES bestaetigt die Nummer. `valid: true`.\n· `invalid`     — VIES verneint sie. `valid: false`.\n· `unavailable` — NICHT PRUEFBAR. `valid` steht ebenfalls auf `false`.\n\nDer dritte Fall ist der gefaehrliche. Wer nur `valid` liest, zeigt eine\nvoellig gueltige Nummer als ungueltig an, sobald der Pruefdienst eines\nMitgliedstaats gerade klemmt. Genau dieser Fehler steckte frueher in\nder SOAP-Fassung dieser Route und ist der Grund fuer das dritte Wort.\nMassgeblich ist `status`, nicht `valid`.\n\nABGLEICH GEGEN DEN EIGENEN DATENSATZ.\nWer `companyName` oder `companyAddress` mitschickt, bekommt\n`companyNameMatch` beziehungsweise `companyAddressMatch` zurueck. Beide\nFelder FEHLEN, wenn eine der beiden Seiten nichts geliefert hat — dann\nist nichts vergleichbar, und die Oberflaeche darf keine rote Abweichung\nzeigen. Ein fehlendes Feld ist also keine Abweichung, sondern das\nAusbleiben einer Aussage. Der Vergleich selbst ist absichtlich\ngrosszuegig: Gross- und Kleinschreibung, Satzzeichen und Akzente\nspielen keine Rolle, und Teilzeichenketten zaehlen als Treffer.\n\nDie Route ist OEFFENTLICH und braucht keine Anmeldung, damit die\nPruefen-Knoepfe in Kontakt- und Belegmasken ohne Sitzung arbeiten. Die\nPlatform verlaesst dabei die getippte Nummer und, bei qualifizierter\nAbfrage, Name und Anschrift des geprueften Unternehmens. Das\nrevisionssichere Protokoll schreibt eine EIGENE, angemeldete Route —\ndiese hier speichert nichts.","security":[]}},"/api/v1/geo/postal":{"get":{"responses":{"200":{"description":"Treffer, entdoppelt. Leer, wenn OpenPLZ nichts kennt.","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"object","properties":{"plz":{"type":"string"},"city":{"type":"string"}},"additionalProperties":false},"results":{"type":"array","items":{"type":"object","properties":{"postalCode":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"}},"required":["postalCode","city","state"],"additionalProperties":false}}},"required":["query","results"],"additionalProperties":false},"example":{"query":{"plz":"string","city":"string"},"results":[{"postalCode":"string","city":"string","state":"string"}]}}}},"400":{"description":"Weder `plz` noch `city` gesetzt, oder `plz` sind nicht 3 bis 5 Ziffern."},"503":{"description":"OpenPLZ nicht erreichbar oder Zeitgrenze von 4 s ueberschritten. Der Rumpf hat dieselbe Form wie der Erfolg, `results` ist leer.","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"object","properties":{"plz":{"type":"string"},"city":{"type":"string"}},"additionalProperties":false},"results":{"type":"array","items":{"type":"object","properties":{"postalCode":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"}},"required":["postalCode","city","state"],"additionalProperties":false}},"error":{"type":"string"}},"required":["query","results","error"],"additionalProperties":false}}}}},"operationId":"getApiV1GeoPostal","tags":["Geo"],"parameters":[{"in":"query","name":"plz","schema":{"type":"string","pattern":"^\\d{3,5}$"}},{"in":"query","name":"city","schema":{"type":"string","minLength":2,"maxLength":100}}],"summary":"Look up German localities by postal code or city name (OpenPLZ)","description":"Schlaegt deutsche Orte nach Postleitzahl ODER Ortsnamen nach. Genau\neiner der beiden Parameter muss gesetzt sein; fehlen beide, kommt ein\n400.\n\nDie Route ist OEFFENTLICH und braucht weder Anmeldung noch\nMandantenkontext. Sie reicht die Anfrage an den freien Dienst OpenPLZ\nweiter (openplzapi.org). Die Platform verlaesst dabei nur, was der\nNutzer selbst getippt hat: Postleitzahl oder Ortsname. Keine\nPersonendaten.\n\nDER FEHLERFALL SIEHT AUS WIE EIN ERFOLG.\nIst OpenPLZ nicht erreichbar oder antwortet nicht binnen vier Sekunden,\nkommt ein 503 — aber MIT Rumpf, in derselben Form wie der Erfolg, mit\nleerem `results` und einem zusaetzlichen `error`. Das ist Absicht: die\nEingabemaske behandelt eine leere Liste als „kein Vorschlag\" und laesst\nden Nutzer von Hand tippen.\n\nFolge fuer Aufrufer: an `results.length === 0` allein ist NICHT zu\nerkennen, ob es keinen Treffer gibt oder der Dienst ausgefallen ist.\nDas entscheidet der Statuscode, nicht der Rumpf.\n\nDie Trefferliste ist auf 50 Datensaetze der Gegenstelle begrenzt und\nnach Postleitzahl plus Ort entdoppelt.","security":[]}},"/api/v1/mcp/oauth/metadata":{"get":{"responses":{"200":{"description":"Metadaten","content":{"application/json":{"schema":{"type":"object","properties":{"issuer":{"type":"string"},"authorization_endpoint":{"type":"string"},"token_endpoint":{"type":"string"},"registration_endpoint":{"type":"string"},"revocation_endpoint":{"type":"string"},"response_types_supported":{"type":"array","items":{"type":"string"}},"grant_types_supported":{"type":"array","items":{"type":"string"}},"code_challenge_methods_supported":{"type":"array","items":{"type":"string"}},"token_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string"}},"scopes_supported":{"type":"array","items":{"type":"string"}}},"required":["issuer","authorization_endpoint","token_endpoint","registration_endpoint","revocation_endpoint","response_types_supported","grant_types_supported","code_challenge_methods_supported","token_endpoint_auth_methods_supported","scopes_supported"],"additionalProperties":true},"example":{"issuer":"string","authorization_endpoint":"string","token_endpoint":"string","registration_endpoint":"string","revocation_endpoint":"string","response_types_supported":["string"],"grant_types_supported":["string"],"code_challenge_methods_supported":["string"],"token_endpoint_auth_methods_supported":["string"],"scopes_supported":["string"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1McpOauthMetadata","tags":["mcp"],"parameters":[],"summary":"OAuth-Autorisierungsserver-Metadaten (RFC 8414)","description":"Beschreibt den Autorisierungsserver fuer MCP-Clients. Oeffentlich unter `/.well-known/oauth-authorization-server` der Web-Adresse (Rewrite hierher). Aussteller ist die oeffentliche Web-Adresse, alle Endpunkte liegen unter /api/v1/mcp/oauth. Nur `authorization_code` mit PKCE S256 und `refresh_token`."}},"/api/v1/mcp/oauth/geschuetzte-ressource":{"get":{"responses":{"200":{"description":"Metadaten","content":{"application/json":{"schema":{"type":"object","properties":{"resource":{"type":"string"},"authorization_servers":{"type":"array","items":{"type":"string"}},"scopes_supported":{"type":"array","items":{"type":"string"}},"bearer_methods_supported":{"type":"array","items":{"type":"string"}},"resource_name":{"type":"string"}},"required":["resource","authorization_servers","scopes_supported","bearer_methods_supported","resource_name"]},"example":{"resource":"string","authorization_servers":["string"],"scopes_supported":["string"],"bearer_methods_supported":["string"],"resource_name":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1McpOauthGeschuetzte-ressource","tags":["mcp"],"parameters":[],"summary":"Metadaten der geschuetzten MCP-Ressource (RFC 9728)","description":"Sagt einem MCP-Client, welcher Autorisierungsserver fuer /api/v1/mcp zustaendig ist. Oeffentlich unter `/.well-known/oauth-protected-resource/api/v1/mcp` der Web-Adresse; jede 401-Antwort des MCP-Endpunkts nennt diese Adresse im Kopf `WWW-Authenticate`."}},"/api/v1/mcp/oauth/register":{"post":{"responses":{"201":{"description":"Registriert","content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string"},"client_secret":{"type":"string"},"client_id_issued_at":{"type":"integer"},"client_secret_expires_at":{"type":"integer"},"client_name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string"}},"grant_types":{"type":"array","items":{"type":"string"}},"response_types":{"type":"array","items":{"type":"string"}},"token_endpoint_auth_method":{"type":"string"}},"required":["client_id","client_id_issued_at","client_name","redirect_uris","grant_types","response_types","token_endpoint_auth_method"]},"example":{"client_id":"string","client_secret":"string","client_id_issued_at":0,"client_secret_expires_at":0,"client_name":"string","redirect_uris":["string"],"grant_types":["string"],"response_types":["string"],"token_endpoint_auth_method":"string"}}}},"400":{"description":"`invalid_redirect_uri` oder `invalid_client_metadata`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1McpOauthRegister","tags":["mcp"],"parameters":[],"summary":"Client registrieren (RFC 7591, offen)","description":"Ein MCP-Client meldet sich an und bekommt eine `client_id`, als vertraulicher Client (`client_secret_post` oder `client_secret_basic`, Vorgabe laut RFC) dazu ein Geheimnis, das NUR in dieser Antwort steht. Weiterleitungen: `https`, oder `http` auf localhost; hoechstens zehn. Unbekannte Felder werden uebergangen. Die Registrierung gewaehrt keinen Zugriff — den gibt erst die Zustimmung eines angemeldeten Nutzers. Clients, die nie ein Token holen, werden nach 30 Tagen entfernt. Je Adresse gedrosselt."}},"/api/v1/mcp/oauth/authorize":{"get":{"responses":{"302":{"description":"Weiter zum Login, zur Zustimmung oder mit Fehler zurueck zum Client"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1McpOauthAuthorize","tags":["mcp"],"parameters":[],"summary":"Autorisierung beginnen (Browser)","description":"Wird im Browser des Nutzers aufgerufen. Prueft Client, Weiterleitung, PKCE (S256 Pflicht), Umfaenge und `resource`. Ist die Weiterleitung unbekannt, geht es NIE dorthin, sondern zur Fehleranzeige der Zustimmungsseite. Ohne Sitzung: 302 zum Login, der mit `from` hierher zurueckkehrt. Mit Sitzung: 302 zur Zustimmungsseite /mcp/freigabe — ein Code entsteht erst nach ausdruecklicher Zustimmung, nie still."}},"/api/v1/mcp/oauth/token":{"post":{"responses":{"200":{"description":"Tokens","content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string","const":"Bearer"},"expires_in":{"type":"integer"},"refresh_token":{"type":"string"},"scope":{"type":"string"}},"required":["access_token","token_type","expires_in","refresh_token","scope"]},"example":{"access_token":"string","token_type":"Bearer","expires_in":0,"refresh_token":"string","scope":"string"}}}},"400":{"description":"`invalid_request`, `invalid_grant`, `unsupported_grant_type`, `invalid_scope`, `invalid_target`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}},"required":["error"]}}}},"401":{"description":"`invalid_client`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1McpOauthToken","tags":["mcp"],"parameters":[],"summary":"Code oder Erneuerungstoken gegen Tokens tauschen","description":"`grant_type=authorization_code`: Code (einmal einloesbar, 60 s gueltig), dieselbe `redirect_uri` und der PKCE-`code_verifier`. Ein zweites Einloesen desselben Codes widerruft alle daraus ausgestellten Tokens. `grant_type=refresh_token`: rotiert — der alte Erneuerungstoken ist danach verbraucht; wird er trotzdem wieder vorgelegt, faellt die ganze Kette. Bei beiden wird geprueft, ob der Nutzer den Mandanten noch betreten darf. Zugriffstoken `mcp_oa_…` gilt 1 h, Erneuerungstoken 30 Tage. Rumpf als Formular, auch JSON."}},"/api/v1/mcp/oauth/revoke":{"post":{"responses":{"200":{"description":"Widerrufen oder unbekannt — der Rumpf ist leer, der Client wertet laut RFC nur den Status aus","content":{"application/json":{"schema":{"type":"object","additionalProperties":false}}}},"401":{"description":"`invalid_client`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1McpOauthRevoke","tags":["mcp"],"parameters":[],"summary":"Token widerrufen (RFC 7009)","description":"Widerruft die ganze Kette, zu der der Token gehoert — Zugriffs- wie Erneuerungstoken. Nur Tokens des anfragenden Clients. Antwortet laut RFC immer 200, auch fuer unbekannte Tokens, damit niemand damit Tokens erraten kann."}},"/api/v1/mcp":{"post":{"responses":{"200":{"description":"JSON-RPC response (incl. error envelopes — never HTTP 401)","content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"result":{},"error":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{}},"required":["code","message"]}},"required":["jsonrpc","id"]},"example":{"jsonrpc":"2.0","id":"string","error":{"code":0,"message":"string"}}}}},"202":{"description":"Benachrichtigung quittiert — kein Rumpf"},"400":{"description":"Body parse failure","content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"result":{},"error":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{}},"required":["code","message"]}},"required":["jsonrpc","id"]}}}}},"operationId":"postApiV1Mcp","tags":["mcp"],"parameters":[],"summary":"JSON-RPC endpoint per Model-Context-Protocol-Spec","description":"Auth via Authorization: Bearer mcp_<token> — Better-Auth-Sitzungen und `nemix_`-API-Keys gelten hier NICHT. Der HTTP-Status sagt nichts ueber den Erfolg: auch eine abgelehnte Anmeldung und jeder Werkzeugfehler kommen als 200 mit einem JSON-RPC-Fehlerumschlag; der Code steht in `error.code`. Eine Benachrichtigung (Methode `notifications/…`) wird mit 202 und LEEREM Rumpf quittiert — ein Fehlerumschlag darauf bricht den Handshake echter Clients. Welche Werkzeuge ein Token sehen und aufrufen darf, entscheiden dessen Umfaenge, die Rolle des Nutzers und der Tarif des Mandanten; GET /umfang nennt sie. Jeder Aufruf wird protokolliert, ein Fehler dabei bricht ihn nie ab.","security":[]},"get":{"responses":{"200":{"description":"Server-Probe","content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string","const":"2.0"},"transport":{"type":"string","const":"http"},"endpoint":{"type":"string"},"docs":{"type":"string"}},"required":["jsonrpc","transport","endpoint","docs"]},"example":{"jsonrpc":"2.0","transport":"http","endpoint":"string","docs":"string"}}}},"405":{"description":"Kein SSE-Strom an diesem Endpunkt (Streamable-HTTP-Transport)"}},"operationId":"getApiV1Mcp","tags":["mcp"],"parameters":[],"summary":"Probe endpoint","description":"Zwei Aufrufer, zwei Antworten. Wer `Accept: text/event-stream` schickt, bekommt 405 mit `Allow: POST` — dieser Endpunkt bietet KEINEN vom Server ausgehenden Ereignisstrom an; ein 200 wuerde ein Client als sofort endenden Strom lesen und im Sekundentakt neu verbinden. Alle anderen bekommen eine feste Sonde mit Transportart, dem eigentlichen Endpunkt (POST) und dem Pfad zur Dokumentation. Die Sonde braucht KEIN Token, liest nichts und verraet nichts ueber den Mandanten — sie beweist nur, dass die Adresse stimmt.","security":[]}},"/api/v1/mcp/umfang":{"get":{"responses":{"200":{"description":"Umfang und Werkzeuge dieses Zugangs. `bestand` zaehlt den Gesamtbestand je Stufe und haengt NICHT am Token — der Vergleich mit `verfuegbar.anzahl` zeigt, was ein groesserer Umfang braechte.","content":{"application/json":{"schema":{"type":"object","properties":{"umfaenge":{"type":"array","items":{"type":"string","enum":["mcp:read","mcp:write","mcp:build","mcp:admin"]}},"alleUmfaenge":{"type":"array","items":{"type":"string","enum":["mcp:read","mcp:write","mcp:build","mcp:admin"]}},"rolle":{"type":"string","enum":["user","manager","admin"]},"tarif":{"type":"string","enum":["starter","professional","enterprise"]},"verfuegbar":{"type":"object","properties":{"anzahl":{"type":"integer"},"werkzeuge":{"type":"array","items":{"type":"string"}}},"required":["anzahl","werkzeuge"]},"bestand":{"type":"object","properties":{"mcp:read":{"type":"integer"},"mcp:write":{"type":"integer"},"mcp:build":{"type":"integer"},"mcp:admin":{"type":"integer"}},"required":["mcp:read","mcp:write","mcp:build","mcp:admin"]},"verborgen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"grund":{"type":"string","enum":["stufe","quarantaene","umfang","tarif","rolle"]},"hinweis":{"type":"string"}},"required":["id","grund","hinweis"]}}},"required":["umfaenge","alleUmfaenge","rolle","tarif","verfuegbar","bestand","verborgen"]},"example":{"umfaenge":["mcp:read"],"alleUmfaenge":["mcp:read"],"rolle":"user","tarif":"starter","verfuegbar":{"anzahl":0,"werkzeuge":["string"]},"bestand":{"mcp:read":0,"mcp:write":0,"mcp:build":0,"mcp:admin":0},"verborgen":[{"id":"string","grund":"stufe","hinweis":"string"}]}}}},"401":{"description":"Kein oder ungueltiger Token"},"503":{"description":"Zugang derzeit nicht pruefbar (Datenbank)"}},"operationId":"getApiV1McpUmfang","tags":["mcp"],"parameters":[],"description":"Was darf dieser MCP-Zugang? Liefert die Umfaenge des Tokens, die damit verfuegbaren Werkzeuge, den Gesamtbestand je Stufe und die verborgenen Werkzeuge samt Grund. Auth via Authorization: Bearer mcp_<token>.","summary":"Was darf dieser MCP-Zugang","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/auth-debug/debug":{"get":{"responses":{"200":{"description":"Diagnose erstellt. Enthaelt bei bestehender Sitzung die E-Mail-Adresse des Aufrufers — in `sessionResolved`, als Text `user=<E-Mail>`. Ohne Sitzung steht dort `no session in cookie`, bei fehlender Auth-Instanz `auth instance unavailable (DB?)`, bei einem Fehler `error: <Meldung>`. Nur der Ausgangswert `false` ist ein boolean.","content":{"application/json":{"schema":{"type":"object","properties":{"host":{"type":["string","null"]},"origin":{"type":["string","null"]},"forwardedHost":{"type":["string","null"]},"forwardedProto":{"type":["string","null"]},"cookieNames":{"type":"array","items":{"type":"string"}},"cookieCount":{"type":"integer"},"hasBetterAuthCookie":{"type":"boolean"},"sessionResolved":{"anyOf":[{"type":"boolean"},{"type":"string"}]},"userAgent":{"type":["string","null"]},"timestamp":{"type":"string"}},"required":["host","origin","forwardedHost","forwardedProto","cookieNames","cookieCount","hasBetterAuthCookie","sessionResolved","userAgent","timestamp"]},"example":{"host":"string","origin":"string","forwardedHost":"string","forwardedProto":"string","cookieNames":["string"],"cookieCount":0,"hasBetterAuthCookie":true,"sessionResolved":true,"userAgent":"string","timestamp":"string"}}}},"404":{"description":"Nur in Produktion und nur, wenn `AUTH_DEBUG_TOKEN` fehlt oder der Bearer nicht passt. Absichtlich 404 statt 401, damit die Route sich nicht selbst ankuendigt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}}},"operationId":"getApiV1Auth-debugDebug","tags":["auth"],"parameters":[],"summary":"Diagnose fuer Anmeldeprobleme: was der Server an dieser Anfrage sieht","description":"Diagnose fuer Anmeldeprobleme: zeigt, WAS der Server an dieser Anfrage sieht, ohne etwas zu aendern. Zurueck kommen host, origin, x-forwarded-host, x-forwarded-proto, User-Agent, die NAMEN der mitgeschickten Cookies (nicht deren Werte), deren Anzahl, ob ein Better-Auth-Cookie dabei ist, und das Ergebnis eines Sitzungsaufloesungs-Versuchs. Letzteres ist bei Erfolg die E-MAIL-ADRESSE des Anwenders, dem das mitgeschickte Cookie gehoert — also ein personenbezogenes Datum, kein blosser Statuswert. Aufgeloest wird ausschliesslich die Sitzung des AUFRUFERS; fremde Sitzungen sind darueber nicht erreichbar. Schlaegt die Aufloesung fehl, steht stattdessen der interne Fehlertext im Feld `sessionResolved`.","security":[]}},"/api/v1/demo/login":{"post":{"responses":{"200":{"description":"Demo session minted. `sessionToken` is returned ONLY here — the validation endpoint does not repeat it. `expiresAt` is unix milliseconds.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"sessionToken":{"type":"string"},"userId":{"type":"string"},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"role":{"type":"string","const":"viewer"},"expiresAt":{"type":"number"},"referrer":{"type":"string"}},"required":["ok","sessionToken","userId","tenantId","tenantSlug","role","expiresAt"],"additionalProperties":false},"example":{"ok":true,"sessionToken":"string","userId":"string","tenantId":"string","tenantSlug":"string","role":"viewer","expiresAt":0,"referrer":"string"}}}}},"operationId":"postApiV1DemoLogin","tags":["demo"],"parameters":[],"description":"Mint a short-lived (60min) read-only session for the public demo tenant. This endpoint is PUBLIC and mounted in front of the auth and tenant middleware, so no cookie, key or tenant header is needed. Every call mints a NEW token and never invalidates an earlier one. The body is optional: a missing or unparsable body is ignored rather than rejected, and only `referrer` is read — it is echoed back and otherwise unused. With the default in-process store the session lives in memory only: it is capped at 10 000 entries (the oldest is dropped when full) and is not shared between replicas, so a token minted on one instance may be unknown on the next request.","summary":"Mint a short-lived (60min) read-only session for the public demo tenant","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/v1/demo/session/{token}":{"get":{"responses":{"200":{"description":"Session valid — the same fields as the login response, MINUS `sessionToken` and `referrer`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"userId":{"type":"string"},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"role":{"type":"string","const":"viewer"},"expiresAt":{"type":"number"}},"required":["ok","userId","tenantId","tenantSlug","role","expiresAt"],"additionalProperties":false},"example":{"ok":true,"userId":"string","tenantId":"string","tenantSlug":"string","role":"viewer","expiresAt":0}}}},"401":{"description":"Invalid or expired. `invalid_token` means the path segment was shorter than 16 chars and was never looked up; `expired` means the store had no live session for it — which also covers a token that was evicted or minted on another replica.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","enum":["invalid_token","expired"]}},"required":["ok","error"],"additionalProperties":false}}}}},"operationId":"getApiV1DemoSessionByToken","tags":["demo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"token","required":true}],"description":"Validate a demo session token. Returns 401 if expired or unknown.","summary":"Validate a demo session token","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/demo/logout":{"post":{"responses":{"200":{"description":"Logged out — a bare acknowledgement, nothing else.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1DemoLogout","tags":["demo"],"parameters":[],"description":"Explicit logout — wipes the demo session token. Idempotent. The token is read from the `x-demo-session` HEADER, not from the body and not from a cookie; a missing header is not an error, the call simply touches nothing. It also answers `ok` for an unknown or already expired token, so the response is no proof that a session existed. Only that one token is dropped — tokens from other logins stay valid.","summary":"Explicit logout — wipes the demo session token","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/plans":{"get":{"responses":{"200":{"description":"Die drei buchbaren Tarife in fester Reihenfolge. `free` ist NICHT dabei — der Tarif existiert im System, wird hier aber nicht angeboten. Die Liste ist fest verdrahtet und haengt an keinem Mandanten; es gibt keinen Fehlerpfad.","content":{"application/json":{"schema":{"type":"object","properties":{"plans":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"starter | professional | enterprise"},"name":{"type":"string","description":"Derzeit identisch mit `id` — kein Anzeigename"},"quotas":{"type":"object","properties":{"maxUsers":{"type":"integer"},"maxStorageGb":{"type":"integer"},"aiRequestsPerMonth":{"type":["integer","null"],"description":"null = unbegrenzt"},"maxApiCallsPerMonth":{"type":["integer","null"],"description":"null = unbegrenzt"},"apiRpm":{"type":["integer","null"],"description":"Anfragen je Minute; null = unbegrenzt"},"emailsPerMonth":{"type":["integer","null"],"description":"null = unbegrenzt"},"voiceMinutesPerMonth":{"type":["integer","null"],"description":"Enthaltene Telefonminuten; null = unbegrenzt"}},"required":["maxUsers","maxStorageGb","aiRequestsPerMonth","maxApiCallsPerMonth"]},"priceId":{"type":["string","null"],"description":"Stripe-Preis-Id; null, wenn sie in dieser Umgebung nicht gesetzt ist"}},"required":["id","name","quotas","priceId"]}}},"required":["plans"]},"example":{"plans":[{"id":"string","name":"string","quotas":{"maxUsers":0,"maxStorageGb":0,"aiRequestsPerMonth":0,"maxApiCallsPerMonth":0,"apiRpm":0,"emailsPerMonth":0,"voiceMinutesPerMonth":0},"priceId":"string"}]}}}}},"operationId":"getApiV1BillingPlans","tags":["billing"],"parameters":[],"summary":"Lists the public subscription plans with quotas and Stripe price ids","description":"List public Nemix subscription plans (starter, professional, enterprise) with quotas and Stripe price-ids.","security":[]}},"/embed/manifest":{"get":{"responses":{"200":{"description":"Manifest mit Tools — fuer alle Mandanten dasselbe","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string","const":"1.0"},"tools":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","enum":["aufmass","hoai","kontierung","gaeb","ocr-beleg","voice-input","elster","datev","zugferd"]},"title":{"type":"string"},"url":{"type":"string","description":"Pfad der einbettbaren Oberflaeche, ohne Host"},"themes":{"type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"}}},"required":["slug","title","url","themes","scopes"]}},"themes":{"type":"array","items":{"type":"string"}},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt des Abrufs, kein Stand der Liste"}},"required":["version","tools","themes","generatedAt"]},"example":{"version":"1.0","tools":[{"slug":"aufmass","title":"string","url":"string","themes":["string"],"scopes":["string"]}],"themes":["string"],"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getEmbedManifest","tags":["embed"],"parameters":[],"description":"Liste aller einbettbaren Tools fuer das WP-Plugin abrufen. Die Route liegt VOR der Anmeldung: sie braucht weder Sitzung noch Mandant und antwortet jedem. Der Inhalt steht fest im Code und ist nicht je Mandant eingerichtet — welche Werkzeuge ein Mandant wirklich gebucht hat, sagt sie NICHT. generatedAt ist der Zeitpunkt des Abrufs, kein Stand der Liste.","summary":"Liste aller einbettbaren Tools fuer das WP-Plugin abrufen","x-nemix-summary-source":"description:first-sentence"}},"/embed/sso/exchange":{"post":{"responses":{"200":{"description":"SSO-Token, 5 Minuten gueltig","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","description":"Undurchsichtige Zeichenkette, 5 Minuten gueltig"},"expiresAt":{"type":"integer","description":"Ablauf als Unix-Zeitstempel in SEKUNDEN"},"tenant":{"type":"string","description":"Mandant aus dem eingereichten JWT"},"tool":{"type":"string","description":"Werkzeug aus dem eingereichten JWT"},"issuer":{"type":"string","description":"Aussteller des eingereichten JWT"}},"required":["token","expiresAt","tenant","tool","issuer"]},"example":{"token":"string","expiresAt":0,"tenant":"string","tool":"string","issuer":"string"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"503":{"description":"EMBED_SSO_SECRET nicht gesetzt"}},"operationId":"postEmbedSsoExchange","tags":["embed"],"parameters":[],"description":"Ainemix-JWT gegen kurzlebigen Nemix-SSO-Token tauschen. Auch diese Route liegt vor der Anmeldung. Geprueft wird ein HS256-JWT gegen EMBED_SSO_SECRET: ein anderes Verfahren, eine falsche Signatur, ein abgelaufener Token oder eine fehlende Angabe ergeben 401; ein unlesbarer Rumpf oder ein fehlendes jwt-Feld 400. Der ausgestellte Token gilt 5 Minuten, traegt Nutzer, Mandant und Werkzeug aus dem eingereichten JWT und wird nirgends gespeichert. Sind die Geheimnisse nicht gesetzt, antwortet die Route mit 503.","summary":"Ainemix-JWT gegen kurzlebigen Nemix-SSO-Token tauschen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/embed/manifest":{"get":{"responses":{"200":{"description":"Manifest mit Tools — fuer alle Mandanten dasselbe","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string","const":"1.0"},"tools":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","enum":["aufmass","hoai","kontierung","gaeb","ocr-beleg","voice-input","elster","datev","zugferd"]},"title":{"type":"string"},"url":{"type":"string","description":"Pfad der einbettbaren Oberflaeche, ohne Host"},"themes":{"type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"}}},"required":["slug","title","url","themes","scopes"]}},"themes":{"type":"array","items":{"type":"string"}},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt des Abrufs, kein Stand der Liste"}},"required":["version","tools","themes","generatedAt"]},"example":{"version":"1.0","tools":[{"slug":"aufmass","title":"string","url":"string","themes":["string"],"scopes":["string"]}],"themes":["string"],"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1EmbedManifest","tags":["embed"],"parameters":[],"description":"Liste aller einbettbaren Tools fuer das WP-Plugin abrufen. Die Route liegt VOR der Anmeldung: sie braucht weder Sitzung noch Mandant und antwortet jedem. Der Inhalt steht fest im Code und ist nicht je Mandant eingerichtet — welche Werkzeuge ein Mandant wirklich gebucht hat, sagt sie NICHT. generatedAt ist der Zeitpunkt des Abrufs, kein Stand der Liste.","summary":"Liste aller einbettbaren Tools fuer das WP-Plugin abrufen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/embed/sso/exchange":{"post":{"responses":{"200":{"description":"SSO-Token, 5 Minuten gueltig","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","description":"Undurchsichtige Zeichenkette, 5 Minuten gueltig"},"expiresAt":{"type":"integer","description":"Ablauf als Unix-Zeitstempel in SEKUNDEN"},"tenant":{"type":"string","description":"Mandant aus dem eingereichten JWT"},"tool":{"type":"string","description":"Werkzeug aus dem eingereichten JWT"},"issuer":{"type":"string","description":"Aussteller des eingereichten JWT"}},"required":["token","expiresAt","tenant","tool","issuer"]},"example":{"token":"string","expiresAt":0,"tenant":"string","tool":"string","issuer":"string"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"503":{"description":"EMBED_SSO_SECRET nicht gesetzt"}},"operationId":"postApiV1EmbedSsoExchange","tags":["embed"],"parameters":[],"description":"Ainemix-JWT gegen kurzlebigen Nemix-SSO-Token tauschen. Auch diese Route liegt vor der Anmeldung. Geprueft wird ein HS256-JWT gegen EMBED_SSO_SECRET: ein anderes Verfahren, eine falsche Signatur, ein abgelaufener Token oder eine fehlende Angabe ergeben 401; ein unlesbarer Rumpf oder ein fehlendes jwt-Feld 400. Der ausgestellte Token gilt 5 Minuten, traegt Nutzer, Mandant und Werkzeug aus dem eingereichten JWT und wird nirgends gespeichert. Sind die Geheimnisse nicht gesetzt, antwortet die Route mit 503.","summary":"Ainemix-JWT gegen kurzlebigen Nemix-SSO-Token tauschen","x-nemix-summary-source":"description:first-sentence"}},"/supplier/login":{"post":{"responses":{"200":{"description":"DREI AUSPRAEGUNGEN DESSELBEN KOERPERS: in der Produktion `ok` und `expiresAt`; ausserhalb der Produktion zusaetzlich `token` im Klartext (Entwicklungshilfe, damit Tests ohne Postfach laufen); bei unbekannter Adresse nur `ok`. Der letzte Fall ist absichtlich nicht sicher unterscheidbar.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` — auch bei unbekannter Adresse. Ob es den Lieferanten gibt, verraet die Antwort NICHT."},"token":{"type":"string","description":"NUR AUSSERHALB DER PRODUKTION: der Anmelde-Schluessel im Klartext, damit Tests und die oertliche Oberflaeche ohne Postfach durchlaufen. In der Produktion fehlt das Feld — dort geht der Schluessel ausschliesslich per E-Mail heraus."},"expiresAt":{"type":"integer","description":"Ablauf des Schluessels in Millisekunden seit 1970. Fehlt, wenn die Adresse unbekannt war — daran ist der Fall aber nicht zuverlaessig erkennbar."}},"required":["ok"],"additionalProperties":false},"example":{"ok":true,"token":"string","expiresAt":0}}}},"400":{"description":"Feld `email` fehlt oder ist leer (`email required`). Als Text."}},"operationId":"postSupplierLogin","tags":["supplier"],"parameters":[],"summary":"Anmelde-Link anfordern","description":"Lieferanten-Portal Magic-Link per Email anfordern. Der Endpunkt antwortet auch bei unbekannter Adresse mit 200 — er verraet nicht, wer im System steht.","security":[]}},"/supplier/login/exchange":{"post":{"responses":{"200":{"description":"Die erzeugte Sitzung. Ihr Schluessel gehoert in den Kopf `Authorization: Bearer …`.","content":{"application/json":{"schema":{"type":"object","properties":{"session":{"type":"string","minLength":1,"description":"Der Sitzungsschluessel. Gehoert bei allen weiteren Aufrufen in den Kopf `Authorization: Bearer …`."},"supplierId":{"type":"string","minLength":1,"description":"Der Lieferant, zu dem die Sitzung gehoert."},"email":{"type":"string","minLength":1,"description":"Die Adresse, an die der Anmelde-Schluessel ging."},"expiresAt":{"type":"integer","description":"Ablauf der Sitzung in Millisekunden seit 1970 — eine ZAHL, keine ISO-Zeichenkette."}},"required":["session","supplierId","email","expiresAt"],"additionalProperties":false},"example":{"session":"string","supplierId":"string","email":"string","expiresAt":0}}}},"400":{"description":"Feld `token` fehlt oder ist leer (`token required`). Als Text."},"401":{"description":"Schluessel unbekannt, abgelaufen oder schon verbraucht. Als Text."}},"operationId":"postSupplierLoginExchange","tags":["supplier"],"parameters":[],"summary":"Anmelde-Link gegen Sitzung tauschen","description":"Magic-Link-Token gegen Lieferanten-Session tauschen. Der Anmelde-Schluessel gilt nur EINMAL; ein zweiter Versuch mit demselben endet in 401.","security":[]}},"/supplier/orders":{"get":{"responses":{"200":{"description":"Die offenen Bestellungen dieses Lieferanten — unter `orders`, nicht unter `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"orders":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Bestellung."},"supplierId":{"type":"string","minLength":1,"description":"Der Lieferant. Fremde Bestellungen liefert dieser Endpunkt nie aus."},"poNumber":{"type":"string","minLength":1,"description":"Bestellnummer des Bestellers."},"status":{"type":"string","enum":["open","confirmed","partially_delivered","delivered","cancelled"],"description":"Zustand: `open` offen, `confirmed` bestaetigt, `partially_delivered` teilgeliefert, `delivered` vollstaendig, `cancelled` storniert."},"createdAt":{"type":"integer","description":"Anlagezeitpunkt in Millisekunden seit 1970 — eine ZAHL, keine ISO-Zeichenkette."},"lines":{"type":"array","items":{"type":"object","properties":{"pos":{"type":"integer","minimum":1,"description":"Laufende Nummer der Position innerhalb der Bestellung."},"sku":{"type":"string","description":"Artikelnummer des Bestellers."},"ean":{"type":"string","description":"EAN des Artikels. Fehlt, wenn keine hinterlegt ist."},"description":{"type":"string","description":"Bezeichnung der Position."},"quantityOrdered":{"type":"number","description":"Bestellte Menge."},"quantityDelivered":{"type":"number","description":"Bereits gelieferte Menge. Steigt mit jeder Liefer-Bestaetigung."},"unit":{"type":"string","description":"Mengeneinheit. Fehlt, wenn keine hinterlegt ist."}},"required":["pos","sku","description","quantityOrdered","quantityDelivered"],"additionalProperties":false},"description":"Die Positionen der Bestellung."}},"required":["id","supplierId","poNumber","status","createdAt","lines"],"additionalProperties":false},"description":"Die offenen Bestellungen dieses Lieferanten. Ungeblaettert und ohne Filter — stornierte und vollstaendig gelieferte sind nicht dabei."}},"required":["orders"],"additionalProperties":false},"example":{"orders":[{"id":"string","supplierId":"string","poNumber":"string","status":"open","createdAt":0,"lines":[{"pos":1,"sku":"string","ean":"string","description":"string","quantityOrdered":0,"quantityDelivered":0,"unit":"string"}]}]}}}},"401":{"description":"Kein oder ungueltiger Sitzungsschluessel (`invalid supplier session`). Als Text."}},"operationId":"getSupplierOrders","tags":["supplier"],"parameters":[],"summary":"Offene Bestellungen auflisten","description":"Offene Bestellungen des eingeloggten Lieferanten auflisten. STAND DER UMSETZUNG: die Bestellungen liegen im Arbeitsspeicher dieses Prozesses, nicht in der Datenbank — die Liste ist nach jedem Neustart leer und wird nur ueber `seedSupplierPortal` gefuellt. Eine leere Antwort heisst hier also nicht zwingend „keine offenen Bestellungen\"."}},"/supplier/orders/{id}":{"get":{"responses":{"200":{"description":"Die Bestellung — unter `order`, nicht unter `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Bestellung."},"supplierId":{"type":"string","minLength":1,"description":"Der Lieferant. Fremde Bestellungen liefert dieser Endpunkt nie aus."},"poNumber":{"type":"string","minLength":1,"description":"Bestellnummer des Bestellers."},"status":{"type":"string","enum":["open","confirmed","partially_delivered","delivered","cancelled"],"description":"Zustand: `open` offen, `confirmed` bestaetigt, `partially_delivered` teilgeliefert, `delivered` vollstaendig, `cancelled` storniert."},"createdAt":{"type":"integer","description":"Anlagezeitpunkt in Millisekunden seit 1970 — eine ZAHL, keine ISO-Zeichenkette."},"lines":{"type":"array","items":{"type":"object","properties":{"pos":{"type":"integer","minimum":1,"description":"Laufende Nummer der Position innerhalb der Bestellung."},"sku":{"type":"string","description":"Artikelnummer des Bestellers."},"ean":{"type":"string","description":"EAN des Artikels. Fehlt, wenn keine hinterlegt ist."},"description":{"type":"string","description":"Bezeichnung der Position."},"quantityOrdered":{"type":"number","description":"Bestellte Menge."},"quantityDelivered":{"type":"number","description":"Bereits gelieferte Menge. Steigt mit jeder Liefer-Bestaetigung."},"unit":{"type":"string","description":"Mengeneinheit. Fehlt, wenn keine hinterlegt ist."}},"required":["pos","sku","description","quantityOrdered","quantityDelivered"],"additionalProperties":false},"description":"Die Positionen der Bestellung."}},"required":["id","supplierId","poNumber","status","createdAt","lines"],"additionalProperties":false,"description":"Die Bestellung. Unter `order`, nicht unter `data`."}},"required":["order"],"additionalProperties":false},"example":{"order":{"id":"string","supplierId":"string","poNumber":"string","status":"open","createdAt":0,"lines":[{"pos":1,"sku":"string","ean":"string","description":"string","quantityOrdered":0,"quantityDelivered":0,"unit":"string"}]}}}}},"401":{"description":"Kein oder ungueltiger Sitzungsschluessel. Als Text."},"404":{"description":"Die Bestellung gibt es nicht ODER sie gehoert einem anderen Lieferanten — beides als `not found`, damit der Zugriff auf fremde Bestellungen nicht bestaetigt wird. Als Text."}},"operationId":"getSupplierOrdersById","tags":["supplier"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelne Bestellung lesen","description":"Einzelne Bestellung des Lieferanten abrufen. Wie bei der Liste stammen die Daten aus dem Arbeitsspeicher des Prozesses, nicht aus der Datenbank."}},"/supplier/orders/{id}/confirm-delivery":{"post":{"responses":{"200":{"description":"Die Bestellung NACH der Bestaetigung — unter `order`, nicht unter `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Bestellung."},"supplierId":{"type":"string","minLength":1,"description":"Der Lieferant. Fremde Bestellungen liefert dieser Endpunkt nie aus."},"poNumber":{"type":"string","minLength":1,"description":"Bestellnummer des Bestellers."},"status":{"type":"string","enum":["open","confirmed","partially_delivered","delivered","cancelled"],"description":"Zustand: `open` offen, `confirmed` bestaetigt, `partially_delivered` teilgeliefert, `delivered` vollstaendig, `cancelled` storniert."},"createdAt":{"type":"integer","description":"Anlagezeitpunkt in Millisekunden seit 1970 — eine ZAHL, keine ISO-Zeichenkette."},"lines":{"type":"array","items":{"type":"object","properties":{"pos":{"type":"integer","minimum":1,"description":"Laufende Nummer der Position innerhalb der Bestellung."},"sku":{"type":"string","description":"Artikelnummer des Bestellers."},"ean":{"type":"string","description":"EAN des Artikels. Fehlt, wenn keine hinterlegt ist."},"description":{"type":"string","description":"Bezeichnung der Position."},"quantityOrdered":{"type":"number","description":"Bestellte Menge."},"quantityDelivered":{"type":"number","description":"Bereits gelieferte Menge. Steigt mit jeder Liefer-Bestaetigung."},"unit":{"type":"string","description":"Mengeneinheit. Fehlt, wenn keine hinterlegt ist."}},"required":["pos","sku","description","quantityOrdered","quantityDelivered"],"additionalProperties":false},"description":"Die Positionen der Bestellung."}},"required":["id","supplierId","poNumber","status","createdAt","lines"],"additionalProperties":false,"description":"Die Bestellung. Unter `order`, nicht unter `data`."}},"required":["order"],"additionalProperties":false},"example":{"order":{"id":"string","supplierId":"string","poNumber":"string","status":"open","createdAt":0,"lines":[{"pos":1,"sku":"string","ean":"string","description":"string","quantityOrdered":0,"quantityDelivered":0,"unit":"string"}]}}}}},"400":{"description":"Feld `lines` fehlt (`lines required`). Als Text."},"401":{"description":"Kein oder ungueltiger Sitzungsschluessel. Als Text."},"404":{"description":"Bestellung unbekannt oder fremd. Als Text."},"422":{"description":"Die Bestaetigung ist fachlich unzulaessig — etwa eine unbekannte Positionsnummer oder mehr Menge als bestellt. Der Grund steht im Text."}},"operationId":"postSupplierOrdersByIdConfirm-delivery","tags":["supplier"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lieferung bestaetigen","description":"Liefer-Bestaetigung fuer eine Bestellung erfassen. Die gemeldeten Mengen werden auf die Positionen gerechnet, danach steht die Bestellung auf `partially_delivered` oder `delivered`. Die Aenderung bleibt im Arbeitsspeicher des Prozesses und uebersteht keinen Neustart."}},"/supplier/orders/{id}/edi":{"get":{"responses":{"200":{"description":"Die Rechnungsdatei als Anhang — EDIFACT-Text oder, bei `format=json`, JSON.","content":{"application/edifact":{"schema":{"type":"string"},"example":"UNH+00000001+INVOIC:D:96A:UN'BGM+380+DC-PO-2026-0001+9'DTM+137:20260101:102'RFF+ON:PO-2026-0001'NAD+BY+NEMIX-BUYER::9'NAD+SU+sup-1::9'LIN+1++4006381333931:EN'IMD+F++:::Schraube M8'QTY+46:200:PCE'UNT+10+00000001'"},"application/json":{"example":{"format":"invoic-json-stub-v1","documentNumber":"DC-PO-2026-0001","documentDate":"20260101","purchaseOrderRef":"PO-2026-0001","buyer":{"id":"NEMIX-BUYER","name":"Nemix Buyer"},"supplier":{"id":"sup-1","name":"lieferant@example.com"},"lines":[{"pos":1,"sku":"SKU-100","ean":"4006381333931","description":"Schraube M8","quantityDelivered":200,"unit":"PCE"}]}}}},"401":{"description":"Kein oder ungueltiger Sitzungsschluessel. Als Text."},"404":{"description":"Bestellung unbekannt oder fremd. Als Text."}},"operationId":"getSupplierOrdersByIdEdi","tags":["supplier"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"EDIFACT-INVOIC exportieren","description":"EDIFACT-INVOIC fuer eine Bestellung als Datei exportieren. Ueber `format=json` kommt stattdessen die JSON-Fassung derselben Nutzdaten. Beides wird als Anhang ausgeliefert."}},"/api/v1/supplier/login":{"post":{"responses":{"200":{"description":"DREI AUSPRAEGUNGEN DESSELBEN KOERPERS: in der Produktion `ok` und `expiresAt`; ausserhalb der Produktion zusaetzlich `token` im Klartext (Entwicklungshilfe, damit Tests ohne Postfach laufen); bei unbekannter Adresse nur `ok`. Der letzte Fall ist absichtlich nicht sicher unterscheidbar.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` — auch bei unbekannter Adresse. Ob es den Lieferanten gibt, verraet die Antwort NICHT."},"token":{"type":"string","description":"NUR AUSSERHALB DER PRODUKTION: der Anmelde-Schluessel im Klartext, damit Tests und die oertliche Oberflaeche ohne Postfach durchlaufen. In der Produktion fehlt das Feld — dort geht der Schluessel ausschliesslich per E-Mail heraus."},"expiresAt":{"type":"integer","description":"Ablauf des Schluessels in Millisekunden seit 1970. Fehlt, wenn die Adresse unbekannt war — daran ist der Fall aber nicht zuverlaessig erkennbar."}},"required":["ok"],"additionalProperties":false},"example":{"ok":true,"token":"string","expiresAt":0}}}},"400":{"description":"Feld `email` fehlt oder ist leer (`email required`). Als Text."}},"operationId":"postApiV1SupplierLogin","tags":["supplier"],"parameters":[],"summary":"Anmelde-Link anfordern","description":"Lieferanten-Portal Magic-Link per Email anfordern. Der Endpunkt antwortet auch bei unbekannter Adresse mit 200 — er verraet nicht, wer im System steht.","security":[]}},"/api/v1/supplier/login/exchange":{"post":{"responses":{"200":{"description":"Die erzeugte Sitzung. Ihr Schluessel gehoert in den Kopf `Authorization: Bearer …`.","content":{"application/json":{"schema":{"type":"object","properties":{"session":{"type":"string","minLength":1,"description":"Der Sitzungsschluessel. Gehoert bei allen weiteren Aufrufen in den Kopf `Authorization: Bearer …`."},"supplierId":{"type":"string","minLength":1,"description":"Der Lieferant, zu dem die Sitzung gehoert."},"email":{"type":"string","minLength":1,"description":"Die Adresse, an die der Anmelde-Schluessel ging."},"expiresAt":{"type":"integer","description":"Ablauf der Sitzung in Millisekunden seit 1970 — eine ZAHL, keine ISO-Zeichenkette."}},"required":["session","supplierId","email","expiresAt"],"additionalProperties":false},"example":{"session":"string","supplierId":"string","email":"string","expiresAt":0}}}},"400":{"description":"Feld `token` fehlt oder ist leer (`token required`). Als Text."},"401":{"description":"Schluessel unbekannt, abgelaufen oder schon verbraucht. Als Text."}},"operationId":"postApiV1SupplierLoginExchange","tags":["supplier"],"parameters":[],"summary":"Anmelde-Link gegen Sitzung tauschen","description":"Magic-Link-Token gegen Lieferanten-Session tauschen. Der Anmelde-Schluessel gilt nur EINMAL; ein zweiter Versuch mit demselben endet in 401.","security":[]}},"/api/v1/supplier/orders":{"get":{"responses":{"200":{"description":"Die offenen Bestellungen dieses Lieferanten — unter `orders`, nicht unter `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"orders":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Bestellung."},"supplierId":{"type":"string","minLength":1,"description":"Der Lieferant. Fremde Bestellungen liefert dieser Endpunkt nie aus."},"poNumber":{"type":"string","minLength":1,"description":"Bestellnummer des Bestellers."},"status":{"type":"string","enum":["open","confirmed","partially_delivered","delivered","cancelled"],"description":"Zustand: `open` offen, `confirmed` bestaetigt, `partially_delivered` teilgeliefert, `delivered` vollstaendig, `cancelled` storniert."},"createdAt":{"type":"integer","description":"Anlagezeitpunkt in Millisekunden seit 1970 — eine ZAHL, keine ISO-Zeichenkette."},"lines":{"type":"array","items":{"type":"object","properties":{"pos":{"type":"integer","minimum":1,"description":"Laufende Nummer der Position innerhalb der Bestellung."},"sku":{"type":"string","description":"Artikelnummer des Bestellers."},"ean":{"type":"string","description":"EAN des Artikels. Fehlt, wenn keine hinterlegt ist."},"description":{"type":"string","description":"Bezeichnung der Position."},"quantityOrdered":{"type":"number","description":"Bestellte Menge."},"quantityDelivered":{"type":"number","description":"Bereits gelieferte Menge. Steigt mit jeder Liefer-Bestaetigung."},"unit":{"type":"string","description":"Mengeneinheit. Fehlt, wenn keine hinterlegt ist."}},"required":["pos","sku","description","quantityOrdered","quantityDelivered"],"additionalProperties":false},"description":"Die Positionen der Bestellung."}},"required":["id","supplierId","poNumber","status","createdAt","lines"],"additionalProperties":false},"description":"Die offenen Bestellungen dieses Lieferanten. Ungeblaettert und ohne Filter — stornierte und vollstaendig gelieferte sind nicht dabei."}},"required":["orders"],"additionalProperties":false},"example":{"orders":[{"id":"string","supplierId":"string","poNumber":"string","status":"open","createdAt":0,"lines":[{"pos":1,"sku":"string","ean":"string","description":"string","quantityOrdered":0,"quantityDelivered":0,"unit":"string"}]}]}}}},"401":{"description":"Kein oder ungueltiger Sitzungsschluessel (`invalid supplier session`). Als Text."}},"operationId":"getApiV1SupplierOrders","tags":["supplier"],"parameters":[],"summary":"Offene Bestellungen auflisten","description":"Offene Bestellungen des eingeloggten Lieferanten auflisten. STAND DER UMSETZUNG: die Bestellungen liegen im Arbeitsspeicher dieses Prozesses, nicht in der Datenbank — die Liste ist nach jedem Neustart leer und wird nur ueber `seedSupplierPortal` gefuellt. Eine leere Antwort heisst hier also nicht zwingend „keine offenen Bestellungen\"."}},"/api/v1/supplier/orders/{id}":{"get":{"responses":{"200":{"description":"Die Bestellung — unter `order`, nicht unter `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Bestellung."},"supplierId":{"type":"string","minLength":1,"description":"Der Lieferant. Fremde Bestellungen liefert dieser Endpunkt nie aus."},"poNumber":{"type":"string","minLength":1,"description":"Bestellnummer des Bestellers."},"status":{"type":"string","enum":["open","confirmed","partially_delivered","delivered","cancelled"],"description":"Zustand: `open` offen, `confirmed` bestaetigt, `partially_delivered` teilgeliefert, `delivered` vollstaendig, `cancelled` storniert."},"createdAt":{"type":"integer","description":"Anlagezeitpunkt in Millisekunden seit 1970 — eine ZAHL, keine ISO-Zeichenkette."},"lines":{"type":"array","items":{"type":"object","properties":{"pos":{"type":"integer","minimum":1,"description":"Laufende Nummer der Position innerhalb der Bestellung."},"sku":{"type":"string","description":"Artikelnummer des Bestellers."},"ean":{"type":"string","description":"EAN des Artikels. Fehlt, wenn keine hinterlegt ist."},"description":{"type":"string","description":"Bezeichnung der Position."},"quantityOrdered":{"type":"number","description":"Bestellte Menge."},"quantityDelivered":{"type":"number","description":"Bereits gelieferte Menge. Steigt mit jeder Liefer-Bestaetigung."},"unit":{"type":"string","description":"Mengeneinheit. Fehlt, wenn keine hinterlegt ist."}},"required":["pos","sku","description","quantityOrdered","quantityDelivered"],"additionalProperties":false},"description":"Die Positionen der Bestellung."}},"required":["id","supplierId","poNumber","status","createdAt","lines"],"additionalProperties":false,"description":"Die Bestellung. Unter `order`, nicht unter `data`."}},"required":["order"],"additionalProperties":false},"example":{"order":{"id":"string","supplierId":"string","poNumber":"string","status":"open","createdAt":0,"lines":[{"pos":1,"sku":"string","ean":"string","description":"string","quantityOrdered":0,"quantityDelivered":0,"unit":"string"}]}}}}},"401":{"description":"Kein oder ungueltiger Sitzungsschluessel. Als Text."},"404":{"description":"Die Bestellung gibt es nicht ODER sie gehoert einem anderen Lieferanten — beides als `not found`, damit der Zugriff auf fremde Bestellungen nicht bestaetigt wird. Als Text."}},"operationId":"getApiV1SupplierOrdersById","tags":["supplier"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelne Bestellung lesen","description":"Einzelne Bestellung des Lieferanten abrufen. Wie bei der Liste stammen die Daten aus dem Arbeitsspeicher des Prozesses, nicht aus der Datenbank."}},"/api/v1/supplier/orders/{id}/confirm-delivery":{"post":{"responses":{"200":{"description":"Die Bestellung NACH der Bestaetigung — unter `order`, nicht unter `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Bestellung."},"supplierId":{"type":"string","minLength":1,"description":"Der Lieferant. Fremde Bestellungen liefert dieser Endpunkt nie aus."},"poNumber":{"type":"string","minLength":1,"description":"Bestellnummer des Bestellers."},"status":{"type":"string","enum":["open","confirmed","partially_delivered","delivered","cancelled"],"description":"Zustand: `open` offen, `confirmed` bestaetigt, `partially_delivered` teilgeliefert, `delivered` vollstaendig, `cancelled` storniert."},"createdAt":{"type":"integer","description":"Anlagezeitpunkt in Millisekunden seit 1970 — eine ZAHL, keine ISO-Zeichenkette."},"lines":{"type":"array","items":{"type":"object","properties":{"pos":{"type":"integer","minimum":1,"description":"Laufende Nummer der Position innerhalb der Bestellung."},"sku":{"type":"string","description":"Artikelnummer des Bestellers."},"ean":{"type":"string","description":"EAN des Artikels. Fehlt, wenn keine hinterlegt ist."},"description":{"type":"string","description":"Bezeichnung der Position."},"quantityOrdered":{"type":"number","description":"Bestellte Menge."},"quantityDelivered":{"type":"number","description":"Bereits gelieferte Menge. Steigt mit jeder Liefer-Bestaetigung."},"unit":{"type":"string","description":"Mengeneinheit. Fehlt, wenn keine hinterlegt ist."}},"required":["pos","sku","description","quantityOrdered","quantityDelivered"],"additionalProperties":false},"description":"Die Positionen der Bestellung."}},"required":["id","supplierId","poNumber","status","createdAt","lines"],"additionalProperties":false,"description":"Die Bestellung. Unter `order`, nicht unter `data`."}},"required":["order"],"additionalProperties":false},"example":{"order":{"id":"string","supplierId":"string","poNumber":"string","status":"open","createdAt":0,"lines":[{"pos":1,"sku":"string","ean":"string","description":"string","quantityOrdered":0,"quantityDelivered":0,"unit":"string"}]}}}}},"400":{"description":"Feld `lines` fehlt (`lines required`). Als Text."},"401":{"description":"Kein oder ungueltiger Sitzungsschluessel. Als Text."},"404":{"description":"Bestellung unbekannt oder fremd. Als Text."},"422":{"description":"Die Bestaetigung ist fachlich unzulaessig — etwa eine unbekannte Positionsnummer oder mehr Menge als bestellt. Der Grund steht im Text."}},"operationId":"postApiV1SupplierOrdersByIdConfirm-delivery","tags":["supplier"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lieferung bestaetigen","description":"Liefer-Bestaetigung fuer eine Bestellung erfassen. Die gemeldeten Mengen werden auf die Positionen gerechnet, danach steht die Bestellung auf `partially_delivered` oder `delivered`. Die Aenderung bleibt im Arbeitsspeicher des Prozesses und uebersteht keinen Neustart."}},"/api/v1/supplier/orders/{id}/edi":{"get":{"responses":{"200":{"description":"Die Rechnungsdatei als Anhang — EDIFACT-Text oder, bei `format=json`, JSON.","content":{"application/edifact":{"schema":{"type":"string"},"example":"UNH+00000001+INVOIC:D:96A:UN'BGM+380+DC-PO-2026-0001+9'DTM+137:20260101:102'RFF+ON:PO-2026-0001'NAD+BY+NEMIX-BUYER::9'NAD+SU+sup-1::9'LIN+1++4006381333931:EN'IMD+F++:::Schraube M8'QTY+46:200:PCE'UNT+10+00000001'"},"application/json":{"example":{"format":"invoic-json-stub-v1","documentNumber":"DC-PO-2026-0001","documentDate":"20260101","purchaseOrderRef":"PO-2026-0001","buyer":{"id":"NEMIX-BUYER","name":"Nemix Buyer"},"supplier":{"id":"sup-1","name":"lieferant@example.com"},"lines":[{"pos":1,"sku":"SKU-100","ean":"4006381333931","description":"Schraube M8","quantityDelivered":200,"unit":"PCE"}]}}}},"401":{"description":"Kein oder ungueltiger Sitzungsschluessel. Als Text."},"404":{"description":"Bestellung unbekannt oder fremd. Als Text."}},"operationId":"getApiV1SupplierOrdersByIdEdi","tags":["supplier"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"EDIFACT-INVOIC exportieren","description":"EDIFACT-INVOIC fuer eine Bestellung als Datei exportieren. Ueber `format=json` kommt stattdessen die JSON-Fassung derselben Nutzdaten. Beides wird als Anhang ausgeliefert."}},"/api/v1/portal/quote-accept/{token}":{"get":{"responses":{"200":{"description":"The quote may be displayed. `accepted` says whether it is still open.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"customerName":{"type":"string"},"quoteNumber":{"type":"string"},"total":{"type":"number"},"status":{"type":"string"},"accepted":{"type":"boolean"}},"required":["customerName","quoteNumber","total","status","accepted"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"customerName":"string","quoteNumber":"string","total":0,"status":"string","accepted":true}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"410":{"description":"Token missing, malformed, tampered with, expired — or tenant/quote not found. One single answer for all of these, on purpose.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_or_expired_link"}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"More than 30 requests per minute from this IP address.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rate_limited"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}},"503":{"description":"Database unavailable. Retry after `retryAfter` seconds.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"portal.quoteAccept.view","tags":["Customer-Portal"],"parameters":[{"name":"token","in":"path","required":true,"description":"Opaque HMAC-signed token from the emailed link, in the form `<base64url-payload>.<base64url-signature>`. Carries tenant, quote and expiry. Minted by `mintQuoteAcceptToken` in `lib/portal-token.ts`.","schema":{"type":"string"}}],"summary":"Read the quote behind a public accept link","description":"Renders the decision page the customer sees before accepting. Read-only\n— this endpoint changes nothing.\n\nPUBLICLY REACHABLE ENDPOINT — NO SESSION, NO API KEY.\nThe signed token in the path IS the credential. The caller is the end\ncustomer following a link from an email, not a signed-in employee, so every\nrequest here arrives from the open internet.\n\nTenant and quote are taken EXCLUSIVELY from the verified token. Nothing in\nthe request body, query string or headers can point this endpoint at a\ndifferent tenant or a different quote.\n\nFail-closed: a missing, malformed, tampered, expired or simply unknown\ntoken all produce the same `410`. The response never reveals whether the\ntenant or the quote exists, so the endpoint cannot be used to enumerate\neither.\n\nThrottled to 30 requests per minute per IP address (`ipRateLimit` in\n`apps/api/src/index.ts`), which is what makes brute-forcing the token\nsignature impractical.\n\nIGNORE THE `401` AND THE INHERITED SECURITY REQUIREMENT SHOWN FOR THIS\nOPERATION. Both are generated automatically for every endpoint behind the\nauth middleware, and this one is not behind it. It accepts no session\ncookie and no API key, and it never answers `401`. The generator derives\nthat flag from `PUBLIC_PATHS`, which does not list this route because the\nroute is public by mount order instead — see the comment in\n`routes/customer-portal/quote-accept.ts`.\n\nDELIBERATELY MINIMAL PAYLOAD. Customer name, quote number, total and\nstatus are all that is returned. No line items, no notes, no addresses,\nno contact data. Anyone holding the link holds this data, so the set is\nkept to what the accept page has to show. Do not extend it without\nweighing that.\n\n`accepted` mirrors `status === \"accepted\"` and lets the page tell an\nopen quote from one that was already accepted, without the client\nhaving to know the tenant status vocabulary."},"post":{"responses":{"200":{"description":"Accepted. Without `alreadyAccepted` this call performed the transition and emitted `quote.accepted`; with it the quote was already accepted and nothing happened.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"status":{"type":"string","const":"accepted"},"alreadyAccepted":{"type":"boolean","const":true},"message":{"type":"string"}},"required":["ok","status","message"],"additionalProperties":false},"example":{"ok":true,"status":"accepted","alreadyAccepted":true,"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"`quote_not_acceptable` — the quote exists and the link is valid, but its status is not `sent`. The blocking status is returned.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"quote_not_acceptable"},"status":{"type":"string"},"message":{"type":"string"}},"required":["error","status","message"],"additionalProperties":false}}}},"410":{"description":"Token missing, malformed, tampered with, expired — or tenant/quote not found. One single answer for all of these, on purpose.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_or_expired_link"}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"More than 30 requests per minute from this IP address.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rate_limited"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}},"503":{"description":"Database unavailable — the quote was NOT accepted. Retry after `retryAfter` seconds; the endpoint is idempotent, so retrying is safe.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"portal.quoteAccept.accept","tags":["Customer-Portal"],"parameters":[{"name":"token","in":"path","required":true,"description":"Opaque HMAC-signed token from the emailed link, in the form `<base64url-payload>.<base64url-signature>`. Carries tenant, quote and expiry. Minted by `mintQuoteAcceptToken` in `lib/portal-token.ts`.","schema":{"type":"string"}}],"summary":"Accept a quote from the emailed customer link","description":"THE CUSTOMER ACCEPTS THE QUOTE HERE. This is the commercially binding\nstate change in the quoting process, and it is triggered by the end\ncustomer, not by anyone inside the company.\n\nPUBLICLY REACHABLE ENDPOINT — NO SESSION, NO API KEY.\nThe signed token in the path IS the credential. The caller is the end\ncustomer following a link from an email, not a signed-in employee, so every\nrequest here arrives from the open internet.\n\nTenant and quote are taken EXCLUSIVELY from the verified token. Nothing in\nthe request body, query string or headers can point this endpoint at a\ndifferent tenant or a different quote.\n\nFail-closed: a missing, malformed, tampered, expired or simply unknown\ntoken all produce the same `410`. The response never reveals whether the\ntenant or the quote exists, so the endpoint cannot be used to enumerate\neither.\n\nThrottled to 30 requests per minute per IP address (`ipRateLimit` in\n`apps/api/src/index.ts`), which is what makes brute-forcing the token\nsignature impractical.\n\nIGNORE THE `401` AND THE INHERITED SECURITY REQUIREMENT SHOWN FOR THIS\nOPERATION. Both are generated automatically for every endpoint behind the\nauth middleware, and this one is not behind it. It accepts no session\ncookie and no API key, and it never answers `401`. The generator derives\nthat flag from `PUBLIC_PATHS`, which does not list this route because the\nroute is public by mount order instead — see the comment in\n`routes/customer-portal/quote-accept.ts`.\n\nWHAT IT CHANGES. On success the quote moves `sent` -> `accepted` and a\n`quote.accepted` outbound webhook is emitted with\n`acceptedVia: \"customer-portal-link\"`. The webhook goes out\nfire-and-forget AFTER the row was written, so a failing subscriber\nnever rolls the acceptance back. There is no undo on this endpoint.\n\nTAKES NO REQUEST BODY. Anything sent is ignored. The token alone\ndetermines what is accepted.\n\nIDEMPOTENT. Calling it twice is safe and returns `200` both times. The\nsecond call carries `alreadyAccepted: true` and emits NO second webhook.\nThe update is written as a conditional `UPDATE … WHERE status = 'sent'`,\nso two simultaneous clicks cannot both fire the event.\n\nONLY `sent` CAN BE ACCEPTED. A quote in `draft`, `rejected`, `expired`\nor `cancelled` is refused with `409` and the blocking status is named in\nthe response. That is a different situation from a bad link (`410`) and\nshould be shown differently: the link was fine, the quote was not."}},"/api/v1/customer-portal/p/{slug}/login":{"post":{"responses":{"200":{"description":"`{ sent: true }` — und zwar IMMER, wenn die Anfrage formal in Ordnung war. Die Antwort verraet WEDER, ob die Adresse zum Portal gehoert (der Aufruf legt das Mitglied bei Bedarf an), NOCH ob der Mailversand geklappt hat. Beides ist Absicht: sonst waere die Route ein Adressen-Pruefdienst.","content":{"application/json":{"schema":{"type":"object","properties":{"sent":{"type":"boolean","const":true,"description":"IMMER true, sobald die Anfrage formal in Ordnung war — unabhaengig davon, ob die Mail wirklich rausging."}},"required":["sent"],"additionalProperties":false},"example":{"sent":true}}}},"400":{"description":"Portal-Kennung oder Adresse ungueltig (text/plain)"},"402":{"description":"Der Mandant hat keinen kostenpflichtigen Tarif und keine laufende Testphase. ACHTUNG: die Pruefung ist fail-open — schlaegt die Tarif-Abfrage selbst fehl, kommt KEIN 402. Ein ausbleibender 402 beweist also keinen Tarif (text/plain)"},"429":{"description":"Zu viele Anfragen fuer diese Adresse (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"postApiV1Customer-portalPBySlugLogin","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Magic-Link für Portal-Login anfordern","description":"Ohne Anmeldung erreichbar; der Mandant ergibt sich aus der Portal-Kennung im Pfad. Das Mitglied wird bei Bedarf ANGELEGT — jede formal gueltige Adresse kann sich damit selbst am Portal eintragen. Erzeugt wird ein Einmal-Token, von dem nur der SHA-256-Hash und die Ablaufzeit gespeichert werden; ein zuvor angefordertes Token wird dabei ueberschrieben, es gilt also immer nur der zuletzt verschickte Link. Eine bestehende Sitzung bleibt unberuehrt. Je Portal und Adresse sind drei Anforderungen pro Stunde erlaubt, danach 429. Der Mailversand ist best-effort: schlaegt er fehl, bleibt die Antwort 200. Jede Anforderung wird mit Adresse, IP und Browserkennung protokolliert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":254}},"required":["email"]},"example":{"email":"beispiel@example.com"}}}},"security":[]}},"/api/v1/customer-portal/p/{slug}/config":{"get":{"responses":{"200":{"description":"Branding und Funktionsschalter. OEFFENTLICH, ohne Anmeldung — die Antwort enthaelt deshalb bewusst keine Mitglieds- oder Mandantendaten, nur was die Anmeldeseite zum Rendern braucht.","content":{"application/json":{"schema":{"type":"object","properties":{"portal":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"}},"required":["slug","name"],"additionalProperties":false},"branding":{"type":"object","properties":{"logoUrl":{"type":"string"},"primaryColor":{"type":"string"},"secondaryColor":{"type":"string"},"fontFamily":{"type":"string"},"welcomeText":{"type":"string"},"footerText":{"type":"string"},"customCss":{"type":"string"}},"required":["logoUrl","primaryColor","secondaryColor","fontFamily","welcomeText","footerText"],"additionalProperties":false},"features":{"type":"object","properties":{"allowForms":{"type":"boolean"},"allowAppointments":{"type":"boolean"},"allowDocuments":{"type":"boolean"},"allowMessages":{"type":"boolean"}},"required":["allowForms","allowAppointments","allowDocuments","allowMessages"],"additionalProperties":false},"plan":{"type":"string"},"poweredBy":{"type":"boolean","description":"Nemix-Hinweis im Fuss — faellt nur bei `enterprise` weg"}},"required":["portal","branding","features","plan","poweredBy"],"additionalProperties":false},"example":{"portal":{"slug":"string","name":"string"},"branding":{"logoUrl":"string","primaryColor":"string","secondaryColor":"string","fontFamily":"string","welcomeText":"string","footerText":"string","customCss":"string"},"features":{"allowForms":true,"allowAppointments":true,"allowDocuments":true,"allowMessages":true},"plan":"string","poweredBy":true}}}},"400":{"description":"Portal-Kennung ungueltig (text/plain)"},"404":{"description":"Kein aktives Portal unter dieser Kennung (text/plain)"}},"operationId":"getApiV1Customer-portalPBySlugConfig","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Public Branding + Feature-Flags eines Portals","description":"Liefert, was die Anmeldeseite zum Rendern braucht, BEVOR sich jemand angemeldet hat: Logo, Farben, Schrift und Texte sowie die vier Funktionsschalter (Formulare, Termine, Dokumente, Nachrichten). Die Schalter stehen auf true, wenn fuer das Portal keine Einstellungen hinterlegt sind ODER die Abfrage scheitert — ein true heiszt also nicht zwingend „bewusst erlaubt\". `plan` faellt aus demselben Grund auf \"free\" zurueck und steuert nur `poweredBy` (der Nemix-Hinweis im Fusz entfaellt allein bei \"enterprise\"). Anders als die uebrigen Portal-Endpunkte prueft dieser den Tarif NICHT: die Seite rendert auch dann, wenn eine Anmeldung mit 402 abgewiesen wuerde.","security":[]}},"/api/v1/customer-portal/p/{slug}/verify":{"get":{"responses":{"200":{"description":"NUR mit `?json=1`. Gedacht fuer Tests und Clients ohne Browser. Nebenwirkung in beiden Faellen: der Sitzungs-Cookie wird gesetzt — DER entscheidet ab jetzt, nicht diese Antwort.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"portalSlug":{"type":"string"},"memberId":{"type":"string"},"email":{"type":"string"},"sessionExpiresAt":{"type":"string"}},"required":["ok","portalSlug","memberId","email","sessionExpiresAt"],"additionalProperties":false},"example":{"ok":true,"portalSlug":"string","memberId":"string","email":"string","sessionExpiresAt":"string"}}}},"302":{"description":"DER NORMALFALL. Ohne `?json=1` antwortet die Route mit einer Weiterleitung auf `/p/:slug/dashboard` — kein JSON. Wer hier einen Koerper erwartet, bekommt keinen."},"400":{"description":"Portal-Kennung oder Token fehlt/ungueltig (text/plain)"},"404":{"description":"Kein Mitglied zu diesem Token (text/plain)"},"410":{"description":"Token abgelaufen — ein neuer Magic-Link ist noetig (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"getApiV1Customer-portalPBySlugVerify","tags":["Customer-Portal"],"parameters":[{"in":"query","name":"token","schema":{"type":"string","minLength":16,"maxLength":128},"required":true},{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Magic-Link-Token verifizieren + Session setzen","description":"Das Token steht als Query-Parameter `token` in der URL. Es ist EINMALIG: bei Erfolg werden Hash und Ablauf am Mitglied geloescht, ein zweiter Aufruf desselben Links ergibt darum 404. Erzeugt wird eine Sitzung, von der wiederum nur der Hash gespeichert wird; der Rohwert geht ausschlieszlich als HttpOnly-Cookie an den Browser und nie in den Antwortkoerper. Die Laufzeit richtet sich nach der Portal-Einstellung. Auszerdem wird der Zeitpunkt der letzten Anmeldung fortgeschrieben. Eine zuvor bestehende Sitzung desselben Mitglieds wird dabei ersetzt und damit ungueltig.","security":[]}},"/api/v1/customer-portal/p/{slug}/logout":{"post":{"responses":{"200":{"description":"Immer `{ ok: true }` — die Route wirft NIE 401, auch nicht ohne gueltige Sitzung. Abmelden ist idempotent. Das Loeschen des Sitzungs-Hash in der Datenbank ist best-effort; der Cookie wird in jedem Fall entfernt.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Portal-Kennung ungueltig (text/plain)"}},"operationId":"postApiV1Customer-portalPBySlugLogout","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Portal-Session beenden","description":"Entfernt den Sitzungs-Cookie und setzt den gespeicherten Sitzungs-Hash des Mitglieds zurueck, sodass ein anderweitig kopiertes Sitzungs-Token nicht weitergilt. Beides greift nur, wenn der Cookie mitkommt und die Datenbank erreichbar ist — scheitert der zweite Schritt, wird er still uebergangen und der Cookie trotzdem geloescht. Es wird kein Rumpf erwartet, und offene Magic-Links werden nicht entwertet.","security":[]}},"/api/v1/customer-portal/p/{slug}/forms":{"get":{"responses":{"200":{"description":"Aktive Formulare des Portals, hoechstens 100.","content":{"application/json":{"schema":{"type":"object","properties":{"forms":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]}},"required":["id","title","description"],"additionalProperties":false}}},"required":["forms"],"additionalProperties":false},"example":{"forms":[{"id":"string","title":"string","description":"string"}]}}}},"400":{"description":"`invalid_portal_slug` — der Slug passt nicht auf das erlaubte Muster."},"401":{"description":"Keine gueltige Mitglieds-Sitzung: `portal_session_required`, `portal_session_invalid`, `portal_member_disabled` oder `portal_session_expired`."},"404":{"description":"`portal_not_found` — kein aktives Portal unter diesem Slug."},"503":{"description":"`database_unavailable`."}},"operationId":"getApiV1Customer-portalPBySlugForms","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Aktive Forms des Portals listen (customer-side)","description":"Listet die Formulare des Portals, die auf `status = active` stehen.\n\nDie Eintraege tragen absichtlich NUR Titel und Beschreibung. Die Felder\neiner Definition holt der Renderer einzeln ueber\n`GET /p/{slug}/forms/{form_id}`.\n\nEs gibt KEINE Paginierung. Die Liste schneidet nach 100 Formularen ab,\nsortiert nach Anlagedatum absteigend. Wer mehr Formulare hat, sieht die\naeltesten nicht.\n\nZur Reihenfolge der Pruefungen: der 404 faellt, BEVOR die\nMitglieds-Sitzung geprueft wird. Ein Aufrufer ohne gueltige Sitzung kann\nan 404 gegen 401 also unterscheiden, ob es das Portal gibt. Das ist\nhinnehmbar, weil der Slug ohnehin in der oeffentlichen Portal-Adresse\nsteht, aber es ist eine Eigenschaft der Route und keine Zufaelligkeit.","security":[]}},"/api/v1/customer-portal/p/{slug}/forms/{form_id}":{"get":{"responses":{"200":{"description":"Formular-Definition mit allen Feldern.","content":{"application/json":{"schema":{"type":"object","properties":{"form":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"fields":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["text","email","date","select","checkbox","file","textarea"]},"label":{"type":"string"},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"string"}},"validation":{"type":"string"},"placeholder":{"type":"string"}},"required":["name","type","label","required"]}}},"required":["id","title","description","fields"],"additionalProperties":false}},"required":["form"],"additionalProperties":false},"example":{"form":{"id":"string","title":"string","description":"string","fields":[{"name":"string","type":"text","label":"string","required":true,"options":["string"],"validation":"string","placeholder":"string"}]}}}}},"400":{"description":"`invalid_portal_slug` oder `invalid_form_id` (leer oder laenger als 100 Zeichen)."},"401":{"description":"Keine gueltige Mitglieds-Sitzung."},"403":{"description":"`form_not_in_member_portal` — das Formular gehoert einem anderen Portal als die Sitzung. Zusaetzlicher Riegel hinter der Slug-Bindung der Abfrage."},"404":{"description":"`form_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"getApiV1Customer-portalPBySlugFormsByForm_id","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"form_id","required":true}],"summary":"Form-Definition für Renderer (customer-side)","description":"Liefert die vollstaendige Definition EINES Formulars samt Feldern. Das\nist die Vorlage, aus der die Portal-Oberflaeche die Eingabemaske baut.\n\nDer 404 deckt drei Faelle unter einer Antwort ab: es gibt das Formular\nnicht, es steht nicht auf `active`, oder das Portal steht nicht auf\n`active`. Ein Formular, das der Mandant gerade deaktiviert hat, ist von\neinem geloeschten nicht zu unterscheiden.\n\nDie Felder werden beim Lesen NICHT erneut validiert. Sie kommen als\nJSONB aus der Zeile und werden durchgereicht, so wie die Anlege-Route\nsie hineingeschrieben hat.","security":[]}},"/api/v1/customer-portal/p/{slug}/forms/{form_id}/submit":{"post":{"responses":{"201":{"description":"Submission gespeichert. Die Felder neben `submission_id` sind Ergebnisse von Nebenlaeufen und duerfen `null` sein.","content":{"application/json":{"schema":{"type":"object","properties":{"submission_id":{"type":"string"},"ticket_id":{"type":["string","null"]},"ai_category":{"type":["string","null"]},"ai_confidence":{"type":["number","null"]},"assigned_to_user_id":{"type":["string","null"]},"message":{"type":"string"}},"required":["submission_id","ticket_id","ai_category","ai_confidence","assigned_to_user_id","message"],"additionalProperties":false},"example":{"submission_id":"string","ticket_id":"string","ai_category":"string","ai_confidence":0,"assigned_to_user_id":"string","message":"string"}}}},"400":{"description":"`invalid_portal_slug`, `invalid_form_id`, `invalid_json`, `invalid_body` oder `submission_validation_failed:<feldnamen>`."},"401":{"description":"Keine gueltige Mitglieds-Sitzung."},"403":{"description":"`form_not_in_member_portal`."},"404":{"description":"`form_not_found` — unbekannt, nicht aktiv, oder Portal nicht aktiv."},"503":{"description":"`database_unavailable`."}},"operationId":"postApiV1Customer-portalPBySlugFormsByForm_idSubmit","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"form_id","required":true}],"summary":"Form-Submission abgeben","description":"Nimmt die ausgefuellte Maske entgegen und legt eine Submission an.\n\nWAS DER 201 BEWEIST — UND WAS NICHT.\nBewiesen ist genau eine Sache: die Submission steht in der Datenbank,\n`submission_id` ist ihre Kennung. Alles danach laeuft best-effort und\ndarf fehlschlagen, ohne die Abgabe zu kippen:\n\n· die KI-Zuordnung (`ai_category`, `ai_confidence`,\n  `assigned_to_user_id`),\n· das automatische Ticket (`ticket_id`),\n· die Bestaetigungsmail an das Mitglied, die gar keine Spur in der\n  Antwort hinterlaesst.\n\nJeder dieser drei Schritte faengt seine Fehler ab und schreibt eine\nWarnung ins Protokoll. Deshalb ist `null` in diesen Feldern\nMEHRDEUTIG: entweder das Formular hat den Schritt nicht bestellt\n(`auto_route_via_ai` beziehungsweise `auto_create_ticket` steht aus),\noder der Schritt lief und schlug fehl. Die Antwort trennt das nicht.\nWer wissen muss, ob ein Ticket entstanden ist, darf sich auf ein\nfehlendes `ticket_id` nicht als Absichtserklaerung verlassen.\n\nDer 400 bei fehlgeschlagener Feldpruefung NENNT die betroffenen Felder:\n`submission_validation_failed:<name>,<name>`. Die Regeln stammen aus der\nDefinition des Formulars und werden zur Laufzeit daraus gebaut.\n\n`message` ist ein fester deutscher Satz fuer die Anzeige. Er ist kein\nStatuswert; es gehoert sich nicht, darauf zu verzweigen.","security":[]}},"/api/v1/customer-portal/p/{slug}/appointments/types":{"get":{"responses":{"200":{"description":"Aktive Termin-Arten dieses Portals, neueste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"durationMinutes":{"type":"integer"},"color":{"type":["string","null"]},"bufferMinutes":{"type":"integer","description":"Puffer NACH dem Termin, geht in die Slot-Rechnung ein"},"advanceNoticeHours":{"type":"integer","description":"Mindest-Vorlauf; frueher liegende Slots entfallen"}},"required":["id","title","description","durationMinutes","color","bufferMinutes","advanceNoticeHours"],"additionalProperties":false}}},"required":["types"],"additionalProperties":false},"example":{"types":[{"id":"string","title":"string","description":"string","durationMinutes":0,"color":"string","bufferMinutes":0,"advanceNoticeHours":0}]}}}},"400":{"description":"Portal-Kennung ungueltig (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"getApiV1Customer-portalPBySlugAppointmentsTypes","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Buchbare Termin-Types listen","description":"Listet die Termin-Arten dieses Portals, die auf `active` stehen — neueste zuerst, ohne Filter und ohne Blaetterung. Je Art kommen Dauer, Pufferzeit und Vorlauffrist in Stunden zurueck; genau diese Werte bestimmen, welche Slots `/availability` spaeter ausgibt. Aufrufbar nur als angemeldetes Mitglied dieses Portals.","security":[]}},"/api/v1/customer-portal/p/{slug}/appointments/availability":{"get":{"responses":{"200":{"description":"Freie Slots. Die Liste ist eine Momentaufnahme — zwischen Abruf und Buchung kann ein Slot weg sein; die Buchung antwortet dann mit 409.","content":{"application/json":{"schema":{"type":"object","properties":{"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string","description":"ISO 8601 UTC"},"end":{"type":"string","description":"ISO 8601 UTC"}},"required":["start","end"],"additionalProperties":false},"description":"Freie Slots im angefragten Fenster. Eine leere Liste heisst „nichts frei\", nicht „Fehler\" — das Fenster darf hoechstens 60 Tage umfassen."}},"required":["slots"],"additionalProperties":false},"example":{"slots":[{"start":"string","end":"string"}]}}}},"400":{"description":"Portal-Kennung, Zeitfenster oder Typ-Id ungueltig — auch, wenn das Fenster groesser als 60 Tage ist (text/plain)"},"404":{"description":"Termin-Art nicht gefunden oder nicht aktiv (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"getApiV1Customer-portalPBySlugAppointmentsAvailability","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Verfuegbare Slots fuer einen Termin-Type","description":"Rechnet die freien Zeitfenster fuer die Termin-Art `type_id` im Bereich `from` bis `to` aus (beide als Zeitpunkt mit Zeitzone). Das Fenster darf hoechstens 60 Tage umfassen — mehr wird mit 400 abgelehnt, damit sich niemand die Datenbank lahmlegt. Beruecksichtigt werden die Oeffnungszeiten der Art, hinterlegte Ausnahmen (Feiertage, Sperrzeiten) und bereits vergebene Termine samt ihrer Puffer. Es wird nichts reserviert: die Liste ist eine Momentaufnahme. Aufrufbar nur als angemeldetes Mitglied dieses Portals.","security":[]}},"/api/v1/customer-portal/p/{slug}/appointments/book":{"post":{"responses":{"201":{"description":"Gebucht. Die Bestaetigungsmail ist BEST-EFFORT — schlaegt sie fehl, bleibt der Termin trotzdem stehen und diese Antwort trotzdem 201.","content":{"application/json":{"schema":{"type":"object","properties":{"appointment_id":{"type":"string"},"scheduled_at":{"type":"string"},"duration_minutes":{"type":"integer"},"status":{"type":"string"},"message":{"type":"string","description":"Fertiger deutscher Satz fuer die Oberflaeche"}},"required":["appointment_id","scheduled_at","duration_minutes","status","message"],"additionalProperties":false},"example":{"appointment_id":"string","scheduled_at":"string","duration_minutes":0,"status":"string","message":"string"}}}},"400":{"description":"Portal-Kennung oder Rumpf ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"404":{"description":"Termin-Art nicht gefunden oder nicht aktiv (text/plain)"},"409":{"description":"Slot inzwischen belegt. Wird an ZWEI Stellen geworfen: bei der Vorpruefung und noch einmal beim Schreiben, damit zwei gleichzeitige Buchungen nicht beide durchgehen (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"postApiV1Customer-portalPBySlugAppointmentsBook","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Termin buchen","description":"Bucht den Slot `start_at` fuer die Termin-Art `type_id` auf das angemeldete Mitglied; `notes` ist eine freie Mitteilung an den Anbieter. Der Termin wird auf `confirmed` gesetzt — es gibt keine Bestaetigungsschleife. Gegen zwei gleichzeitige Buchungen sichert nicht die Vorpruefung, sondern das Schreiben selbst: es greift nur, wenn sich kein bestehender Termin mit dem Slot samt Puffer ueberschneidet, sonst 409. Nebenwirkungen, alle nur nach Kraeften und ohne Einfluss auf das Ergebnis: der Termin wandert in den Kalender, der zustaendige Mitarbeiter bekommt eine Nachricht in seinen Posteingang, und an das Mitglied geht eine Bestaetigungsmail mit einem Stornolink. Das Merkmal aus diesem Link steht in KEINER Antwort dieser API — wer es verliert, kann ueber das Portal nicht mehr stornieren.","security":[]}},"/api/v1/customer-portal/p/{slug}/appointments/my":{"get":{"responses":{"200":{"description":"Eigene Termine, neueste zuerst, hoechstens 100. KEINE Paginierung — wer mehr hat, sieht die aeltesten nicht. Stornierte sind enthalten (`status`).","content":{"application/json":{"schema":{"type":"object","properties":{"appointments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"typeId":{"type":"string"},"title":{"type":["string","null"]},"color":{"type":["string","null"]},"scheduledAt":{"type":"string"},"durationMinutes":{"type":"integer"},"status":{"type":"string"},"notes":{"type":["string","null"]}},"required":["id","typeId","title","color","scheduledAt","durationMinutes","status","notes"],"additionalProperties":false}}},"required":["appointments"],"additionalProperties":false},"example":{"appointments":[{"id":"string","typeId":"string","title":"string","color":"string","scheduledAt":"string","durationMinutes":0,"status":"string","notes":"string"}]}}}},"400":{"description":"Portal-Kennung ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"getApiV1Customer-portalPBySlugAppointmentsMy","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Eigene Termine listen","description":"Listet die Termine des ANGEMELDETEN Mitglieds in diesem Portal, spaeteste zuerst. Fremde Termine sind nicht erreichbar; die Eingrenzung geschieht ueber die Sitzung, nicht ueber einen Parameter. Es gibt keine Blaetterung und keinen Filter: es kommen hoechstens 100 Zeilen, aeltere fallen weg. Stornierte Termine sind enthalten und nur an `status` erkennbar. Titel und Farbe stammen aus der Termin-Art und bleiben leer, wenn diese inzwischen entfernt wurde.","security":[]}},"/api/v1/customer-portal/p/{slug}/appointments/cancel":{"post":{"responses":{"200":{"description":"ZWEI Koerper unter demselben Code: `{ok,cancelled,appointment_id}`, wenn dieser Aufruf storniert hat — `{ok,already_cancelled}`, wenn er schon storniert war. Der Unterschied sagt, ob die Aktion gewirkt hat.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"cancelled":{"type":"boolean","const":true},"appointment_id":{"type":"string"}},"required":["ok","cancelled","appointment_id"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"already_cancelled":{"type":"boolean","const":true}},"required":["ok","already_cancelled"],"additionalProperties":false}]},"example":{"ok":true,"cancelled":true,"appointment_id":"string"}}}},"400":{"description":"Portal-Kennung oder Token fehlt/ungueltig (text/plain)"},"404":{"description":"Kein Termin zu diesem Token (text/plain)"},"409":{"description":"Termin ist bereits stattgefunden oder als nicht erschienen vermerkt — nicht mehr stornierbar (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"postApiV1Customer-portalPBySlugAppointmentsCancel","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Termin via Magic-Token stornieren","description":"Storniert einen Termin ueber das `token` aus dem Stornolink der Bestaetigungsmail (Abfrageparameter, 16 bis 128 Zeichen). Der Aufruf braucht KEINE Anmeldung — das Merkmal allein weist aus; gespeichert ist davon nur die Pruefsumme. Das Merkmal ist EINMALIG: mit dem Storno wird es geloescht, ein zweiter Aufruf desselben Links findet nichts mehr und antwortet 404. Die Antwort `already_cancelled` kommt daher nicht vom zweiten Klick, sondern wenn der Termin auf anderem Weg bereits storniert wurde. Ein stattgefundener oder als nicht erschienen vermerkter Termin laesst sich nicht mehr stornieren (409). Nach dem Storno wird der Kalender-Eintrag zurueckgenommen und eine Absagemail verschickt — beides nur nach Kraeften, ihr Fehlschlag aendert das Ergebnis nicht.","security":[]}},"/api/v1/customer-portal/p/{slug}/appointments/{id}/storno-link":{"post":{"responses":{"200":{"description":"Mail ist raus. `sent_to` ist die Adresse des angemeldeten Mitglieds — sie laesst sich NICHT im Aufruf waehlen, sonst waere das ein Versandkanal fuer Fremde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"sent_to":{"type":"string","description":"Die Adresse des angemeldeten Mitglieds, nicht frei waehlbar"}},"required":["ok","sent_to"],"additionalProperties":false},"example":{"ok":true,"sent_to":"string"}}}},"400":{"description":"Portal-Kennung oder Termin-Id ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"404":{"description":"Kein eigener Termin mit dieser Id (text/plain)"},"409":{"description":"Termin bereits storniert oder abgeschlossen (text/plain)"},"502":{"description":"Mailversand fehlgeschlagen — HIER nicht best-effort, weil ohne Mail kein Link ankommt (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"postApiV1Customer-portalPBySlugAppointmentsByIdStorno-link","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Storno-Link erneut anfordern","description":"Erzeugt ein neues Einmal-Token fuer den eigenen Termin und schickt den Storno-Link per E-Mail. Das zuvor verschickte Token verfaellt dabei.","security":[]}},"/api/v1/customer-portal/p/{slug}/documents/upload":{"post":{"responses":{"201":{"description":"Angelegt. Die Uebernahme ins DMS laeuft danach ohne Wartezeit weiter — deshalb steht hier noch keine `dmsDocumentId`.","content":{"application/json":{"schema":{"type":"object","properties":{"document":{"type":"object","properties":{"id":{"type":"string"},"fileName":{"type":"string"},"sizeBytes":{"type":"number"},"mimeType":{"type":["string","null"]},"uploadedAt":{"type":"string"},"status":{"type":"string","const":"active"}},"required":["id","fileName","sizeBytes","mimeType","uploadedAt","status"],"additionalProperties":false}},"required":["document"],"additionalProperties":false},"example":{"document":{"id":"string","fileName":"string","sizeBytes":0,"mimeType":"string","uploadedAt":"string","status":"active"}}}}},"400":{"description":"Kein `file`-Feld im Formular, leere Datei oder ungültige Portal-Kennung (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"404":{"description":"Kein aktives Portal mit dieser Kennung (text/plain)"},"413":{"description":"Datei über 25 MB, oder das Kontingent des Mitglieds von 100 MB wäre überschritten (text/plain)"},"415":{"description":"Dateityp nicht erlaubt oder Endung gesperrt (text/plain)"},"500":{"description":"Der Datenbank-Eintrag zur Datei konnte nicht geschrieben werden (text/plain)"},"502":{"description":"Die Ablage hat die Datei nicht angenommen, gespeichert wurde nichts (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"postApiV1Customer-portalPBySlugDocumentsUpload","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Datei hochladen (multipart/form-data, Feld \"file\")","description":"Die Grenzen zieht der Aufruf selbst: höchstens 25 MB je Datei und 100 MB je Mitglied über alle noch aktiven Dateien zusammen, beides mit 413 beantwortet. Erlaubt sind Bilder, PDF, Word-, Excel- und PowerPoint-Dateien sowie Text und CSV; alles andere, Archive eingeschlossen, endet in 415. Die Endung wiegt dabei schwerer als der gemeldete Typ: eine gesperrte Endung wird auch dann abgelehnt, wenn der Browser einen harmlosen MIME-Typ dazu meldet. Der Dateiname wird vor dem Ablegen bereinigt, Pfadanteile fallen weg und bei 200 Zeichen ist Schluss; in dieser Form steht er auch in der Antwort. Die Übernahme ins DMS des Mandanten läuft danach im Hintergrund: gelingt sie, erscheint die Verknüpfung später als `dmsDocumentId` in der Dokumentliste, scheitert sie, bleibt der Upload trotzdem gültig.","security":[]}},"/api/v1/customer-portal/p/{slug}/documents/my":{"get":{"responses":{"200":{"description":"Die eigenen aktiven Dateien, neueste zuerst, hoechstens 200. Es gibt KEINE Paginierung — wer mehr hat, sieht die aeltesten nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"documents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"fileName":{"type":"string"},"mimeType":{"type":["string","null"]},"sizeBytes":{"type":["number","null"]},"uploadedAt":{"type":"string"},"status":{"type":"string"},"dmsDocumentId":{"type":["string","null"],"description":"Verknuepfung ins DMS, `null` solange die Uebernahme laeuft oder scheiterte"}},"required":["id","fileName","mimeType","sizeBytes","uploadedAt","status","dmsDocumentId"],"additionalProperties":false}}},"required":["documents"],"additionalProperties":false},"example":{"documents":[{"id":"string","fileName":"string","mimeType":"string","sizeBytes":0,"uploadedAt":"string","status":"string","dmsDocumentId":"string"}]}}}},"400":{"description":"Portal-Kennung ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"404":{"description":"Kein aktives Portal mit dieser Kennung (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"getApiV1Customer-portalPBySlugDocumentsMy","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Eigene hochgeladene Dokumente listen","description":"Zeigt ausschließlich die eigenen Uploads dieses Portals: gefiltert wird auf Mitglied und Portal zugleich, dasselbe Konto in einem anderen Portal sieht davon nichts. Gelöschte Dateien fehlen, weil das Löschen nur den Status umsetzt und diese Liste allein auf `active` sieht. `dmsDocumentId` füllt sich, sobald die Übernahme ins DMS des Mandanten durch ist, und bleibt sonst leer.","security":[]}},"/api/v1/customer-portal/p/{slug}/documents/{id}/download":{"get":{"responses":{"200":{"description":"NICHT die Datei, sondern eine signierte URL darauf. Der Aufrufer laedt anschliessend direkt beim Ablageanbieter.","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"Signierte URL beim Ablageanbieter — NICHT die Datei selbst"},"fileName":{"type":"string"},"expiresInSeconds":{"type":"integer"}},"required":["url","fileName","expiresInSeconds"],"additionalProperties":false},"example":{"url":"string","fileName":"string","expiresInSeconds":0}}}},"400":{"description":"Portal-Kennung oder Dokument-Id ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"403":{"description":"Die Datei gehoert einem anderen Mitglied (text/plain)"},"404":{"description":"Kein aktives Portal mit dieser Kennung oder kein Dokument mit dieser Id darin (text/plain)"},"410":{"description":"Dokument existiert, ist aber nicht mehr aktiv (text/plain)"},"502":{"description":"Ablageanbieter konnte keine signierte URL ausstellen (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"getApiV1Customer-portalPBySlugDocumentsByIdDownload","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigene Datei via signed URL herunterladen (TTL 24h)","description":"`expiresInSeconds` ist fest: die Signatur läuft nach 24 Stunden ab, und jeder Aufruf stellt eine neue aus. Drei Fälle bleiben getrennt, statt zu einer Antwort zusammenzufallen: eine in diesem Portal unbekannte Id ergibt 404, die Datei eines anderen Mitglieds 403 und eine bereits gelöschte Datei 410. Dieser Weg verrät damit mehr als das Löschen, das den fremden Fall bewusst als 404 tarnt. Stellt die Ablage keine Signatur aus, endet der Aufruf in 502; am Dokument ändert sich dabei nichts.","security":[]}},"/api/v1/customer-portal/p/{slug}/documents/{id}":{"delete":{"responses":{"200":{"description":"Auf `status = deleted` gesetzt. Die Datei liegt weiter in der Ablage — `deleted: true` heisst geloescht aus Sicht des Portals, nicht vom Datentraeger.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deleted":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","deleted","id"],"additionalProperties":false},"example":{"ok":true,"deleted":true,"id":"string"}}}},"400":{"description":"Portal-Kennung oder Dokument-Id ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"404":{"description":"Nicht vorhanden, schon geloescht ODER fremd — BEWUSST kein 403, damit die Antwort die Existenz fremder Dateien nicht verraet (siehe Kommentar am UPDATE). Auch eine Kennung, zu der es kein aktives Portal gibt, endet hier."},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"deleteApiV1Customer-portalPBySlugDocumentsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigene Datei loeschen (soft-delete)","description":"Setzt den Status in einem einzigen Schritt und nur dann, wenn die Datei zu diesem Portal, zu diesem Mitglied und noch zum Zustand `active` gehört. Trifft eines davon nicht zu, ist die Antwort immer dieselbe 404. Wiederholbar ist der Aufruf deshalb nicht: ein zweites Löschen derselben Datei meldet 404, obwohl das erste geklappt hat. Der Eintrag im DMS des Mandanten bleibt bestehen; das Löschen wirkt nur im Portal."}},"/api/v1/customer-portal/p/{slug}/messages":{"get":{"responses":{"200":{"description":"Die eigenen Nachrichten, neueste zuerst. `hasMore` heisst nur, dass die Seite voll war — nicht, dass sicher weitere existieren.","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"memberId":{"type":"string"},"threadId":{"type":["string","null"]},"direction":{"type":"string","enum":["in","out"],"description":"`in` = vom Kunden, `out` = vom Mandanten"},"senderUserId":{"type":["string","null"]},"body":{"type":"string"},"attachments":{"type":"array","items":{},"description":"Rohform aus der Spalte; leer, wenn nichts anhaengt"},"aiCategory":{"type":["string","null"],"description":"Beste Klassifikation, `null` wenn nicht klassifiziert"},"aiConfidence":{"type":["number","null"]},"readAt":{"type":["string","null"]},"deliveryStatus":{"type":"string"},"emailMessageId":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","portalId","memberId","threadId","direction","senderUserId","body","attachments","aiCategory","aiConfidence","readAt","deliveryStatus","emailMessageId","createdAt"],"additionalProperties":false}},"hasMore":{"type":"boolean","description":"true, wenn die Seite voll ist — weiter mit `before` am aeltesten `createdAt`"}},"required":["messages","hasMore"],"additionalProperties":false},"example":{"messages":[{"id":"string","portalId":"string","memberId":"string","threadId":"string","direction":"in","senderUserId":"string","body":"string","attachments":[],"aiCategory":"string","aiConfidence":0,"readAt":"string","deliveryStatus":"string","emailMessageId":"string","createdAt":"string"}],"hasMore":true}}}},"400":{"description":"Portal-Kennung oder Abfrage ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"getApiV1Customer-portalPBySlugMessages","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Eigene Nachrichten listen (paginiert)","description":"Liest customer_portal_messages im Schema des Mandanten, gefiltert auf DIESES Portal und das angemeldete Mitglied — fremde Nachrichten sind hierueber nicht erreichbar. Sortiert wird absteigend nach Anlagezeitpunkt. limit nimmt 1 bis 200 an (Vorgabe 50); before blaettert weiter und erwartet einen ISO-8601-Zeitpunkt, ueblicherweise das createdAt der aeltesten bereits geholten Nachricht. Angemeldet wird nicht ueber eine Mandanten-Sitzung, sondern ueber die Portal-Mitgliedschaft zum Slug.","security":[]}},"/api/v1/customer-portal/p/{slug}/messages/send":{"post":{"responses":{"201":{"description":"Angelegt. Die Klassifikation ist BEST-EFFORT: `ai_category` und `ai_confidence` koennen `null` sein, die Nachricht ist trotzdem zugestellt.","content":{"application/json":{"schema":{"type":"object","properties":{"message_id":{"type":"string"},"ai_category":{"type":["string","null"]},"ai_confidence":{"type":["number","null"]},"created_at":{"type":"string"}},"required":["message_id","ai_category","ai_confidence","created_at"],"additionalProperties":false},"example":{"message_id":"string","ai_category":"string","ai_confidence":0,"created_at":"string"}}}},"400":{"description":"Rumpf oder Portal-Kennung ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"postApiV1Customer-portalPBySlugMessagesSend","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Nachricht an Mandanten senden","description":"Legt eine eingehende Nachricht (direction \"in\") in customer_portal_messages an. Der Text darf 1 bis 20 000 Zeichen haben, dazu bis zu 20 Anhaenge als Verweis — hochgeladen wird hier nichts. Vor dem Schreiben laeuft eine KI-Klassifikation; faellt sie aus, wird die Nachricht trotzdem gespeichert und die Kategorie bleibt leer. Danach bekommt ein Admin oder Manager des Mandanten einen Posteingangs-Eintrag und die Gegenseite eine WebSocket-Meldung; beides ist best-effort und darf ausbleiben, ohne dass der Aufruf scheitert.","security":[]}},"/api/v1/customer-portal/p/{slug}/messages/{id}/read":{"post":{"responses":{"200":{"description":"Quittiert. `read_at` bleibt beim ERSTEN Lesezeitpunkt — ein zweiter Aufruf ueberschreibt ihn nicht (COALESCE im UPDATE).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"read_at":{"type":["string","null"]}},"required":["ok","read_at"],"additionalProperties":false},"example":{"ok":true,"read_at":"string"}}}},"400":{"description":"Portal-Kennung oder Nachrichten-Id ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"},"404":{"description":"Keine eigene eingehende Nachricht mit dieser Id (text/plain)"},"503":{"description":"Datenbank nicht erreichbar (text/plain)"}},"operationId":"postApiV1Customer-portalPBySlugMessagesByIdRead","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Nachricht als gelesen markieren","description":"Setzt read_at und den Zustellstatus auf gelesen — aber nur fuer eine AUSGEHENDE Nachricht (direction \"out\") des eigenen Mitglieds in diesem Portal. Eine selbst gesendete oder eine fremde Kennung trifft nichts und ergibt 404. Die Lesebestaetigung geht zusaetzlich per WebSocket an die Gegenseite, best-effort.","security":[]}},"/api/v1/customer-portal/p/{slug}/messages/typing":{"post":{"responses":{"200":{"description":"Angenommen. Die Zustellung an die Gegenseite laeuft ueber WebSocket und ist BEST-EFFORT — `ok: true` sagt nichts darueber, ob jemand zuhoert.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Portal-Kennung oder Rumpf ungueltig (text/plain)"},"401":{"description":"Nicht als Portal-Mitglied angemeldet (text/plain)"}},"operationId":"postApiV1Customer-portalPBySlugMessagesTyping","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Typing-Indicator (WS-only)","description":"Meldet, dass das Mitglied gerade tippt. Gespeichert wird NICHTS: der Zustand geht ausschliesslich per WebSocket an die Gegenseite und ist danach weg. Der Rumpf traegt allein isTyping. Angemeldet wird ueber die Portal-Mitgliedschaft zum Slug.","security":[]}},"/api/v1/webhooks/portal-message-reply":{"post":{"responses":{"200":{"description":"Immer 200 — auch im Fehlerfall. `ok: true` heisst gespeichert, `ok: false` heisst verworfen, dann nennt `error` den Grund.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"message_id":{"type":"string","format":"uuid"},"ai_category":{"type":["string","null"]}},"required":["ok","message_id","ai_category"]},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","enum":["invalid_body","no_token","invalid_token","token_invalid_or_expired","empty_body","database_unavailable"]}},"required":["ok","error"]}]},"example":{"ok":true,"message_id":"00000000-0000-4000-8000-000000000000","ai_category":"string"}}}}},"operationId":"postApiV1WebhooksPortal-message-reply","tags":["webhooks","Customer-Portal"],"parameters":[],"summary":"Eingehende E-Mail-Antwort auf eine Portal-Nachricht annehmen","description":"Nimmt eine von SES/Mailgun/Postmark/Resend geparste Inbound-E-Mail entgegen und legt sie als eingehende Nachricht im passenden Mandanten-Schema ab. ANTWORTET IMMER MIT 200 — auch im Fehlerfall. Ein Aufrufer muss das Feld `ok` auswerten, nicht den Statuscode; `error` nennt dann den Grund.","security":[]}},"/api/v1/customer-portal/p/{slug}/custom-fields":{"get":{"responses":{"200":{"description":"Sichtbare Felddefinitionen samt Werten des angemeldeten Mitglieds.","content":{"application/json":{"schema":{"type":"object","properties":{"fields":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"fieldKey":{"type":"string"},"fieldLabel":{"type":"string"},"fieldType":{"type":"string"},"required":{"type":"boolean"},"defaultValue":{"type":["string","null"]},"options":{"type":["array","null"],"items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false}},"placeholder":{"type":["string","null"]},"helpText":{"type":["string","null"]},"editableByCustomer":{"type":"boolean"},"displayOrder":{"type":"number"},"value":{}},"required":["id","fieldKey","fieldLabel","fieldType","required","defaultValue","options","placeholder","helpText","editableByCustomer","displayOrder"],"additionalProperties":false}}},"required":["fields"],"additionalProperties":false},"example":{"fields":[{"id":"string","fieldKey":"string","fieldLabel":"string","fieldType":"string","required":true,"defaultValue":"string","options":[{"value":"string","label":"string"}],"placeholder":"string","helpText":"string","editableByCustomer":true,"displayOrder":0}]}}}},"400":{"description":"`invalid_portal_slug`."},"401":{"description":"Keine gueltige Mitglieds-Sitzung: `portal_session_required`, `portal_session_invalid`, `portal_member_disabled` oder `portal_session_expired`."},"404":{"description":"`portal_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"getApiV1Customer-portalPBySlugCustom-fields","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Custom-Felder + Customer-Werte fuer Dashboard-Formular","description":"Liefert die Felddefinitionen des Portals zusammen mit den Werten des\nangemeldeten Mitglieds. Die Portal-Oberflaeche baut daraus ihr\nDashboard-Formular.\n\nGezeigt werden NUR Felder mit `visible_on_dashboard`. Was der Mandant\nintern pflegt, taucht hier nicht auf. Das ist die Sichtgrenze zwischen\nMandant und Kunde, nicht bloss eine Anzeigeoption.\n\n`editableByCustomer` sagt, ob das Feld ueber die Schreibroute\nveraenderbar ist. Ein Feld ohne diese Erlaubnis wird ausgeliefert und\nangezeigt, aber jede Aenderung daran verworfen.\n\nEs gibt keine Paginierung. Ein Portal traegt eine ueberschaubare Zahl\nFelder, sortiert nach `displayOrder`.","security":[]},"post":{"responses":{"200":{"description":"Gespeichert. `written` ist die Zahl der tatsaechlich geschriebenen Felder, nicht die der gesendeten.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"written":{"type":"number"}},"required":["ok","written"],"additionalProperties":false},"example":{"ok":true,"written":0}}}},"400":{"description":"`invalid_portal_slug`, oder der Rumpf passt nicht auf `values` (Schluessel bis 63 Zeichen, Werte als Text, Zahl, Wahrheitswert, Liste oder `null`)."},"401":{"description":"Keine gueltige Mitglieds-Sitzung."},"404":{"description":"`portal_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"postApiV1Customer-portalPBySlugCustom-fields","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Customer-Werte fuer Custom-Felder speichern","description":"Speichert die Werte des angemeldeten Mitglieds. Vorhandene Werte werden\nueberschrieben, fehlende angelegt.\n\nNICHT JEDER GESENDETE WERT WIRD GESCHRIEBEN — UND DIE ANTWORT SAGT\nNICHT, WELCHER FEHLTE.\n`written` zaehlt die tatsaechlich geschriebenen Felder. Vier Gruende\nlassen einen Schluessel still unter den Tisch fallen:\n\n· der Schluessel gehoert zu keinem Feld dieses Portals,\n· das Feld traegt kein `editable_by_customer`,\n· das Feld ist nicht `visible_on_dashboard`,\n· das Feld ist ein Pflichtfeld und der gesendete Wert ist leer oder\n  `null`. Pflichtwerte lassen sich also nicht loeschen, nur ersetzen.\n\nIst `written` kleiner als die Zahl der gesendeten Schluessel, ist das\nkein Fehler, sondern der Normalfall bei nicht schreibbaren Feldern. Die\nAntwort nennt die verworfenen Schluessel NICHT. Wer wissen muss, was\nankam, liest anschliessend `GET /p/{slug}/custom-fields`.\n\nRUECKGABEFORM WEICHT VON DER EINGABEFORM AB.\nWahrheitswerte und Zahlen werden als Text abgelegt und kommen beim\nLesen als ZEICHENKETTE zurueck: aus gesendetem `true` wird `\"true\"`,\naus `42` wird `\"42\"`. Nur Listen und Objekte behalten ihre Form, weil\nsie in die JSONB-Spalte gehen. Ein Aufrufer, der den geschriebenen Wert\ngleich wieder einliest und auf Typgleichheit prueft, findet einen\nUnterschied, der keiner ist.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"values":{"type":"object","propertyNames":{"type":"string","minLength":1,"maxLength":63},"additionalProperties":{"anyOf":[{"type":"string","maxLength":10000},{"type":"number"},{"type":"boolean"},{"type":"array","items":{"type":"string","maxLength":1000},"maxItems":100},{"type":"null"}]}}},"required":["values"]}}}},"security":[]}},"/stb-portal/invite":{"post":{"responses":{"200":{"description":"Einladungslink + Token (nur einmalig im Response)","content":{"application/json":{"schema":{"type":"object","properties":{"invitationId":{"type":"string","description":"Kennung der Einladung"},"email":{"type":"string","description":"Adresse des Steuerberaters"},"name":{"type":["string","null"],"description":"Name; null wenn keiner uebergeben wurde"},"kanzleiName":{"type":["string","null"],"description":"Kanzlei; null wenn keine uebergeben wurde"},"scope":{"type":"array","items":{"type":"string","enum":["susa","journal","datev_export","comments","period_close"]},"description":"Die freigeschalteten Bereiche der Einladung"},"expiresAt":{"type":"string","format":"date-time","description":"Ablauf der Einladung — 90 Tage ab Anlage"},"portalUrl":{"type":"string","description":"Fertiger Portal-Link mit Token und Mandanten-Slug"},"token":{"type":"string","description":"Das Klartext-Token — NUR in dieser einen Antwort. Gespeichert wird nur sein SHA-256-Abdruck"}},"required":["invitationId","email","name","kanzleiName","scope","expiresAt","portalUrl","token"]},"example":{"invitationId":"string","email":"string","name":"string","kanzleiName":"string","scope":["susa"],"expiresAt":"2026-01-01T12:00:00.000Z","portalUrl":"string","token":"string"}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nicht berechtigt"},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postStb-portalInvite","tags":["stb-portal"],"parameters":[],"summary":"Steuerberater einladen — Link und Token kommen genau einmal zurueck","description":"Legt eine Einladung an und gibt Portal-Link samt Klartext-Token GENAU EINMAL zurueck — gespeichert wird nur der SHA-256-Abdruck, ein spaeterer Abruf ist unmoeglich. Die Einladung laeuft nach 90 Tagen ab. `scope` bestimmt, was der Steuerberater darf; ohne Angabe gilt `susa`, `journal`, `datev_export` und `comments` — `period_close` also NICHT. Es wird keine E-Mail verschickt: den Link muss der Mandant selbst weitergeben. Mehrere Einladungen an dieselbe Adresse sind moeglich, das wird nicht geprueft. Der Vorgang wird protokolliert. Antwortet mit 200, nicht mit 201. Erfordert Admin-Rechte im Mandanten."}},"/stb-portal/auth":{"post":{"responses":{"200":{"description":"Sitzungstoken samt Mandant, Bereichen und Ablauf","content":{"application/json":{"schema":{"type":"object","properties":{"jwt":{"type":"string","description":"Das Sitzungstoken fuer alle weiteren Aufrufe"},"tenantId":{"type":"string","description":"Mandant, fuer den die Einladung gilt"},"tenantSlug":{"type":"string","description":"Slug des Mandanten; ersatzweise dessen Kennung"},"email":{"type":"string","description":"Adresse aus der Einladung"},"name":{"description":"Name aus der Einladung; null wenn keiner erfasst ist"},"kanzleiName":{"description":"Kanzlei aus der Einladung; null wenn keine erfasst ist"},"scope":{"type":"array","items":{"type":"string"},"description":"Freigeschaltete Bereiche; ohne gespeicherten Wert `susa` und `journal`"},"expiresAt":{"type":"string","format":"date-time","description":"Ablauf des Sitzungstokens — 8 Stunden ab jetzt"}},"required":["jwt","tenantId","tenantSlug","email","scope","expiresAt"]},"example":{"jwt":"string","tenantId":"string","tenantSlug":"string","email":"string","scope":["string"],"expiresAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Kein `token` im Rumpf","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"Token ungültig ODER abgelaufen — ununterscheidbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"429":{"description":"Zu viele Fehlversuche — 15 Minuten gesperrt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postStb-portalAuth","tags":["stb-portal"],"parameters":[],"summary":"Einladungs-Token gegen ein Sitzungstoken tauschen (8 Stunden gueltig)","description":"Tauscht das Klartext-Token der Einladung gegen ein Sitzungstoken, 8 Stunden gueltig. Der Aufruf braucht KEINE Anmeldung am ERP — das Token allein genuegt, wer es hat, kommt hinein. Vor der Pruefung greift eine Sperre: nach drei Fehlversuchen auf dasselbe Token kommt 15 Minuten lang nur noch 429. Ein unbekanntes UND ein abgelaufenes Token ergeben dieselbe 401. Bei Erfolg werden `last_login_at` und beim ersten Mal `accepted_at` gesetzt und der Zugriff samt IP protokolliert. Das Sitzungstoken laeuft ab, die Einladung selbst bleibt bis zu ihrem eigenen Ablauf gueltig und kann erneut eingetauscht werden.","security":[]}},"/stb-portal/me":{"get":{"responses":{"200":{"description":"Angaben zur laufenden Steuerberater-Sitzung. Ohne Datenbank faellt die Antwort auf die reinen Token-Angaben zurueck (dann fehlen tenantName, tenantSlug, email).","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"tenantName":{"type":["string","null"]},"email":{"type":"string"},"scope":{"type":"array","items":{"type":"string"}},"expiresAt":{"type":"string","format":"date-time"},"invitationId":{"type":"string"}},"required":["tenantId","scope","expiresAt"],"additionalProperties":false,"description":"Ohne Datenbank antwortet die Route verkuerzt — daher die optionalen Felder."},"example":{"tenantId":"string","tenantSlug":"string","tenantName":"string","email":"string","scope":["string"],"expiresAt":"2026-01-01T12:00:00.000Z","invitationId":"string"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getStb-portalMe","tags":["stb-portal"],"parameters":[],"summary":"Laufende Steuerberater-Sitzung anzeigen","description":"Gibt die Angaben zur laufenden Steuerberater-Sitzung zurueck, gelesen aus dem Sitzungstoken. Nur der Mandantenname wird nachgeschlagen; ist die Datenbank nicht erreichbar, kommt die verkuerzte Form mit `tenantId`, `scope` und `expiresAt` — dann FEHLEN `tenantName`, `tenantSlug`, `email` und `invitationId`. Es wird nicht geprueft, ob die zugrunde liegende Einladung noch gilt: ein gesperrtes Token bleibt bis zum Ablauf des Sitzungstokens gueltig."}},"/stb-portal/susa":{"get":{"responses":{"200":{"description":"Summen- und Saldenliste je Sachkonto. Betraege sind Zahlen (die Datenbank liefert NUMERIC als String — hier ist es bereits gewandelt). Stornobuchungen sind ausgenommen. Ohne Datenbank kommt `source: \"unavailable\"` mit leerer Liste.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"account_code":{"type":"string","description":"Sachkontonummer, z. B. „1200\""},"account_name":{"type":"string","description":"Bezeichnung des Sachkontos"},"soll":{"type":"number","description":"Summe der Sollbuchungen im Zeitraum"},"haben":{"type":"number","description":"Summe der Habenbuchungen im Zeitraum"},"saldo":{"type":"number","description":"Soll minus Haben"}},"required":["account_code","account_name","soll","haben","saldo"],"additionalProperties":false}},"period":{"type":"string","description":"„YYYY-MM\" oder „all\""},"source":{"type":"string","enum":["db","unavailable"]},"note":{"type":"string","description":"nur bei „unavailable\": warum"}},"required":["data","source"],"additionalProperties":false},"example":{"data":[{"account_code":"string","account_name":"string","soll":0,"haben":0,"saldo":0}],"period":"string","source":"db","note":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Scope „susa\" fehlt"}},"operationId":"getStb-portalSusa","tags":["stb-portal"],"parameters":[],"description":"Summen-und-Saldenliste je Sachkonto: Soll, Haben und Saldo aus `journal_entries`, gruppiert ueber `sachkonten`, aufsteigend nach Kontonummer und auf 1000 Konten begrenzt. Stornobuchungen bleiben ausgenommen, sonst zaehlte die Gegenbuchung doppelt. `?period=YYYY-MM` grenzt auf einen Monat ein; ohne oder bei abweichender Form wird der gesamte Bestand ausgewertet und `period` ist `all`. Konten ohne Bewegung erscheinen mit Nullen. Der Zugriff wird protokolliert.\n\nACHTUNG bei `source: \"unavailable\"`: dann wurde NICHT nachgesehen — eine leere `data` ist dort kein Beleg dafuer, dass es keine Buchungen gibt. Erfordert den Bereich `susa`.","summary":"Summen-und-Saldenliste je Sachkonto","x-nemix-summary-source":"description:first-sentence"}},"/stb-portal/journal":{"get":{"responses":{"200":{"description":"Buchungsjournal, neueste zuerst, 100 je Seite. Eine Zeile je Buchung mit Soll- und Habenkonto (doppische Form, wie im DATEV-Journal). Stornobuchungen bleiben sichtbar und sind ueber `is_storno` gekennzeichnet — geloescht wird nichts (GoBD).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"posting_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Buchungsdatum"},"document_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Belegdatum"},"posting_number":{"type":["string","null"],"description":"Buchungsnummer"},"account_debit":{"type":["string","null"],"description":"Sollkonto"},"account_credit":{"type":["string","null"],"description":"Habenkonto"},"amount":{"type":"number","description":"Buchungsbetrag in der Belegwaehrung"},"currency":{"type":"string","description":"ISO-4217, i. d. R. EUR"},"description":{"type":["string","null"],"description":"Buchungstext"},"reference":{"type":["string","null"],"description":"Belegnummer"},"is_storno":{"type":"boolean","description":"Stornobuchung — bleibt sichtbar (GoBD)"}},"required":["id","posting_date","document_date","posting_number","account_debit","account_credit","amount","currency","description","reference","is_storno"],"additionalProperties":false}},"page":{"type":"number"},"total":{"type":"number"},"pages":{"type":"number"},"source":{"type":"string","enum":["db","unavailable"]}},"required":["data","page","total","source"],"additionalProperties":false},"example":{"data":[{"id":"string","posting_date":"2026-01-01","document_date":"2026-01-01","posting_number":"string","account_debit":"string","account_credit":"string","amount":0,"currency":"string","description":"string","reference":"string","is_storno":true}],"page":0,"total":0,"pages":0,"source":"db"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Scope „journal\" fehlt"}},"operationId":"getStb-portalJournal","tags":["stb-portal"],"parameters":[],"description":"Buchungsjournal des Mandanten, neueste zuerst. Die Seitengroesse ist FEST auf 100 und laesst sich nicht setzen; geblaettert wird ueber `?page=` (ab 1), die Gesamtzahl und die Seitenzahl stehen in `total` und `pages`. `?from=` und `?to=` grenzen das Buchungsdatum ein (jeweils einschliesslich). Stornobuchungen bleiben SICHTBAR und sind ueber `is_storno` gekennzeichnet — geloescht wird nichts (GoBD). Der Zugriff wird protokolliert.\n\nACHTUNG bei `source: \"unavailable\"`: dann wurde NICHT nachgesehen, `total` ist 0 und `pages` fehlt — eine leere `data` ist dort kein Beleg dafuer, dass es keine Buchungen gibt. Erfordert den Bereich `journal`.","summary":"Buchungsjournal des Mandanten, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/stb-portal/datev-export":{"post":{"responses":{"200":{"description":"Das DATEV-Paket als Anhang, Dateiname `datev-stb-<Mandant>-<von>-<bis>.zip`","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Bereich `datev_export` fehlt"},"500":{"description":"Export fehlgeschlagen — `details` nennt den Grund","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postStb-portalDatev-export","tags":["stb-portal"],"parameters":[],"summary":"DATEV-Paket des Zeitraums als ZIP herunterladen (kein JSON-Rumpf)","description":"Erzeugt aus Rechnungen und Zahlungen des Zeitraums ein DATEV-Paket und liefert es als ZIP zum Herunterladen, kein JSON-Rumpf. Der Zeitraum steht in der QUERY, nicht im Rumpf: `?from=` (ohne Angabe der 1. Januar des laufenden Jahres) und `?to=` (ohne Angabe heute). Fest eingestellt sind Kontenrahmen SKR03, vierstellige Konten, EUR und UTF-8 (kein Windows-1252). Berater- und Mandantennummer stammen aus der Serverkonfiguration, nicht aus dem Aufruf. Trotz POST wird nichts gespeichert; der Vorgang wird protokolliert. Die Kopfzeile `X-DATEV-Invoice-Count` nennt die Zahl der einbezogenen Rechnungen. Erfordert den Bereich `datev_export`."}},"/stb-portal/comments":{"post":{"responses":{"201":{"description":"Kommentar gespeichert — nur die Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":["string","null"],"description":"Kennung des Kommentars; null wenn keine zurueckkam"},"ok":{"type":"boolean","const":true}},"required":["id","ok"]},"example":{"id":"string","ok":true}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Bereich `comments` fehlt"},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postStb-portalComments","tags":["stb-portal"],"parameters":[],"summary":"Kommentar zu Journaleintrag, Rechnung oder Periode hinterlegen","description":"Hinterlegt einen Kommentar zu einem Journaleintrag, einer Rechnung oder einer Periode (`targetType` je `journal_entry`, `invoice` oder `period`; hoechstens 5000 Zeichen). Ob es das benannte Objekt gibt, wird NICHT geprueft. Der Kommentar haengt an der Einladung, aus der er stammt, und der Vorgang wird protokolliert. Es wird niemand benachrichtigt. Zurueck kommt nur die Kennung, nicht der Kommentar. Erfordert den Bereich `comments`."},"get":{"responses":{"200":{"description":"Die Kommentare zum Objekt; leer auch dann, wenn nicht nachgesehen werden konnte","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Kommentars"},"comment":{"type":"string","description":"Der Text"},"target_type":{"type":"string","description":"journal_entry, invoice oder period"},"target_id":{"type":"string","description":"Kennung des kommentierten Objekts"},"created_at":{"type":"string","description":"Zeitpunkt"}},"required":["id","comment","target_type","target_id","created_at"]},"description":"Die Kommentare zum angefragten Objekt, aelteste zuerst; leer ohne Datenbank"}},"required":["data"]},"example":{"data":[{"id":"string","comment":"string","target_type":"string","target_id":"string","created_at":"string"}]}}}},"400":{"description":"`targetType` oder `targetId` fehlt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Bereich `comments` fehlt"}},"operationId":"getStb-portalComments","tags":["stb-portal"],"parameters":[],"description":"Liefert die Kommentare zu GENAU EINEM Objekt, aelteste zuerst. `targetType` und `targetId` sind in der Query Pflicht — ohne sie kommt 400, es gibt keinen Abruf „alle Kommentare\". Zurueck kommen ALLE Kommentare des Mandanten zu diesem Objekt, auch die anderer Steuerberater; der Verfasser steht NICHT dabei. Es wird nicht geblaettert.\n\nACHTUNG: ohne Datenbank kommt 200 mit leerer Liste und OHNE Kennzeichen — „keine Kommentare\" und „nicht nachgesehen\" sind hier nicht zu unterscheiden. Erfordert den Bereich `comments`.","summary":"Liefert die Kommentare zu GENAU EINEM Objekt, aelteste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/stb-portal/period-close":{"post":{"responses":{"200":{"description":"Periode geschlossen — oder es gab sie nicht; beides antwortet gleich","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"periodId":{"type":"string","description":"Die geschlossene Periode"},"closedBy":{"type":"string","description":"Wer geschlossen hat, in der Form `stb:<E-Mail>`"}},"required":["ok","periodId","closedBy"]},"example":{"ok":true,"periodId":"string","closedBy":"string"}}}},"400":{"description":"Validierungsfehler — `periodId` fehlt oder ist keine UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Bereich `period_close` fehlt"},"409":{"description":"Bereits geschlossen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postStb-portalPeriod-close","tags":["stb-portal"],"parameters":[],"summary":"Buchungsperiode schliessen — Bereich `period_close` erforderlich","description":"Setzt eine Buchungsperiode auf `closed` und vermerkt Zeitpunkt und Schliessenden in der Form `stb:<E-Mail>`. Eine bereits geschlossene Periode ergibt 409. ACHTUNG: eine UNBEKANNTE Kennung ergibt KEIN 404 — der Aufruf trifft keine Zeile, endet aber mit 200 und `ok: true`. Aus der Antwort laesst sich also nicht schliessen, dass wirklich eine Periode geschlossen wurde. Ein Zurueckoeffnen ist ueber diese Route nicht moeglich. Der Vorgang wird protokolliert. Erfordert den Bereich `period_close`, der in der Voreinstellung einer Einladung NICHT enthalten ist."}},"/stb-portal/elster-status":{"get":{"responses":{"200":{"description":"Die letzten 24 Laeufe; `source` sagt, ob wirklich nachgesehen wurde","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Laufs"},"jahr":{"description":"Jahr der Voranmeldung"},"quartal":{"description":"Quartal; leer bei monatlicher Abgabe"},"monat":{"description":"Monat; leer bei quartalsweiser Abgabe"},"status":{"description":"Zustand des Laufs"},"zahllast":{"description":"Zahllast — als NUMERIC aus der Datenbank, ungewandelt"},"eingereicht_am":{"description":"Zeitpunkt der Uebermittlung; leer solange nicht eingereicht"},"created_at":{"description":"Anlagezeitpunkt"}},"required":["id"]},"description":"Die letzten 24 Laeufe, neueste zuerst"},"source":{"type":"string","enum":["db","unavailable"],"description":"`unavailable` heisst: nicht nachgesehen, NICHT „keine Laeufe\""},"note":{"type":"string","description":"Nur bei `unavailable`: der Grund"}},"required":["data","source"]},"example":{"data":[{"id":"string"}],"source":"db","note":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getStb-portalElster-status","tags":["stb-portal"],"parameters":[],"summary":"Die letzten 24 Umsatzsteuer-Voranmeldungslaeufe des Mandanten","description":"Liefert die letzten 24 Umsatzsteuer-Voranmeldungslaeufe des Mandanten, neueste zuerst — mit Zeitraum, Zustand, Zahllast und Uebermittlungszeitpunkt. Es wird nicht gefiltert und nicht geblaettert; aeltere Laeufe sind ueber diese Route nicht erreichbar. Die Zahllast kommt roh aus der Datenbank und ist als NUMERIC eine Zeichenkette, KEINE Zahl.\n\nACHTUNG bei `source: \"unavailable\"`: dann wurde NICHT nachgesehen — eine leere `data` ist dort kein Beleg dafuer, dass es keine Laeufe gibt. Diese Route braucht KEINEN besonderen Bereich, ein gueltiges Sitzungstoken genuegt."}},"/stb-portal/maengel":{"get":{"responses":{"200":{"description":"Bis zu 100 ungeloeste Befunde; `source` sagt, ob wirklich nachgesehen wurde","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Befunds"},"check_type":{"description":"Welche Pruefung angeschlagen hat"},"severity":{"description":"Gewicht des Befunds"},"message":{"description":"Der Befund im Klartext"},"created_at":{"description":"Zeitpunkt der Pruefung"}},"required":["id"]},"description":"Hoechstens 100 UNGELOESTE Befunde, schwerste zuerst"},"count":{"type":"integer","minimum":0,"description":"Anzahl der ausgelieferten Befunde — hoechstens 100, nicht die Gesamtzahl"},"source":{"type":"string","enum":["db","unavailable"],"description":"`unavailable` heisst: nicht nachgesehen, NICHT „keine Maengel\""},"note":{"type":"string","description":"Nur bei `unavailable`: der Grund"}},"required":["data","count","source"]},"example":{"data":[{"id":"string"}],"count":0,"source":"db","note":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getStb-portalMaengel","tags":["stb-portal"],"parameters":[],"summary":"Ungeloeste Befunde der GoBD-Pruefungen, hoechstens 100","description":"Liefert die UNGELOESTEN Befunde der GoBD-/Audit-Pruefungen (ohne `resolved_at`), schwerste zuerst, auf 100 begrenzt. `count` ist die Zahl der AUSGELIEFERTEN Befunde — bei genau 100 kann es also mehr geben, eine Gesamtzahl gibt es nicht. Es wird nicht gefiltert und nicht geblaettert, und es werden keine Pruefungen ausgeloest: gelesen wird, was vorliegt.\n\nACHTUNG bei `source: \"unavailable\"`: dann wurde NICHT nachgesehen und `count` ist 0 — das ist kein Beleg dafuer, dass es keine Maengel gibt. Diese Route braucht KEINEN besonderen Bereich, ein gueltiges Sitzungstoken genuegt."}},"/stb-portal/revoke/{invitationId}":{"post":{"responses":{"200":{"description":"Gesperrt — offene Sitzungen laufen bis zu 8 Stunden weiter","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"invitationId":{"type":"string","description":"Die gesperrte Einladung"},"revokedBy":{"type":"string","description":"Wer gesperrt hat; `unknown` wenn kein Anwender im Kontext war"}},"required":["ok","invitationId","revokedBy"]},"example":{"ok":true,"invitationId":"string","revokedBy":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nicht berechtigt"},"404":{"description":"Keine Einladung mit dieser Kennung im eigenen Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postStb-portalRevokeByInvitationId","tags":["stb-portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"invitationId","required":true}],"summary":"Einladung sperren — offene Sitzungen laufen bis zu 8 Stunden weiter","description":"Sperrt eine Einladung, indem ihr Ablauf auf eine Sekunde in der Vergangenheit gesetzt wird — ein neuer Tausch von Token gegen Sitzungstoken ist damit sofort unmoeglich. ACHTUNG: BEREITS AUSGESTELLTE Sitzungstoken laufen weiter, bis zu acht Stunden lang; die Sperre wirkt nicht rueckwirkend auf eine offene Sitzung. Die Zeile bleibt bestehen und erscheint weiter in `GET /stb-portal/invitations`. Gesperrt wird nur im eigenen Mandanten; eine fremde oder unbekannte Kennung ergibt 404. Der Aufruf ist wiederholbar und wird protokolliert. Erfordert Admin-Rechte im Mandanten."}},"/stb-portal/invitations":{"get":{"responses":{"200":{"description":"Alle Einladungen des Mandanten; leer auch dann, wenn nicht nachgesehen werden konnte","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Einladung"},"email":{"description":"Adresse des Steuerberaters"},"name":{"description":"Name; leer wenn keiner erfasst ist"},"kanzlei_name":{"description":"Kanzlei; leer wenn keine erfasst ist"},"scope":{"description":"Freigeschaltete Bereiche"},"expires_at":{"description":"Ablauf; ein Zeitpunkt in der Vergangenheit heisst gesperrt oder abgelaufen"},"accepted_at":{"description":"Erste Anmeldung; leer solange nie angemeldet"},"last_login_at":{"description":"Letzte Anmeldung"},"created_at":{"description":"Anlagezeitpunkt"},"created_by":{"description":"Wer eingeladen hat"}},"required":["id"]},"description":"ALLE Einladungen des Mandanten, neueste zuerst — auch abgelaufene und gesperrte"}},"required":["data"]},"example":{"data":[{"id":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nicht berechtigt"}},"operationId":"getStb-portalInvitations","tags":["stb-portal"],"parameters":[],"description":"Listet ALLE Einladungen des Mandanten, neueste zuerst — auch abgelaufene und gesperrte. Ein Ablauf in der Vergangenheit heisst „nicht mehr einloesbar\"; ob das am Zeitablauf oder an einer Sperre lag, ist hier NICHT zu unterscheiden. Das Token erscheint nie, gespeichert ist nur sein Abdruck. `accepted_at` und `last_login_at` sind leer, solange nie angemeldet wurde. Es wird nicht gefiltert und nicht geblaettert.\n\nACHTUNG: ohne Datenbank kommt 200 mit leerer Liste und OHNE Kennzeichen — „keine Einladungen\" und „nicht nachgesehen\" sind hier nicht zu unterscheiden. Erfordert Admin-Rechte im Mandanten.","summary":"Listet ALLE Einladungen des Mandanten, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/stb-portal/invite":{"post":{"responses":{"200":{"description":"Einladungslink + Token (nur einmalig im Response)","content":{"application/json":{"schema":{"type":"object","properties":{"invitationId":{"type":"string","description":"Kennung der Einladung"},"email":{"type":"string","description":"Adresse des Steuerberaters"},"name":{"type":["string","null"],"description":"Name; null wenn keiner uebergeben wurde"},"kanzleiName":{"type":["string","null"],"description":"Kanzlei; null wenn keine uebergeben wurde"},"scope":{"type":"array","items":{"type":"string","enum":["susa","journal","datev_export","comments","period_close"]},"description":"Die freigeschalteten Bereiche der Einladung"},"expiresAt":{"type":"string","format":"date-time","description":"Ablauf der Einladung — 90 Tage ab Anlage"},"portalUrl":{"type":"string","description":"Fertiger Portal-Link mit Token und Mandanten-Slug"},"token":{"type":"string","description":"Das Klartext-Token — NUR in dieser einen Antwort. Gespeichert wird nur sein SHA-256-Abdruck"}},"required":["invitationId","email","name","kanzleiName","scope","expiresAt","portalUrl","token"]},"example":{"invitationId":"string","email":"string","name":"string","kanzleiName":"string","scope":["susa"],"expiresAt":"2026-01-01T12:00:00.000Z","portalUrl":"string","token":"string"}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nicht berechtigt"},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postApiV1Stb-portalInvite","tags":["stb-portal"],"parameters":[],"summary":"Steuerberater einladen — Link und Token kommen genau einmal zurueck","description":"Legt eine Einladung an und gibt Portal-Link samt Klartext-Token GENAU EINMAL zurueck — gespeichert wird nur der SHA-256-Abdruck, ein spaeterer Abruf ist unmoeglich. Die Einladung laeuft nach 90 Tagen ab. `scope` bestimmt, was der Steuerberater darf; ohne Angabe gilt `susa`, `journal`, `datev_export` und `comments` — `period_close` also NICHT. Es wird keine E-Mail verschickt: den Link muss der Mandant selbst weitergeben. Mehrere Einladungen an dieselbe Adresse sind moeglich, das wird nicht geprueft. Der Vorgang wird protokolliert. Antwortet mit 200, nicht mit 201. Erfordert Admin-Rechte im Mandanten."}},"/api/v1/stb-portal/auth":{"post":{"responses":{"200":{"description":"Sitzungstoken samt Mandant, Bereichen und Ablauf","content":{"application/json":{"schema":{"type":"object","properties":{"jwt":{"type":"string","description":"Das Sitzungstoken fuer alle weiteren Aufrufe"},"tenantId":{"type":"string","description":"Mandant, fuer den die Einladung gilt"},"tenantSlug":{"type":"string","description":"Slug des Mandanten; ersatzweise dessen Kennung"},"email":{"type":"string","description":"Adresse aus der Einladung"},"name":{"description":"Name aus der Einladung; null wenn keiner erfasst ist"},"kanzleiName":{"description":"Kanzlei aus der Einladung; null wenn keine erfasst ist"},"scope":{"type":"array","items":{"type":"string"},"description":"Freigeschaltete Bereiche; ohne gespeicherten Wert `susa` und `journal`"},"expiresAt":{"type":"string","format":"date-time","description":"Ablauf des Sitzungstokens — 8 Stunden ab jetzt"}},"required":["jwt","tenantId","tenantSlug","email","scope","expiresAt"]},"example":{"jwt":"string","tenantId":"string","tenantSlug":"string","email":"string","scope":["string"],"expiresAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Kein `token` im Rumpf","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"Token ungültig ODER abgelaufen — ununterscheidbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"429":{"description":"Zu viele Fehlversuche — 15 Minuten gesperrt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postApiV1Stb-portalAuth","tags":["stb-portal"],"parameters":[],"summary":"Einladungs-Token gegen ein Sitzungstoken tauschen (8 Stunden gueltig)","description":"Tauscht das Klartext-Token der Einladung gegen ein Sitzungstoken, 8 Stunden gueltig. Der Aufruf braucht KEINE Anmeldung am ERP — das Token allein genuegt, wer es hat, kommt hinein. Vor der Pruefung greift eine Sperre: nach drei Fehlversuchen auf dasselbe Token kommt 15 Minuten lang nur noch 429. Ein unbekanntes UND ein abgelaufenes Token ergeben dieselbe 401. Bei Erfolg werden `last_login_at` und beim ersten Mal `accepted_at` gesetzt und der Zugriff samt IP protokolliert. Das Sitzungstoken laeuft ab, die Einladung selbst bleibt bis zu ihrem eigenen Ablauf gueltig und kann erneut eingetauscht werden.","security":[]}},"/api/v1/stb-portal/me":{"get":{"responses":{"200":{"description":"Angaben zur laufenden Steuerberater-Sitzung. Ohne Datenbank faellt die Antwort auf die reinen Token-Angaben zurueck (dann fehlen tenantName, tenantSlug, email).","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"tenantName":{"type":["string","null"]},"email":{"type":"string"},"scope":{"type":"array","items":{"type":"string"}},"expiresAt":{"type":"string","format":"date-time"},"invitationId":{"type":"string"}},"required":["tenantId","scope","expiresAt"],"additionalProperties":false,"description":"Ohne Datenbank antwortet die Route verkuerzt — daher die optionalen Felder."},"example":{"tenantId":"string","tenantSlug":"string","tenantName":"string","email":"string","scope":["string"],"expiresAt":"2026-01-01T12:00:00.000Z","invitationId":"string"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Stb-portalMe","tags":["stb-portal"],"parameters":[],"summary":"Laufende Steuerberater-Sitzung anzeigen","description":"Gibt die Angaben zur laufenden Steuerberater-Sitzung zurueck, gelesen aus dem Sitzungstoken. Nur der Mandantenname wird nachgeschlagen; ist die Datenbank nicht erreichbar, kommt die verkuerzte Form mit `tenantId`, `scope` und `expiresAt` — dann FEHLEN `tenantName`, `tenantSlug`, `email` und `invitationId`. Es wird nicht geprueft, ob die zugrunde liegende Einladung noch gilt: ein gesperrtes Token bleibt bis zum Ablauf des Sitzungstokens gueltig."}},"/api/v1/stb-portal/susa":{"get":{"responses":{"200":{"description":"Summen- und Saldenliste je Sachkonto. Betraege sind Zahlen (die Datenbank liefert NUMERIC als String — hier ist es bereits gewandelt). Stornobuchungen sind ausgenommen. Ohne Datenbank kommt `source: \"unavailable\"` mit leerer Liste.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"account_code":{"type":"string","description":"Sachkontonummer, z. B. „1200\""},"account_name":{"type":"string","description":"Bezeichnung des Sachkontos"},"soll":{"type":"number","description":"Summe der Sollbuchungen im Zeitraum"},"haben":{"type":"number","description":"Summe der Habenbuchungen im Zeitraum"},"saldo":{"type":"number","description":"Soll minus Haben"}},"required":["account_code","account_name","soll","haben","saldo"],"additionalProperties":false}},"period":{"type":"string","description":"„YYYY-MM\" oder „all\""},"source":{"type":"string","enum":["db","unavailable"]},"note":{"type":"string","description":"nur bei „unavailable\": warum"}},"required":["data","source"],"additionalProperties":false},"example":{"data":[{"account_code":"string","account_name":"string","soll":0,"haben":0,"saldo":0}],"period":"string","source":"db","note":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Scope „susa\" fehlt"}},"operationId":"getApiV1Stb-portalSusa","tags":["stb-portal"],"parameters":[],"description":"Summen-und-Saldenliste je Sachkonto: Soll, Haben und Saldo aus `journal_entries`, gruppiert ueber `sachkonten`, aufsteigend nach Kontonummer und auf 1000 Konten begrenzt. Stornobuchungen bleiben ausgenommen, sonst zaehlte die Gegenbuchung doppelt. `?period=YYYY-MM` grenzt auf einen Monat ein; ohne oder bei abweichender Form wird der gesamte Bestand ausgewertet und `period` ist `all`. Konten ohne Bewegung erscheinen mit Nullen. Der Zugriff wird protokolliert.\n\nACHTUNG bei `source: \"unavailable\"`: dann wurde NICHT nachgesehen — eine leere `data` ist dort kein Beleg dafuer, dass es keine Buchungen gibt. Erfordert den Bereich `susa`.","summary":"Summen-und-Saldenliste je Sachkonto","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/stb-portal/journal":{"get":{"responses":{"200":{"description":"Buchungsjournal, neueste zuerst, 100 je Seite. Eine Zeile je Buchung mit Soll- und Habenkonto (doppische Form, wie im DATEV-Journal). Stornobuchungen bleiben sichtbar und sind ueber `is_storno` gekennzeichnet — geloescht wird nichts (GoBD).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"posting_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Buchungsdatum"},"document_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Belegdatum"},"posting_number":{"type":["string","null"],"description":"Buchungsnummer"},"account_debit":{"type":["string","null"],"description":"Sollkonto"},"account_credit":{"type":["string","null"],"description":"Habenkonto"},"amount":{"type":"number","description":"Buchungsbetrag in der Belegwaehrung"},"currency":{"type":"string","description":"ISO-4217, i. d. R. EUR"},"description":{"type":["string","null"],"description":"Buchungstext"},"reference":{"type":["string","null"],"description":"Belegnummer"},"is_storno":{"type":"boolean","description":"Stornobuchung — bleibt sichtbar (GoBD)"}},"required":["id","posting_date","document_date","posting_number","account_debit","account_credit","amount","currency","description","reference","is_storno"],"additionalProperties":false}},"page":{"type":"number"},"total":{"type":"number"},"pages":{"type":"number"},"source":{"type":"string","enum":["db","unavailable"]}},"required":["data","page","total","source"],"additionalProperties":false},"example":{"data":[{"id":"string","posting_date":"2026-01-01","document_date":"2026-01-01","posting_number":"string","account_debit":"string","account_credit":"string","amount":0,"currency":"string","description":"string","reference":"string","is_storno":true}],"page":0,"total":0,"pages":0,"source":"db"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Scope „journal\" fehlt"}},"operationId":"getApiV1Stb-portalJournal","tags":["stb-portal"],"parameters":[],"description":"Buchungsjournal des Mandanten, neueste zuerst. Die Seitengroesse ist FEST auf 100 und laesst sich nicht setzen; geblaettert wird ueber `?page=` (ab 1), die Gesamtzahl und die Seitenzahl stehen in `total` und `pages`. `?from=` und `?to=` grenzen das Buchungsdatum ein (jeweils einschliesslich). Stornobuchungen bleiben SICHTBAR und sind ueber `is_storno` gekennzeichnet — geloescht wird nichts (GoBD). Der Zugriff wird protokolliert.\n\nACHTUNG bei `source: \"unavailable\"`: dann wurde NICHT nachgesehen, `total` ist 0 und `pages` fehlt — eine leere `data` ist dort kein Beleg dafuer, dass es keine Buchungen gibt. Erfordert den Bereich `journal`.","summary":"Buchungsjournal des Mandanten, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/stb-portal/datev-export":{"post":{"responses":{"200":{"description":"Das DATEV-Paket als Anhang, Dateiname `datev-stb-<Mandant>-<von>-<bis>.zip`","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Bereich `datev_export` fehlt"},"500":{"description":"Export fehlgeschlagen — `details` nennt den Grund","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postApiV1Stb-portalDatev-export","tags":["stb-portal"],"parameters":[],"summary":"DATEV-Paket des Zeitraums als ZIP herunterladen (kein JSON-Rumpf)","description":"Erzeugt aus Rechnungen und Zahlungen des Zeitraums ein DATEV-Paket und liefert es als ZIP zum Herunterladen, kein JSON-Rumpf. Der Zeitraum steht in der QUERY, nicht im Rumpf: `?from=` (ohne Angabe der 1. Januar des laufenden Jahres) und `?to=` (ohne Angabe heute). Fest eingestellt sind Kontenrahmen SKR03, vierstellige Konten, EUR und UTF-8 (kein Windows-1252). Berater- und Mandantennummer stammen aus der Serverkonfiguration, nicht aus dem Aufruf. Trotz POST wird nichts gespeichert; der Vorgang wird protokolliert. Die Kopfzeile `X-DATEV-Invoice-Count` nennt die Zahl der einbezogenen Rechnungen. Erfordert den Bereich `datev_export`."}},"/api/v1/stb-portal/comments":{"post":{"responses":{"201":{"description":"Kommentar gespeichert — nur die Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":["string","null"],"description":"Kennung des Kommentars; null wenn keine zurueckkam"},"ok":{"type":"boolean","const":true}},"required":["id","ok"]},"example":{"id":"string","ok":true}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Bereich `comments` fehlt"},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postApiV1Stb-portalComments","tags":["stb-portal"],"parameters":[],"summary":"Kommentar zu Journaleintrag, Rechnung oder Periode hinterlegen","description":"Hinterlegt einen Kommentar zu einem Journaleintrag, einer Rechnung oder einer Periode (`targetType` je `journal_entry`, `invoice` oder `period`; hoechstens 5000 Zeichen). Ob es das benannte Objekt gibt, wird NICHT geprueft. Der Kommentar haengt an der Einladung, aus der er stammt, und der Vorgang wird protokolliert. Es wird niemand benachrichtigt. Zurueck kommt nur die Kennung, nicht der Kommentar. Erfordert den Bereich `comments`."},"get":{"responses":{"200":{"description":"Die Kommentare zum Objekt; leer auch dann, wenn nicht nachgesehen werden konnte","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Kommentars"},"comment":{"type":"string","description":"Der Text"},"target_type":{"type":"string","description":"journal_entry, invoice oder period"},"target_id":{"type":"string","description":"Kennung des kommentierten Objekts"},"created_at":{"type":"string","description":"Zeitpunkt"}},"required":["id","comment","target_type","target_id","created_at"]},"description":"Die Kommentare zum angefragten Objekt, aelteste zuerst; leer ohne Datenbank"}},"required":["data"]},"example":{"data":[{"id":"string","comment":"string","target_type":"string","target_id":"string","created_at":"string"}]}}}},"400":{"description":"`targetType` oder `targetId` fehlt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Bereich `comments` fehlt"}},"operationId":"getApiV1Stb-portalComments","tags":["stb-portal"],"parameters":[],"description":"Liefert die Kommentare zu GENAU EINEM Objekt, aelteste zuerst. `targetType` und `targetId` sind in der Query Pflicht — ohne sie kommt 400, es gibt keinen Abruf „alle Kommentare\". Zurueck kommen ALLE Kommentare des Mandanten zu diesem Objekt, auch die anderer Steuerberater; der Verfasser steht NICHT dabei. Es wird nicht geblaettert.\n\nACHTUNG: ohne Datenbank kommt 200 mit leerer Liste und OHNE Kennzeichen — „keine Kommentare\" und „nicht nachgesehen\" sind hier nicht zu unterscheiden. Erfordert den Bereich `comments`.","summary":"Liefert die Kommentare zu GENAU EINEM Objekt, aelteste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/stb-portal/period-close":{"post":{"responses":{"200":{"description":"Periode geschlossen — oder es gab sie nicht; beides antwortet gleich","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"periodId":{"type":"string","description":"Die geschlossene Periode"},"closedBy":{"type":"string","description":"Wer geschlossen hat, in der Form `stb:<E-Mail>`"}},"required":["ok","periodId","closedBy"]},"example":{"ok":true,"periodId":"string","closedBy":"string"}}}},"400":{"description":"Validierungsfehler — `periodId` fehlt oder ist keine UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Bereich `period_close` fehlt"},"409":{"description":"Bereits geschlossen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postApiV1Stb-portalPeriod-close","tags":["stb-portal"],"parameters":[],"summary":"Buchungsperiode schliessen — Bereich `period_close` erforderlich","description":"Setzt eine Buchungsperiode auf `closed` und vermerkt Zeitpunkt und Schliessenden in der Form `stb:<E-Mail>`. Eine bereits geschlossene Periode ergibt 409. ACHTUNG: eine UNBEKANNTE Kennung ergibt KEIN 404 — der Aufruf trifft keine Zeile, endet aber mit 200 und `ok: true`. Aus der Antwort laesst sich also nicht schliessen, dass wirklich eine Periode geschlossen wurde. Ein Zurueckoeffnen ist ueber diese Route nicht moeglich. Der Vorgang wird protokolliert. Erfordert den Bereich `period_close`, der in der Voreinstellung einer Einladung NICHT enthalten ist."}},"/api/v1/stb-portal/elster-status":{"get":{"responses":{"200":{"description":"Die letzten 24 Laeufe; `source` sagt, ob wirklich nachgesehen wurde","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Laufs"},"jahr":{"description":"Jahr der Voranmeldung"},"quartal":{"description":"Quartal; leer bei monatlicher Abgabe"},"monat":{"description":"Monat; leer bei quartalsweiser Abgabe"},"status":{"description":"Zustand des Laufs"},"zahllast":{"description":"Zahllast — als NUMERIC aus der Datenbank, ungewandelt"},"eingereicht_am":{"description":"Zeitpunkt der Uebermittlung; leer solange nicht eingereicht"},"created_at":{"description":"Anlagezeitpunkt"}},"required":["id"]},"description":"Die letzten 24 Laeufe, neueste zuerst"},"source":{"type":"string","enum":["db","unavailable"],"description":"`unavailable` heisst: nicht nachgesehen, NICHT „keine Laeufe\""},"note":{"type":"string","description":"Nur bei `unavailable`: der Grund"}},"required":["data","source"]},"example":{"data":[{"id":"string"}],"source":"db","note":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Stb-portalElster-status","tags":["stb-portal"],"parameters":[],"summary":"Die letzten 24 Umsatzsteuer-Voranmeldungslaeufe des Mandanten","description":"Liefert die letzten 24 Umsatzsteuer-Voranmeldungslaeufe des Mandanten, neueste zuerst — mit Zeitraum, Zustand, Zahllast und Uebermittlungszeitpunkt. Es wird nicht gefiltert und nicht geblaettert; aeltere Laeufe sind ueber diese Route nicht erreichbar. Die Zahllast kommt roh aus der Datenbank und ist als NUMERIC eine Zeichenkette, KEINE Zahl.\n\nACHTUNG bei `source: \"unavailable\"`: dann wurde NICHT nachgesehen — eine leere `data` ist dort kein Beleg dafuer, dass es keine Laeufe gibt. Diese Route braucht KEINEN besonderen Bereich, ein gueltiges Sitzungstoken genuegt."}},"/api/v1/stb-portal/maengel":{"get":{"responses":{"200":{"description":"Bis zu 100 ungeloeste Befunde; `source` sagt, ob wirklich nachgesehen wurde","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Befunds"},"check_type":{"description":"Welche Pruefung angeschlagen hat"},"severity":{"description":"Gewicht des Befunds"},"message":{"description":"Der Befund im Klartext"},"created_at":{"description":"Zeitpunkt der Pruefung"}},"required":["id"]},"description":"Hoechstens 100 UNGELOESTE Befunde, schwerste zuerst"},"count":{"type":"integer","minimum":0,"description":"Anzahl der ausgelieferten Befunde — hoechstens 100, nicht die Gesamtzahl"},"source":{"type":"string","enum":["db","unavailable"],"description":"`unavailable` heisst: nicht nachgesehen, NICHT „keine Maengel\""},"note":{"type":"string","description":"Nur bei `unavailable`: der Grund"}},"required":["data","count","source"]},"example":{"data":[{"id":"string"}],"count":0,"source":"db","note":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Stb-portalMaengel","tags":["stb-portal"],"parameters":[],"summary":"Ungeloeste Befunde der GoBD-Pruefungen, hoechstens 100","description":"Liefert die UNGELOESTEN Befunde der GoBD-/Audit-Pruefungen (ohne `resolved_at`), schwerste zuerst, auf 100 begrenzt. `count` ist die Zahl der AUSGELIEFERTEN Befunde — bei genau 100 kann es also mehr geben, eine Gesamtzahl gibt es nicht. Es wird nicht gefiltert und nicht geblaettert, und es werden keine Pruefungen ausgeloest: gelesen wird, was vorliegt.\n\nACHTUNG bei `source: \"unavailable\"`: dann wurde NICHT nachgesehen und `count` ist 0 — das ist kein Beleg dafuer, dass es keine Maengel gibt. Diese Route braucht KEINEN besonderen Bereich, ein gueltiges Sitzungstoken genuegt."}},"/api/v1/stb-portal/revoke/{invitationId}":{"post":{"responses":{"200":{"description":"Gesperrt — offene Sitzungen laufen bis zu 8 Stunden weiter","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"invitationId":{"type":"string","description":"Die gesperrte Einladung"},"revokedBy":{"type":"string","description":"Wer gesperrt hat; `unknown` wenn kein Anwender im Kontext war"}},"required":["ok","invitationId","revokedBy"]},"example":{"ok":true,"invitationId":"string","revokedBy":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nicht berechtigt"},"404":{"description":"Keine Einladung mit dieser Kennung im eigenen Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlermeldung im Klartext"},"details":{"description":"Einzelheiten der Pruefung, sofern vorhanden"}},"required":["error"]}}}}},"operationId":"postApiV1Stb-portalRevokeByInvitationId","tags":["stb-portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"invitationId","required":true}],"summary":"Einladung sperren — offene Sitzungen laufen bis zu 8 Stunden weiter","description":"Sperrt eine Einladung, indem ihr Ablauf auf eine Sekunde in der Vergangenheit gesetzt wird — ein neuer Tausch von Token gegen Sitzungstoken ist damit sofort unmoeglich. ACHTUNG: BEREITS AUSGESTELLTE Sitzungstoken laufen weiter, bis zu acht Stunden lang; die Sperre wirkt nicht rueckwirkend auf eine offene Sitzung. Die Zeile bleibt bestehen und erscheint weiter in `GET /stb-portal/invitations`. Gesperrt wird nur im eigenen Mandanten; eine fremde oder unbekannte Kennung ergibt 404. Der Aufruf ist wiederholbar und wird protokolliert. Erfordert Admin-Rechte im Mandanten."}},"/api/v1/stb-portal/invitations":{"get":{"responses":{"200":{"description":"Alle Einladungen des Mandanten; leer auch dann, wenn nicht nachgesehen werden konnte","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Einladung"},"email":{"description":"Adresse des Steuerberaters"},"name":{"description":"Name; leer wenn keiner erfasst ist"},"kanzlei_name":{"description":"Kanzlei; leer wenn keine erfasst ist"},"scope":{"description":"Freigeschaltete Bereiche"},"expires_at":{"description":"Ablauf; ein Zeitpunkt in der Vergangenheit heisst gesperrt oder abgelaufen"},"accepted_at":{"description":"Erste Anmeldung; leer solange nie angemeldet"},"last_login_at":{"description":"Letzte Anmeldung"},"created_at":{"description":"Anlagezeitpunkt"},"created_by":{"description":"Wer eingeladen hat"}},"required":["id"]},"description":"ALLE Einladungen des Mandanten, neueste zuerst — auch abgelaufene und gesperrte"}},"required":["data"]},"example":{"data":[{"id":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nicht berechtigt"}},"operationId":"getApiV1Stb-portalInvitations","tags":["stb-portal"],"parameters":[],"description":"Listet ALLE Einladungen des Mandanten, neueste zuerst — auch abgelaufene und gesperrte. Ein Ablauf in der Vergangenheit heisst „nicht mehr einloesbar\"; ob das am Zeitablauf oder an einer Sperre lag, ist hier NICHT zu unterscheiden. Das Token erscheint nie, gespeichert ist nur sein Abdruck. `accepted_at` und `last_login_at` sind leer, solange nie angemeldet wurde. Es wird nicht gefiltert und nicht geblaettert.\n\nACHTUNG: ohne Datenbank kommt 200 mit leerer Liste und OHNE Kennzeichen — „keine Einladungen\" und „nicht nachgesehen\" sind hier nicht zu unterscheiden. Erfordert Admin-Rechte im Mandanten.","summary":"Listet ALLE Einladungen des Mandanten, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/voice/transcribe":{"post":{"responses":{"200":{"description":"Erkannter Text plus `requires_confirmation: true`.","content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","description":"Der erkannte Text, unveraendert vom Dienst"},"requires_confirmation":{"type":"boolean","const":true,"description":"IMMER true — kein Zustand, sondern eine feste Auflage: den Text erst dem Menschen zeigen, bevor er an einen Aktions-Endpunkt geht."}},"required":["text","requires_confirmation"]},"example":{"text":"string","requires_confirmation":true}}}},"400":{"description":"`audio_required` — im Formular fehlt das Feld `audio`."},"401":{"description":"Keine gueltige Sitzung oder kein Mandanten-Kontext."},"429":{"description":"Rate-Grenze fuer `/api/v1/*` erreicht."},"500":{"description":"Transkription fehlgeschlagen; `error` traegt die Meldung des Dienstes. Auch ein Rumpf, der kein gueltiges `multipart/form-data` ist, landet hier."},"503":{"description":"`no_transcription_provider_configured` — kein `OPENAI_API_KEY` gesetzt."}},"operationId":"postApiV1VoiceTranscribe","tags":["voice"],"parameters":[],"summary":"Sprachaufnahme in Text umwandeln","description":"Nimmt eine Audiodatei als `multipart/form-data`-Feld `audio` entgegen und\ngibt AUSSCHLIESSLICH den erkannten Text zurueck. Die Route ruft kein\nKI-Werkzeug auf, aendert keine Daten und reicht den Text nirgendwo weiter.\n\n`requires_confirmation` ist immer `true` — das ist kein Zustand, sondern ein\nfester Hinweis an den Aufrufer: den Text erst dem Menschen zeigen und\nbestaetigen lassen, bevor er an `/api/v1/ai/chat` oder einen Aktions-\nEndpunkt geht. Sonst koennte eine Aufnahme („ignoriere alle Anweisungen …\")\nunbemerkt eine Aktion ausloesen.\n\nErkannt wird ueber OpenAI Whisper; ohne gesetzten `OPENAI_API_KEY`\nantwortet die Route 503. Die verbrauchten Sprachminuten werden dem\nMandanten angerechnet (angefangene Minute zaehlt voll). Scheitert das\nZaehlen, bleibt die Transkription trotzdem erfolgreich.\n\nErfordert eine angemeldete Mitarbeiter-Sitzung (Cookie oder API-Key) und\neinen Mandanten-Kontext."}},"/api/v1/voice/synthesize":{"post":{"responses":{"200":{"description":"Bestaetigung OHNE Audio. Es gibt keine Datei, keine Adresse und keinen Auftrag — `status: \"ok\"` besagt nur, dass `text` gesetzt war.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"ok","description":"Bedeutet NICHT, dass Audio entstanden ist"},"message":{"type":"string","const":"TTS scheduled","description":"Fester Text; es wurde nichts eingeplant"},"voice":{"type":"string","description":"Die angefragte Stimme, unveraendert zurueckgespiegelt"}},"required":["status","message","voice"]},"example":{"status":"ok","message":"TTS scheduled","voice":"string"}}}},"400":{"description":"`text_required` — `text` fehlt oder ist leer."},"401":{"description":"Keine gueltige Sitzung oder kein Mandanten-Kontext."},"429":{"description":"Rate-Grenze fuer `/api/v1/*` erreicht."},"500":{"description":"`synthesis_failed`. Trifft auch einen Rumpf, der kein gueltiges JSON ist — der Parse-Fehler faellt in denselben catch und wird nicht als 400 gemeldet."}},"operationId":"postApiV1VoiceSynthesize","tags":["voice"],"parameters":[],"summary":"Platzhalter — erzeugt KEIN Audio","description":"ACHTUNG: Diese Route synthetisiert nichts. Sie prueft nur, ob `text`\ngesetzt ist, und antwortet mit einer Bestaetigung. Es entsteht keine\nAudiodatei, keine URL und kein Auftrag — trotz `status: \"ok\"` und der\nMeldung „TTS scheduled\" im Rumpf. Auch `voice` wird nur zurueckgespiegelt.\n\nWer echte Sprachausgabe braucht, nimmt `POST /api/v1/ai/tts`: die Route\nliefert eine MP3 im Datenstrom und rechnet die erzeugte Laenge ab.\n\nErfordert eine angemeldete Mitarbeiter-Sitzung (Cookie oder API-Key) und\neinen Mandanten-Kontext."}},"/api/v1/rag-query/{id}/query":{"post":{"responses":{"200":{"description":"Die K naechsten Abschnitte, absteigend nach Aehnlichkeit. Ohne Relevanzschwelle.","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"content":{"type":"string"},"score":{"type":"number"},"metadata":{"type":"object","additionalProperties":{}}},"required":["content","score","metadata"],"additionalProperties":false}},"count":{"type":"number"}},"required":["results","count"],"additionalProperties":false},"example":{"results":[{"content":"string","score":0,"metadata":{}}],"count":0}}}},"400":{"description":"`empty_query` — die Frage besteht nur aus Leerzeichen. Zusaetzlich, wenn `query` fehlt, leer oder laenger als 8000 Zeichen ist, oder `topK` ausserhalb 1 bis 50 liegt."},"401":{"description":"`unauthorized` — kein API-Schluessel geschickt, oder Schluessel und Sammlung passen nicht zusammen."},"429":{"description":"`rate_limited` — zu viele Anfragen fuer diesen Schluessel. Die Wartezeit steht im Kopf `Retry-After` und noch einmal als `retryAfter` im Rumpf."},"503":{"description":"`database_unavailable` — siehe Vorbehalt in der Beschreibung."}},"operationId":"postApiV1Rag-queryByIdQuery","tags":["rag-query"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Semantische Suche in einer RAG-Sammlung (Top-K, API-Schluessel)","description":"Sucht die semantisch naechsten Abschnitte einer Sammlung. Gedacht fuer\nn8n und ERP-Agenten.\n\nANMELDUNG PER API-SCHLUESSEL, NICHT PER SITZUNG. Der Schluessel steht\nin `Authorization: Bearer <key>` oder in `x-api-key`. Der Mandant wird\nNIE aus dem Rumpf uebernommen, sondern ueber den Schluessel aus der\nSammlung aufgeloest. Schluessel und `:id` muessen zur selben aktiven\nSammlung gehoeren.\n\nES GIBT KEINE RELEVANZ-UNTERGRENZE.\nDie Antwort enthaelt immer die K naechsten Abschnitte, egal wie fern\nsie sind. Eine voellig unpassende Frage liefert deshalb genauso viele\nTreffer wie eine passende, nur mit niedrigem `score`. Eine nicht leere\nListe ist also KEIN Beleg dafuer, dass etwas gefunden wurde — wer eine\nSchwelle braucht, zieht sie selbst gegen `score`. Leer wird die Liste\nnur, wenn die Sammlung keine eingebetteten Abschnitte hat.\n\nDIE DROSSEL IST FAIL-OPEN. Sie zaehlt pro Schluessel und laeuft VOR dem\nkostenpflichtigen Einbetten. Faellt sie selbst aus, wird das protokolliert\nund durchgelassen. Ein ausbleibender 429 beweist deshalb nicht, dass man\nunter der Grenze liegt.\n\n`topK` liegt standardmaessig bei 5 und darf 1 bis 50 sein.\n\nDer 401 fasst zwei Faelle zusammen: kein Schluessel geschickt, ODER\nSchluessel und Sammlung passen nicht zusammen beziehungsweise die\nSammlung ist abgeschaltet. Die Trennung bliebe sonst ein Werkzeug, um\ngueltige Sammlungs-Kennungen zu erraten.\n\nDer 503 traegt `database_unavailable`, faengt aber JEDEN unerwarteten\nFehler dieses Pfades ab, auch einen des Einbettungsdienstes. Der Name\nbenennt den haeufigsten Fall, nicht zwingend den vorliegenden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","minLength":1,"maxLength":8000},"topK":{"type":"integer","minimum":1,"maximum":50}},"required":["query"]},"example":{"query":"string","topK":1}}}},"security":[]}},"/api/v1/entwickler-paket/einloesen":{"post":{"responses":{"200":{"description":"Redeemed — body carries the key, shown here and nowhere else","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string","description":"The raw API key. Shown exactly once, never recoverable afterwards"},"prefix":{"type":"string","description":"First characters of the key — safe to display and to store"},"scopes":{"type":"array","items":{"type":"string"},"description":"Fixed at download time and capped against the downloader — this call cannot widen them"},"tenantSlug":{"type":"string"},"expiresAt":{"type":"string","format":"date-time","description":"Expiry of the KEY, not of the voucher"},"hinweis":{"type":"string","description":"German one-liner for the customer; the only German text in this answer"}},"required":["key","prefix","scopes","tenantSlug","expiresAt","hinweis"]},"example":{"key":"string","prefix":"string","scopes":["string"],"tenantSlug":"string","expiresAt":"2026-01-01T12:00:00.000Z","hinweis":"string"}}}},"400":{"description":"Malformed body"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"No such code"},"410":{"description":"Code expired (VOUCHER_EXPIRED) or already redeemed (VOUCHER_ALREADY_REDEEMED, with the redemption timestamp)"},"429":{"description":"Too many attempts from this address"},"503":{"description":"Database unavailable — retry after the given number of seconds"}},"operationId":"postApiV1Entwickler-paketEinloesen","tags":["developer"],"parameters":[],"summary":"Redeem a developer-package voucher for an API key","description":"Trades the one-shot code shipped in the developer package for a real API key. Deliberately UNAUTHENTICATED: the caller is the customer's own AI, which has no credential yet — obtaining one is the point. The key is returned exactly once and is never recoverable afterwards. Its scopes were fixed when the package was downloaded and capped against the rights of the person who downloaded it; this endpoint cannot widen them. A code is valid for 30 minutes and can be redeemed once. Expired and already-redeemed answer differently on purpose: the second means somebody else held the archive, and the response carries the timestamp.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":200}},"required":["code"]},"example":{"code":"string"}}}}}},"/api-docs":{"get":{"responses":{"200":{"description":"Die Uebersichtsseite als HTML-Dokument.","content":{"text/html":{}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur in Produktion: es fehlt `Authorization: Bearer <DOCS_BEARER_TOKEN>`, die Variable ist gar nicht gesetzt, oder `DISABLE_PUBLIC_DOCS=true` sperrt das Portal ganz. Ausserhalb der Produktion antwortet keine dieser Routen 403. Ein 401 gibt es in diesem Pfad nicht."}},"operationId":"getApi-docs","tags":["api-docs"],"parameters":[],"summary":"Uebersicht aller Fachbereiche","description":"Liefert eine fertige HTML-SEITE (`text/html`), kein JSON — die Einstiegsseite des API-Portals. Sie listet die Fachbereiche mit je einem Link auf ihre Referenz, nennt je Bereich die Zahl der Operationen und darunter, wie viele Operationen zu KEINEM Bereich gehoeren (unbekanntes Pfad-Praefix). Die Zahlen entstehen bei der Anfrage aus dem laufenden Router, nicht aus einer eingecheckten Datei; sie koennen deshalb nicht veralten. Die Seite kommt ohne JavaScript aus. Maschinell auswerten laesst sich das nicht — dafuer gibt es `/api-docs/{modul}/spec.json` je Bereich oder `/openapi.json` fuer alles."}},"/api-docs/{modul}/spec.json":{"get":{"responses":{"200":{"description":"Das OpenAPI-Dokument des Bereichs. Form ist die von OpenAPI, deshalb steht hier kein eigenes Schema.","content":{"application/json":{}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur in Produktion: es fehlt `Authorization: Bearer <DOCS_BEARER_TOKEN>`, die Variable ist gar nicht gesetzt, oder `DISABLE_PUBLIC_DOCS=true` sperrt das Portal ganz. Ausserhalb der Produktion antwortet keine dieser Routen 403. Ein 401 gibt es in diesem Pfad nicht."},"404":{"description":"Diesen Fachbereich gibt es nicht. Rumpf: `{ \"error\": \"not_found\", \"message\": \"Unbekannter Fachbereich: <id>\" }`. Gueltig sind ausschliesslich die Ids aus der Uebersicht."}},"operationId":"getApi-docsByModulSpec.json","tags":["api-docs"],"parameters":[{"name":"modul","in":"path","required":true,"description":"Id eines Fachbereichs.","schema":{"type":"string","enum":["identity","crm","sales","purchasing","inventory","finance","projects","hr","realestate","documents","collaboration","ai","reporting","administration","integrations","platform"]}}],"summary":"Spezifikation eines Fachbereichs","description":"Liefert eine vollstaendige OpenAPI-Spezifikation als JSON — aber nur den AUSSCHNITT eines Fachbereichs: enthalten sind ausschliesslich die Pfade, deren Praefix zu `{modul}` gehoert. `info.title` und `info.description` tragen den Namen des Bereichs, `x-nemix-module` seine Id, `x-nemix-owner` das zustaendige Team. Jede Operation wird dabei angereichert (Sicherheitsschema, Risikoklasse, Kennzeichen fuer datenveraendernde Aufrufe, Quelldatei), und `components` bekommt die gemeinsamen Schemas und Sicherheitsschemata dazu. Fachliche Beschreibungen stammen aus `describeRoute` der jeweiligen Handler: wo sie dort fehlen, fehlen sie auch hier und werden NICHT durch erfundenen Text ersetzt. Erzeugt wird bei der Anfrage aus dem laufenden Router. Fuer die gesamte API statt eines Bereichs: `/openapi.json`."}},"/api-docs/{modul}":{"get":{"responses":{"200":{"description":"Die Referenzseite des Bereichs als HTML-Dokument.","content":{"text/html":{}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur in Produktion: es fehlt `Authorization: Bearer <DOCS_BEARER_TOKEN>`, die Variable ist gar nicht gesetzt, oder `DISABLE_PUBLIC_DOCS=true` sperrt das Portal ganz. Ausserhalb der Produktion antwortet keine dieser Routen 403. Ein 401 gibt es in diesem Pfad nicht."},"404":{"description":"Diesen Fachbereich gibt es nicht. Rumpf: `{ \"error\": \"not_found\", \"message\": \"Unbekannter Fachbereich: <id>\" }`. Gueltig sind ausschliesslich die Ids aus der Uebersicht."}},"operationId":"getApi-docsByModul","tags":["api-docs"],"parameters":[{"name":"modul","in":"path","required":true,"description":"Id eines Fachbereichs.","schema":{"type":"string","enum":["identity","crm","sales","purchasing","inventory","finance","projects","hr","realestate","documents","collaboration","ai","reporting","administration","integrations","platform"]}}],"summary":"Referenz eines Fachbereichs","description":"Liefert eine fertige HTML-SEITE (`text/html`), kein JSON — die lesbare Referenz eines Fachbereichs. Sie zeigt dieselben Operationen wie `/api-docs/{modul}/spec.json`, nach Ressource gruppiert: Verb, Pfad und Kurzbeschreibung sind immer sichtbar, Parameter sowie Anfrage- und Antwortschemata stehen aufgeklappt darunter. Die Seite wird SERVERSEITIG gerendert und braucht kein JavaScript — die Sicherheitskopfzeilen der API erlauben keine fremden Skripte, weshalb hier weder Swagger UI noch Scalar laufen. Wo ein Handler keine Beschreibung oder kein Antwortschema hinterlegt hat, sagt die Seite das an der Stelle, statt die Luecke zu fuellen. Maschinell auswerten: dieselbe Adresse mit `/spec.json` am Ende."}},"/api/ai/dashboard":{"post":{"responses":{"200":{"description":"Erzeugte Dashboard-Konfiguration samt Modell- und Kostenangaben.","content":{"application/json":{"schema":{"type":"object","properties":{"config":{"type":"object","properties":{"layout":{"type":"string","enum":["grid","masonry"],"default":"grid"},"columns":{"type":"number","minimum":1,"maximum":4,"default":4},"widgets":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"kpi"},"id":{"type":"string"},"title":{"type":"string"},"metric":{"type":"string","enum":["revenue","orders_count","customers_new","invoices_open","inventory_low","projects_active"]},"period":{"type":"string","enum":["today","week","month","quarter","year"]},"comparison":{"type":"boolean","default":true},"position":{"type":"object","properties":{"col":{"type":"number","minimum":0,"maximum":3},"row":{"type":"number","minimum":0}},"required":["col","row"]},"size":{"type":"string","enum":["sm","md","lg"],"default":"md"}},"required":["type","id","title","metric","period","comparison","position","size"]},{"type":"object","properties":{"type":{"type":"string","const":"table"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string","enum":["orders","customers","invoices","inventory","projects","leads"]},"columns":{"type":"array","items":{"type":"string"},"maxItems":8},"sort":{"type":"string"},"filter":{"type":"string"},"limit":{"type":"number","minimum":5,"maximum":50,"default":10},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","source","columns","limit","position"]},{"type":"object","properties":{"type":{"type":"string","const":"chart"},"id":{"type":"string"},"title":{"type":"string"},"chartType":{"type":"string","enum":["bar","line","pie","area","funnel"]},"metric":{"type":"string"},"groupBy":{"type":"string"},"period":{"type":"string","enum":["week","month","quarter","year"]},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","chartType","metric","period","position"]},{"type":"object","properties":{"type":{"type":"string","const":"alert"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string"},"condition":{"type":"string"},"severity":{"type":"string","enum":["info","warning","critical"]},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","source","condition","severity","position"]}]},"minItems":1,"maxItems":12},"name":{"type":"string"}},"required":["layout","columns","widgets"]},"id":{"type":["string","null"]},"persisted":{"type":"boolean"},"message":{"type":"string"},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"},"tokensUsed":{"type":"object","properties":{"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"cachedInputTokens":{"type":"number"},"eurCents":{"type":"number"}},"required":["inputTokens","outputTokens","cachedInputTokens","eurCents"]}},"required":["config","id","persisted","message","modelUsed","fellBack","tokensUsed"]},"example":{"config":{"layout":"grid","columns":1,"widgets":[{"type":"kpi","id":"string","title":"string","metric":"revenue","period":"today","comparison":true,"position":{"col":0,"row":0},"size":"sm"}],"name":"string"},"id":"string","persisted":true,"message":"string","modelUsed":"string","fellBack":true,"tokensUsed":{"inputTokens":0,"outputTokens":0,"cachedInputTokens":0,"eurCents":0}}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiAiDashboard","tags":["ai"],"parameters":[],"summary":"Dashboard-Konfiguration aus Freitext erzeugen und speichern","description":"Laesst ein Sprachmodell aus dem Freitext in `intent` eine Widget-Konfiguration (KPI, Tabelle, Diagramm, Alarm) bauen; `existingConfig` wird als Ausgangsstand mitgegeben und geaendert statt ersetzt. Das Ergebnis wird als UI-Konfiguration vom Typ `dashboard` im Scope `global` gespeichert — schlaegt das fehl, kommt die Konfiguration trotzdem zurueck, dann mit `persisted: false`. Kostet Kontingent: ueberschrittenes Monatsbudget antwortet 402, ein gescheiterter Modellaufruf wird dem Mandanten wieder gutgeschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","minLength":1,"maxLength":1000},"existingConfig":{"type":"object","properties":{"layout":{"type":"string","enum":["grid","masonry"],"default":"grid"},"columns":{"type":"number","minimum":1,"maximum":4,"default":4},"widgets":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"kpi"},"id":{"type":"string"},"title":{"type":"string"},"metric":{"type":"string","enum":["revenue","orders_count","customers_new","invoices_open","inventory_low","projects_active"]},"period":{"type":"string","enum":["today","week","month","quarter","year"]},"comparison":{"type":"boolean","default":true},"position":{"type":"object","properties":{"col":{"type":"number","minimum":0,"maximum":3},"row":{"type":"number","minimum":0}},"required":["col","row"]},"size":{"type":"string","enum":["sm","md","lg"],"default":"md"}},"required":["type","id","title","metric","period","position"]},{"type":"object","properties":{"type":{"type":"string","const":"table"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string","enum":["orders","customers","invoices","inventory","projects","leads"]},"columns":{"type":"array","items":{"type":"string"},"maxItems":8},"sort":{"type":"string"},"filter":{"type":"string"},"limit":{"type":"number","minimum":5,"maximum":50,"default":10},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","source","columns","position"]},{"type":"object","properties":{"type":{"type":"string","const":"chart"},"id":{"type":"string"},"title":{"type":"string"},"chartType":{"type":"string","enum":["bar","line","pie","area","funnel"]},"metric":{"type":"string"},"groupBy":{"type":"string"},"period":{"type":"string","enum":["week","month","quarter","year"]},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","chartType","metric","period","position"]},{"type":"object","properties":{"type":{"type":"string","const":"alert"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string"},"condition":{"type":"string"},"severity":{"type":"string","enum":["info","warning","critical"]},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","source","condition","severity","position"]}]},"minItems":1,"maxItems":12},"name":{"type":"string"}},"required":["widgets"]}},"required":["intent"]},"example":{"intent":"string","existingConfig":{"layout":"grid","columns":1,"widgets":[{"type":"kpi","id":"string","title":"string","metric":"revenue","period":"today","comparison":true,"position":{"col":0,"row":0},"size":"sm"}],"name":"string"}}}}}},"get":{"responses":{"200":{"description":"Gespeicherte Konfiguration, oder `config: null` wenn keine hinterlegt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":["string","null"]},"config":{},"name":{"type":["string","null"]},"scope":{"type":["string","null"]},"updatedAt":{"type":["string","null"]},"warning":{"type":"string"}},"required":["id"]},"example":{"id":"string","name":"string","scope":"string","updatedAt":"string","warning":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiAiDashboard","tags":["ai"],"parameters":[],"description":"Liest die zuletzt gespeicherte Dashboard-Konfiguration des Mandanten. Der Abfrageparameter `scope` waehlt die Variante (Standard `global`). Gibt es keine, sind `config` und `id` `null` — das ist kein Fehler. Ist die Datenbank nicht erreichbar, antwortet der Endpunkt ebenfalls mit 200 und setzt zusaetzlich `warning`.","summary":"Liest die zuletzt gespeicherte Dashboard-Konfiguration des Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/ai/workflow":{"post":{"responses":{"200":{"description":"Workflow-Entwurf samt Modell- und Kostenangaben.","content":{"application/json":{"schema":{"type":"object","properties":{"workflow":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"trigger":{"type":"object","properties":{"type":{"type":"string","enum":["event","schedule","webhook_in","manual","record_change"]},"event":{"type":"string"},"cron":{"type":"string"},"entity":{"type":"string"}},"required":["type"]},"conditions":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains"]},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"required":["field","operator","value"]}},"actions":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["email","webhook_out","set_field","notify","create_record","approval"]},"description":{"type":"string"},"config":{"type":"object","additionalProperties":{}}},"required":["type","description","config"]}},"explanation":{"type":"string"}},"required":["name","description","trigger","actions","explanation"]},"message":{"type":"string"},"explanation":{"type":"string"},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"},"tokensUsed":{"type":"object","properties":{"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"cachedInputTokens":{"type":"number"},"eurCents":{"type":"number"}},"required":["inputTokens","outputTokens","cachedInputTokens","eurCents"]}},"required":["workflow","message","explanation","modelUsed","fellBack","tokensUsed"]},"example":{"workflow":{"name":"string","description":"string","trigger":{"type":"event","event":"string","cron":"string","entity":"string"},"conditions":[{"field":"string","operator":"eq","value":"string"}],"actions":[{"type":"email","description":"string","config":{}}],"explanation":"string"},"message":"string","explanation":"string","modelUsed":"string","fellBack":true,"tokensUsed":{"inputTokens":0,"outputTokens":0,"cachedInputTokens":0,"eurCents":0}}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiAiWorkflow","tags":["ai"],"parameters":[],"description":"Uebersetzt den Freitext in `intent` in einen Workflow-Entwurf: Ausloeser (Ereignis, Zeitplan, Webhook, manuell, Datensatzaenderung), Bedingungen und Aktionen, dazu eine Begruendung in `explanation`. Der Entwurf wird NICHT gespeichert — er kommt nur zurueck. Kostet Kontingent: ueberschrittenes Monatsbudget antwortet 402, ein gescheiterter Modellaufruf wird gutgeschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","minLength":1,"maxLength":1000}},"required":["intent"]},"example":{"intent":"string"}}}},"summary":"Uebersetzt den Freitext in `intent` in einen Workflow-Entwurf","x-nemix-summary-source":"description:first-sentence"}},"/api/ai/search":{"post":{"responses":{"200":{"description":"Treffer nach absteigender Aehnlichkeit. Leere Liste auch im Ausfall (siehe `warning`/`error`).","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"results":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"snippet":{"type":"string"},"score":{"type":"number"}},"required":["source","id","title","snippet","score"]}},"total":{"type":"number"},"warning":{"type":"string"},"error":{"type":"string"}},"required":["query","results","total"]},"example":{"query":"string","results":[{"source":"string","id":"string","title":"string","snippet":"string","score":0}],"total":0,"warning":"string","error":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiAiSearch","tags":["ai"],"parameters":[],"description":"Semantische Suche: `query` wird in einen 1536-dimensionalen Vektor uebersetzt und per Kosinus-Abstand gegen `public.ai_embeddings` gesucht, eingegrenzt auf den eigenen Mandanten und optional auf die in `sources` genannten Entitaetstypen. `limit` steuert die Trefferzahl (1..20, Standard 5). Faellt die Datenbank oder die Vektorsuche aus, antwortet der Endpunkt trotzdem mit 200 und leerer Trefferliste — der Grund steht dann in `warning` bzw. `error`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","minLength":1},"sources":{"type":"array","items":{"type":"string"}},"limit":{"type":"number","minimum":1,"maximum":20,"default":5}},"required":["query"]},"example":{"query":"string","sources":["string"],"limit":1}}}},"summary":"Semantische Suche","x-nemix-summary-source":"description:first-sentence"}},"/api/ai/chat":{"post":{"responses":{"200":{"description":"Ohne `noStream` ein SSE-Strom (`data:`-Zeilen mit `{ type, value }`). Mit `noStream: true` das hier beschriebene JSON-Objekt.","content":{"text/event-stream":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"ragEnabled":{"type":"boolean"},"snippetCount":{"type":"number"},"cacheEnabled":{"type":"boolean"},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"},"message":{"type":"string"},"toolCalls":{"type":"array","items":{}},"toolResults":{"type":"array","items":{}},"citations":{"type":"array","items":{}}},"required":["tenantId","ragEnabled","snippetCount","cacheEnabled","modelUsed","fellBack","message","toolCalls","toolResults","citations"]},"example":{"tenantId":"string","ragEnabled":true,"snippetCount":0,"cacheEnabled":true,"modelUsed":"string","fellBack":true,"message":"string","toolCalls":[],"toolResults":[],"citations":[]}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiAiChat","tags":["ai"],"parameters":[],"description":"Der Chat des ERP-Assistenten mit Werkzeugaufrufen. `message` (bis 4000 Zeichen) ist die Frage, `history` (bis 20 Zuege) der Gespraechsverlauf. Standardfall ist ein Server-Sent-Events-Strom; `noStream: true` liefert stattdessen ein einzelnes JSON-Objekt. Der Text des Nutzers laeuft vorher durch die Injection-Pruefung und wird bereinigt weitergereicht, nicht abgelehnt. Destruktive Werkzeuge bleiben hier fail-closed: eine Bestaetigung aus dem Anfragerumpf oeffnet die Wand NICHT. Kostet Kontingent; ueberschrittenes Monatsbudget antwortet 402.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","minLength":1,"maxLength":4000},"history":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string"}},"required":["role","content"]},"maxItems":20},"useRag":{"type":"boolean"},"ragOptions":{"type":"object","properties":{"maxContext":{"type":"number","minimum":1,"maximum":50},"entityTypes":{"type":"array","items":{"type":"string"},"maxItems":10}}},"noStream":{"type":"boolean"}},"required":["message"]},"example":{"message":"string","history":[{"role":"user","content":"string"}],"useRag":true,"ragOptions":{"maxContext":1,"entityTypes":["string"]},"noStream":true}}}},"summary":"Der Chat des ERP-Assistenten mit Werkzeugaufrufen","x-nemix-summary-source":"description:first-sentence"}},"/api/ai/report":{"post":{"responses":{"200":{"description":"Die erzeugte Report-Definition und das benutzte Modell.","content":{"application/json":{"schema":{"type":"object","properties":{"report":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"source":{"type":"string","enum":["orders","customers","invoices","inventory","projects","leads"]},"metrics":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":8},"groupBy":{"type":"string"},"period":{"type":"string","enum":["week","month","quarter","year"],"default":"month"},"format":{"type":"string","enum":["table","chart","kpi"],"default":"table"},"explanation":{"type":"string"}},"required":["title","description","source","metrics","period","format","explanation"]},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"}},"required":["report","modelUsed","fellBack"]},"example":{"report":{"title":"string","description":"string","source":"orders","metrics":["string"],"groupBy":"string","period":"week","format":"table","explanation":"string"},"modelUsed":"string","fellBack":true}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiAiReport","tags":["ai"],"parameters":[],"description":"Baut aus dem Freitext in `intent` eine Report-Definition: Datenquelle, Kennzahlen, Gruppierung, Zeitraum und Darstellungsform (Tabelle, Diagramm oder Kennzahl). Der Report wird NICHT gespeichert und NICHT ausgefuehrt — es kommt nur die Definition zurueck. Kostet Kontingent; ueberschrittenes Monatsbudget antwortet 402.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","minLength":1,"maxLength":1000}},"required":["intent"]},"example":{"intent":"string"}}}},"summary":"Baut aus dem Freitext in `intent` eine Report-Definition","x-nemix-summary-source":"description:first-sentence"}},"/api/ai/custom-field":{"post":{"responses":{"200":{"description":"Der Feldvorschlag samt Anzeige-SQL. Bei `apply: true` zusaetzlich `execution` mit dem Ergebnis der Pipeline.","content":{"application/json":{"schema":{"type":"object","properties":{"proposal":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","invoices","products","projects"]},"fieldKey":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,40}$"},"label":{"type":"string"},"type":{"type":"string","enum":["text","number","date","boolean","json"]},"required":{"type":"boolean","default":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"explanation":{"type":"string"}},"required":["entity","fieldKey","label","type","required","explanation"]},"proposedSql":{"type":"string"},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"},"applied":{"type":"boolean"},"message":{"type":"string"},"execution":{}},"required":["proposal","proposedSql","modelUsed","fellBack","applied"]}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiAiCustom-field","tags":["ai"],"parameters":[],"summary":"Zusatzfeld aus Freitext vorschlagen, auf Wunsch anlegen","description":"Leitet aus dem Freitext in `intent` eine Zusatzfeld-Definition ab (Entitaet, Feldschluessel in snake_case, Typ, Pflicht, Vorgabewert) und gibt daneben das zugehoerige `ALTER TABLE` als reinen Anzeigetext zurueck — dieser Text wird nie ausgefuehrt. Ohne `apply` bleibt es beim Vorschlag (`applied: false`). Mit `apply: true` laeuft die Aenderung durch die Werkzeug-Pipeline (RBAC, Vorschau, Transaktion, GoBD-Eintrag); vorher greifen eine Werkzeugwand (Dienstkonten 403, unbekanntes Werkzeug 404) und ein Tageslimit je Mandant (429).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","minLength":1,"maxLength":800},"apply":{"type":"boolean"}},"required":["intent"]},"example":{"intent":"string","apply":true}}}}}},"/api/ai/integration-mapping":{"post":{"responses":{"200":{"description":"Die vorgeschlagene Feldzuordnung samt Sicherheiten und Restliste.","content":{"application/json":{"schema":{"type":"object","properties":{"mapping":{"type":"object","properties":{"provider":{"type":"string"},"fieldMap":{"type":"array","items":{"type":"object","properties":{"external":{"type":"string"},"internal":{"type":"string"},"transform":{"type":"string"},"confidence":{"type":"number","minimum":0,"maximum":1}},"required":["external","internal","confidence"]}},"unmapped":{"type":"array","items":{"type":"string"},"default":[]},"explanation":{"type":"string"}},"required":["provider","fieldMap","unmapped","explanation"]},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"}},"required":["mapping","modelUsed","fellBack"]},"example":{"mapping":{"provider":"string","fieldMap":[{"external":"string","internal":"string","transform":"string","confidence":0}],"unmapped":["string"],"explanation":"string"},"modelUsed":"string","fellBack":true}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiAiIntegration-mapping","tags":["ai"],"parameters":[],"summary":"Fremdsystem-Felder einer Nemix-Entitaet zuordnen","description":"Ordnet die in `externalFields` genannten Feldnamen eines Fremdsystems (bis 200) den Feldern der Nemix-Entitaet `internalEntity` zu. Je Zuordnung kommt eine Sicherheit zwischen 0 und 1 zurueck; was nicht sicher zuzuordnen war, steht in `unmapped`. Es wird nichts gespeichert und keine Integration eingerichtet. Kostet Kontingent; ueberschrittenes Monatsbudget antwortet 402.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"externalFields":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":200},"internalEntity":{"type":"string","enum":["customers","orders","invoices","products","projects"]}},"required":["provider","externalFields","internalEntity"]},"example":{"provider":"string","externalFields":["string"],"internalEntity":"customers"}}}}}},"/api/v1/capabilities":{"get":{"responses":{"200":{"description":"Die kuratierte Liste. Kein Fehlerpfad — die Antwort ist fest verdrahtet. Die genannten `methods` sind die des BEREICHS, nicht die eines einzelnen Pfades: dass `orders` DELETE kennt, heisst nicht, dass jeder Unterpfad das tut.","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string","const":"1.0.0","description":"Version DIESER Auskunft, nicht der API"},"modules":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string","description":"Der Basispfad des Bereichs, ohne Unterpfade"},"methods":{"type":"array","items":{"type":"string"},"description":"Die Methoden, die dieser Bereich kennt"}},"required":["name","path","methods"]}},"integrations":{"type":"array","items":{"type":"string"},"description":"Namen angebundener Fremdsysteme, ohne Pfade"},"docs":{"type":"string","const":"/docs","description":"In Produktion ohne DOCS_BEARER_TOKEN mit 403"}},"required":["version","modules","integrations","docs"]},"example":{"version":"1.0.0","modules":[{"name":"string","path":"string","methods":["string"]}],"integrations":["string"],"docs":"/docs"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Capabilities","tags":["platform"],"parameters":[],"summary":"Kuratierte Selbstauskunft der API (anmeldefrei)","description":"Nennt eine von Hand gepflegte Auswahl von Modulen und Integrationen. ACHTUNG: Das ist KEIN vollstaendiges Verzeichnis. Gemessen am 30.08.2026 antworten unter /api/v1 insgesamt 211 Bereiche; diese Liste nennt 17. Alle 17 Angaben stimmen — Pfade und Methoden wurden gegen die Routentabelle geprueft —, aber wer den Umfang der API wissen will, liest /openapi.json (2396 Operationen) und nicht diese Liste. Der Verweis \"docs\" zeigt auf /docs, das in Produktion ohne DOCS_BEARER_TOKEN mit 403 antwortet."}},"/api/v1/customers":{"get":{"responses":{"200":{"description":"Liste der Kunden","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Kunden (UUID)"},"customerNumber":{"type":["string","null"],"description":"Kundennummer aus dem Nummernkreis des Mandanten"},"vatId":{"type":["string","null"],"description":"USt-IdNr.; unterhalb der Rolle accountant immer null"},"customerCategory":{"type":"string","description":"Kundenkategorie, Vorgabewert \"Interessent\""},"name":{"type":["string","null"],"description":"Anzeigename des Kunden"},"entityKind":{"type":"string","enum":["organization","person"],"description":"Firma oder natuerliche Person; Vorgabe \"organization\""},"salutation":{"type":["string","null"],"description":"Anrede (Herr, Frau, Divers, Firma)"},"email":{"type":["string","null"],"description":"E-Mail-Adresse"},"phone":{"type":["string","null"],"description":"Festnetznummer"},"mobile":{"type":["string","null"],"description":"Mobilnummer"},"phoneConsent":{"type":"boolean","description":"Einwilligung in telefonische Kontaktaufnahme; fehlt die Spalte, gilt false"},"website":{"type":["string","null"],"description":"Webadresse"},"company":{"type":["string","null"],"description":"Firmenname"},"type":{"type":["string","null"],"description":"Kundentyp, frei belegbar"},"status":{"type":["string","null"],"description":"Status: active, inactive oder blocked"},"address":{"description":"Hauptanschrift; Form haengt an den Mandantendaten"},"addresses":{"type":["array","null"],"items":{},"description":"Weitere Anschriften"},"paymentTerms":{"type":["string","null"],"description":"Zahlungsbedingung im Klartext"},"defaultPaymentTermsDays":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"null"}],"description":"Zahlungsziel in Tagen; je nach Zugriffsweg Zahl oder Zeichenkette"},"defaultCurrency":{"type":["string","null"],"description":"Bevorzugte Belegwaehrung (ISO-4217)"},"language":{"type":["string","null"],"description":"Belegsprache, Vorgabewert \"de\""},"ragEnabled":{"type":"boolean","description":"Wird der Datensatz in den KI-Kontext indexiert?"},"categoryId":{"type":["string","null"],"description":"Kennung der Kundenkategorie"},"nameSuffix":{"type":["string","null"],"description":"Namenszusatz"},"debtorNumber":{"type":["string","null"],"description":"Debitorennummer der Buchhaltung"},"creditorNumber":{"type":["string","null"],"description":"Kreditorennummer der Buchhaltung"},"eInvoiceDefault":{"type":"boolean","description":"E-Rechnung als Vorgabe fuer diesen Kunden"},"taxExempt":{"type":"boolean","description":"Steuerbefreiung hinterlegt"},"taxExemptReason":{"type":["string","null"],"description":"Begruendung der Steuerbefreiung"},"iban":{"type":["string","null"],"description":"IBAN; unterhalb der Rolle accountant immer null"},"bic":{"type":["string","null"],"description":"BIC; unterhalb der Rolle accountant immer null"},"bankName":{"type":["string","null"],"description":"Bankname; unterhalb der Rolle accountant immer null"},"accountHolder":{"type":["string","null"],"description":"Kontoinhaber; unterhalb der Rolle accountant immer null"},"ansprechpartnerName":{"type":["string","null"],"description":"Name des Hauptansprechpartners"},"ansprechpartnerEmail":{"type":["string","null"],"description":"E-Mail des Hauptansprechpartners"},"ansprechpartnerTelefon":{"type":["string","null"],"description":"Telefon des Hauptansprechpartners"},"ansprechpartnerPosition":{"type":["string","null"],"description":"Position des Hauptansprechpartners"},"tags":{"type":["array","null"],"items":{},"description":"Freie Schlagworte"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"description":"Anlagezeitpunkt als ISO-Zeichenkette oder null"},"updatedAt":{"description":"Letzte Aenderung als ISO-Zeichenkette oder null"}},"required":["id","customerNumber","vatId","customerCategory","entityKind","salutation","mobile","phoneConsent","website","paymentTerms","defaultPaymentTermsDays","defaultCurrency","language","ragEnabled","categoryId","nameSuffix","debtorNumber","creditorNumber","eInvoiceDefault","taxExempt","taxExemptReason","iban","bic","bankName","accountHolder","ansprechpartnerName","ansprechpartnerEmail","ansprechpartnerTelefon","ansprechpartnerPosition"],"additionalProperties":false},"description":"Die Kunden dieser Seite"},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":1,"description":"Angeforderte Seite, 1-basiert"},"limit":{"type":"integer","minimum":1,"maximum":1000,"description":"Angewendete Seitengroesse — gekappt, nicht der Wunschwert"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer ueber alle Seiten"},"pages":{"type":"integer","minimum":1,"description":"Anzahl Seiten bei dieser Seitengroesse"}},"required":["page","limit","total","pages"],"description":"Seitenangaben"},"meta":{"type":"object","additionalProperties":{},"description":"Mandanten-Id und Datenquelle"}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","customerNumber":"string","vatId":"string","customerCategory":"string","name":"string","entityKind":"organization","salutation":"string","email":"string","phone":"string","mobile":"string","phoneConsent":true,"website":"string","company":"string","type":"string","status":"string","addresses":[],"paymentTerms":"string","defaultPaymentTermsDays":0,"defaultCurrency":"string","language":"string","ragEnabled":true,"categoryId":"string","nameSuffix":"string","debtorNumber":"string","creditorNumber":"string","eInvoiceDefault":true,"taxExempt":true,"taxExemptReason":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","ansprechpartnerName":"string","ansprechpartnerEmail":"string","ansprechpartnerTelefon":"string","ansprechpartnerPosition":"string","tags":[],"notes":"string","customFields":{}}],"pagination":{"page":1,"limit":1,"total":0,"pages":1},"meta":{}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Customers","tags":["customers"],"parameters":[{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":1000,"default":25}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"type","schema":{"type":"string","minLength":1,"maxLength":120}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive","blocked"]}},{"in":"query","name":"sort","schema":{"type":"string"}},{"in":"query","name":"order","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}],"summary":"List customers","description":"Listet alle Kunden des aktuellen Mandanten mit Paginierung und Filter"},"post":{"responses":{"201":{"description":"Kunde angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Kunden (UUID)"},"customerNumber":{"type":["string","null"],"description":"Kundennummer aus dem Nummernkreis des Mandanten"},"vatId":{"type":["string","null"],"description":"USt-IdNr.; unterhalb der Rolle accountant immer null"},"customerCategory":{"type":"string","description":"Kundenkategorie, Vorgabewert \"Interessent\""},"name":{"type":["string","null"],"description":"Anzeigename des Kunden"},"entityKind":{"type":"string","enum":["organization","person"],"description":"Firma oder natuerliche Person; Vorgabe \"organization\""},"salutation":{"type":["string","null"],"description":"Anrede (Herr, Frau, Divers, Firma)"},"email":{"type":["string","null"],"description":"E-Mail-Adresse"},"phone":{"type":["string","null"],"description":"Festnetznummer"},"mobile":{"type":["string","null"],"description":"Mobilnummer"},"phoneConsent":{"type":"boolean","description":"Einwilligung in telefonische Kontaktaufnahme; fehlt die Spalte, gilt false"},"website":{"type":["string","null"],"description":"Webadresse"},"company":{"type":["string","null"],"description":"Firmenname"},"type":{"type":["string","null"],"description":"Kundentyp, frei belegbar"},"status":{"type":["string","null"],"description":"Status: active, inactive oder blocked"},"address":{"description":"Hauptanschrift; Form haengt an den Mandantendaten"},"addresses":{"type":["array","null"],"items":{},"description":"Weitere Anschriften"},"paymentTerms":{"type":["string","null"],"description":"Zahlungsbedingung im Klartext"},"defaultPaymentTermsDays":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"null"}],"description":"Zahlungsziel in Tagen; je nach Zugriffsweg Zahl oder Zeichenkette"},"defaultCurrency":{"type":["string","null"],"description":"Bevorzugte Belegwaehrung (ISO-4217)"},"language":{"type":["string","null"],"description":"Belegsprache, Vorgabewert \"de\""},"ragEnabled":{"type":"boolean","description":"Wird der Datensatz in den KI-Kontext indexiert?"},"categoryId":{"type":["string","null"],"description":"Kennung der Kundenkategorie"},"nameSuffix":{"type":["string","null"],"description":"Namenszusatz"},"debtorNumber":{"type":["string","null"],"description":"Debitorennummer der Buchhaltung"},"creditorNumber":{"type":["string","null"],"description":"Kreditorennummer der Buchhaltung"},"eInvoiceDefault":{"type":"boolean","description":"E-Rechnung als Vorgabe fuer diesen Kunden"},"taxExempt":{"type":"boolean","description":"Steuerbefreiung hinterlegt"},"taxExemptReason":{"type":["string","null"],"description":"Begruendung der Steuerbefreiung"},"iban":{"type":["string","null"],"description":"IBAN; unterhalb der Rolle accountant immer null"},"bic":{"type":["string","null"],"description":"BIC; unterhalb der Rolle accountant immer null"},"bankName":{"type":["string","null"],"description":"Bankname; unterhalb der Rolle accountant immer null"},"accountHolder":{"type":["string","null"],"description":"Kontoinhaber; unterhalb der Rolle accountant immer null"},"ansprechpartnerName":{"type":["string","null"],"description":"Name des Hauptansprechpartners"},"ansprechpartnerEmail":{"type":["string","null"],"description":"E-Mail des Hauptansprechpartners"},"ansprechpartnerTelefon":{"type":["string","null"],"description":"Telefon des Hauptansprechpartners"},"ansprechpartnerPosition":{"type":["string","null"],"description":"Position des Hauptansprechpartners"},"tags":{"type":["array","null"],"items":{},"description":"Freie Schlagworte"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"description":"Anlagezeitpunkt als ISO-Zeichenkette oder null"},"updatedAt":{"description":"Letzte Aenderung als ISO-Zeichenkette oder null"}},"required":["id","customerNumber","vatId","customerCategory","entityKind","salutation","mobile","phoneConsent","website","paymentTerms","defaultPaymentTermsDays","defaultCurrency","language","ragEnabled","categoryId","nameSuffix","debtorNumber","creditorNumber","eInvoiceDefault","taxExempt","taxExemptReason","iban","bic","bankName","accountHolder","ansprechpartnerName","ansprechpartnerEmail","ansprechpartnerTelefon","ansprechpartnerPosition"],"additionalProperties":false},"example":{"id":"string","customerNumber":"string","vatId":"string","customerCategory":"string","name":"string","entityKind":"organization","salutation":"string","email":"string","phone":"string","mobile":"string","phoneConsent":true,"website":"string","company":"string","type":"string","status":"string","addresses":[],"paymentTerms":"string","defaultPaymentTermsDays":0,"defaultCurrency":"string","language":"string","ragEnabled":true,"categoryId":"string","nameSuffix":"string","debtorNumber":"string","creditorNumber":"string","eInvoiceDefault":true,"taxExempt":true,"taxExemptReason":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","ansprechpartnerName":"string","ansprechpartnerEmail":"string","ansprechpartnerTelefon":"string","ansprechpartnerPosition":"string","tags":[],"notes":"string","customFields":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"422":{"description":"Entity-Rule-Verletzung (mandantenspezifische Pflichtfeld-/Wertregel)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"customers.create","tags":["customers"],"parameters":[],"summary":"Create customer","description":"Legt einen neuen Kunden für den Mandanten an. Die Kundennummer vergibt der Server aus dem Nummernkreis des Mandanten, wenn keine mitgeschickt wird. Unterhalb der Rolle `accountant` kommen vatId/iban/bic/bankName/accountHolder in der Antwort als `null` zurück — auch wenn sie eben erst geschrieben wurden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entityKind":{"type":"string","enum":["organization","person"],"default":"organization"},"name":{"type":"string","minLength":2,"maxLength":255},"customerCategory":{"type":"string","enum":["Interessent","Geschäftskunde","Privatkunde","Sonstige"]},"salutation":{"type":"string","enum":["Herr","Frau","Divers","Firma"]},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string"},"mobile":{"type":"string"},"phoneConsent":{"type":"boolean"},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"company":{"type":"string"},"type":{"type":"string","minLength":1,"maxLength":120,"default":"Interessent"},"status":{"type":"string","enum":["active","inactive","blocked"],"default":"active"},"address":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":120,"default":"Rechnungsadresse"},"label":{"type":"string","maxLength":120},"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"},"email":{"type":"string","maxLength":200},"phone":{"type":"string","maxLength":60}}},"addresses":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":120,"default":"Rechnungsadresse"},"label":{"type":"string","maxLength":120},"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"},"email":{"type":"string","maxLength":200},"phone":{"type":"string","maxLength":60}}}},"paymentTerms":{"type":"string","maxLength":100},"defaultPaymentTermsDays":{"type":["integer","null"],"minimum":0,"maximum":365},"defaultCurrency":{"type":"string","enum":["EUR","USD","GBP","CHF","JPY","PLN","CZK","HUF"]},"language":{"type":"string","enum":["de","en","fr","es","pl","nl","da","cs","zh"]},"tags":{"type":"array","items":{"type":"string"}},"notes":{"type":"string"},"categoryId":{"type":"string","format":"uuid"},"nameSuffix":{"type":"string","maxLength":255},"customerNumber":{"type":"string","maxLength":20},"debtorNumber":{"type":"string","maxLength":20},"creditorNumber":{"type":"string","maxLength":20},"vatId":{"type":"string","maxLength":40},"eInvoiceDefault":{"type":"boolean"},"iban":{"type":["string","null"],"maxLength":40},"bic":{"type":["string","null"],"maxLength":20},"bankName":{"type":["string","null"],"maxLength":120},"accountHolder":{"type":["string","null"],"maxLength":255},"taxExempt":{"type":"boolean"},"taxExemptReason":{"type":["string","null"],"enum":["innergemeinschaftlich","drittland","kleinunternehmer","reverse_charge",null]},"ansprechpartnerName":{"type":"string","maxLength":255},"ansprechpartnerEmail":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"ansprechpartnerTelefon":{"type":"string","maxLength":50},"ansprechpartnerPosition":{"type":"string","maxLength":120},"customFields":{"type":"object","additionalProperties":{}}},"required":["name"]},"example":{"entityKind":"organization","name":"string","customerCategory":"Interessent","salutation":"Herr","email":"beispiel@example.com","phone":"string","mobile":"string","phoneConsent":true,"website":"https://example.com","company":"string","type":"string","status":"active","address":{"type":"string","label":"string","street":"string","city":"string","zip":"string","country":"string","email":"string","phone":"string"},"addresses":[{"type":"string","label":"string","street":"string","city":"string","zip":"string","country":"string","email":"string","phone":"string"}],"paymentTerms":"string","defaultPaymentTermsDays":0,"defaultCurrency":"EUR","language":"de","tags":["string"],"notes":"string","categoryId":"00000000-0000-4000-8000-000000000000","nameSuffix":"string","customerNumber":"string","debtorNumber":"string","creditorNumber":"string","vatId":"string","eInvoiceDefault":true,"iban":"string","bic":"string","bankName":"string","accountHolder":"string","taxExempt":true,"taxExemptReason":"innergemeinschaftlich","ansprechpartnerName":"string","ansprechpartnerEmail":"beispiel@example.com","ansprechpartnerTelefon":"string","ansprechpartnerPosition":"string","customFields":{}}}}}}},"/api/v1/customers/search":{"get":{"responses":{"200":{"description":"Treffer-Liste — dieselbe Kundenform wie beim Lesen, inklusive Maskierung","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Kunden (UUID)"},"customerNumber":{"type":["string","null"],"description":"Kundennummer aus dem Nummernkreis des Mandanten"},"vatId":{"type":["string","null"],"description":"USt-IdNr.; unterhalb der Rolle accountant immer null"},"customerCategory":{"type":"string","description":"Kundenkategorie, Vorgabewert \"Interessent\""},"name":{"type":["string","null"],"description":"Anzeigename des Kunden"},"entityKind":{"type":"string","enum":["organization","person"],"description":"Firma oder natuerliche Person; Vorgabe \"organization\""},"salutation":{"type":["string","null"],"description":"Anrede (Herr, Frau, Divers, Firma)"},"email":{"type":["string","null"],"description":"E-Mail-Adresse"},"phone":{"type":["string","null"],"description":"Festnetznummer"},"mobile":{"type":["string","null"],"description":"Mobilnummer"},"phoneConsent":{"type":"boolean","description":"Einwilligung in telefonische Kontaktaufnahme; fehlt die Spalte, gilt false"},"website":{"type":["string","null"],"description":"Webadresse"},"company":{"type":["string","null"],"description":"Firmenname"},"type":{"type":["string","null"],"description":"Kundentyp, frei belegbar"},"status":{"type":["string","null"],"description":"Status: active, inactive oder blocked"},"address":{"description":"Hauptanschrift; Form haengt an den Mandantendaten"},"addresses":{"type":["array","null"],"items":{},"description":"Weitere Anschriften"},"paymentTerms":{"type":["string","null"],"description":"Zahlungsbedingung im Klartext"},"defaultPaymentTermsDays":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"null"}],"description":"Zahlungsziel in Tagen; je nach Zugriffsweg Zahl oder Zeichenkette"},"defaultCurrency":{"type":["string","null"],"description":"Bevorzugte Belegwaehrung (ISO-4217)"},"language":{"type":["string","null"],"description":"Belegsprache, Vorgabewert \"de\""},"ragEnabled":{"type":"boolean","description":"Wird der Datensatz in den KI-Kontext indexiert?"},"categoryId":{"type":["string","null"],"description":"Kennung der Kundenkategorie"},"nameSuffix":{"type":["string","null"],"description":"Namenszusatz"},"debtorNumber":{"type":["string","null"],"description":"Debitorennummer der Buchhaltung"},"creditorNumber":{"type":["string","null"],"description":"Kreditorennummer der Buchhaltung"},"eInvoiceDefault":{"type":"boolean","description":"E-Rechnung als Vorgabe fuer diesen Kunden"},"taxExempt":{"type":"boolean","description":"Steuerbefreiung hinterlegt"},"taxExemptReason":{"type":["string","null"],"description":"Begruendung der Steuerbefreiung"},"iban":{"type":["string","null"],"description":"IBAN; unterhalb der Rolle accountant immer null"},"bic":{"type":["string","null"],"description":"BIC; unterhalb der Rolle accountant immer null"},"bankName":{"type":["string","null"],"description":"Bankname; unterhalb der Rolle accountant immer null"},"accountHolder":{"type":["string","null"],"description":"Kontoinhaber; unterhalb der Rolle accountant immer null"},"ansprechpartnerName":{"type":["string","null"],"description":"Name des Hauptansprechpartners"},"ansprechpartnerEmail":{"type":["string","null"],"description":"E-Mail des Hauptansprechpartners"},"ansprechpartnerTelefon":{"type":["string","null"],"description":"Telefon des Hauptansprechpartners"},"ansprechpartnerPosition":{"type":["string","null"],"description":"Position des Hauptansprechpartners"},"tags":{"type":["array","null"],"items":{},"description":"Freie Schlagworte"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"description":"Anlagezeitpunkt als ISO-Zeichenkette oder null"},"updatedAt":{"description":"Letzte Aenderung als ISO-Zeichenkette oder null"}},"required":["id","customerNumber","vatId","customerCategory","entityKind","salutation","mobile","phoneConsent","website","paymentTerms","defaultPaymentTermsDays","defaultCurrency","language","ragEnabled","categoryId","nameSuffix","debtorNumber","creditorNumber","eInvoiceDefault","taxExempt","taxExemptReason","iban","bic","bankName","accountHolder","ansprechpartnerName","ansprechpartnerEmail","ansprechpartnerTelefon","ansprechpartnerPosition"],"additionalProperties":false},"description":"Gefundene Kunden, maskiert wie beim Lesen"},"total":{"type":"integer","minimum":0,"description":"Anzahl der zurueckgegebenen Treffer — nicht die Gesamtzahl der Datenbank"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"string","customerNumber":"string","vatId":"string","customerCategory":"string","name":"string","entityKind":"organization","salutation":"string","email":"string","phone":"string","mobile":"string","phoneConsent":true,"website":"string","company":"string","type":"string","status":"string","addresses":[],"paymentTerms":"string","defaultPaymentTermsDays":0,"defaultCurrency":"string","language":"string","ragEnabled":true,"categoryId":"string","nameSuffix":"string","debtorNumber":"string","creditorNumber":"string","eInvoiceDefault":true,"taxExempt":true,"taxExemptReason":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","ansprechpartnerName":"string","ansprechpartnerEmail":"string","ansprechpartnerTelefon":"string","ansprechpartnerPosition":"string","tags":[],"notes":"string","customFields":{}}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar — es wird NIE eine leere Trefferliste vorgetäuscht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CustomersSearch","tags":["customers"],"parameters":[],"summary":"Search customers","description":"Volltext-Suche über Kunden des Mandanten. `limit` wird auf 1..100 gekappt; ein höherer Wert liefert trotzdem höchstens 100 Treffer. Die Antwort trägt KEINEN `pagination`-Block — `total` ist die Länge der gelieferten Liste, nicht die Gesamtzahl der passenden Kunden."}},"/api/v1/customers/{id}":{"get":{"responses":{"200":{"description":"Kundendatensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Kunden (UUID)"},"customerNumber":{"type":["string","null"],"description":"Kundennummer aus dem Nummernkreis des Mandanten"},"vatId":{"type":["string","null"],"description":"USt-IdNr.; unterhalb der Rolle accountant immer null"},"customerCategory":{"type":"string","description":"Kundenkategorie, Vorgabewert \"Interessent\""},"name":{"type":["string","null"],"description":"Anzeigename des Kunden"},"entityKind":{"type":"string","enum":["organization","person"],"description":"Firma oder natuerliche Person; Vorgabe \"organization\""},"salutation":{"type":["string","null"],"description":"Anrede (Herr, Frau, Divers, Firma)"},"email":{"type":["string","null"],"description":"E-Mail-Adresse"},"phone":{"type":["string","null"],"description":"Festnetznummer"},"mobile":{"type":["string","null"],"description":"Mobilnummer"},"phoneConsent":{"type":"boolean","description":"Einwilligung in telefonische Kontaktaufnahme; fehlt die Spalte, gilt false"},"website":{"type":["string","null"],"description":"Webadresse"},"company":{"type":["string","null"],"description":"Firmenname"},"type":{"type":["string","null"],"description":"Kundentyp, frei belegbar"},"status":{"type":["string","null"],"description":"Status: active, inactive oder blocked"},"address":{"description":"Hauptanschrift; Form haengt an den Mandantendaten"},"addresses":{"type":["array","null"],"items":{},"description":"Weitere Anschriften"},"paymentTerms":{"type":["string","null"],"description":"Zahlungsbedingung im Klartext"},"defaultPaymentTermsDays":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"null"}],"description":"Zahlungsziel in Tagen; je nach Zugriffsweg Zahl oder Zeichenkette"},"defaultCurrency":{"type":["string","null"],"description":"Bevorzugte Belegwaehrung (ISO-4217)"},"language":{"type":["string","null"],"description":"Belegsprache, Vorgabewert \"de\""},"ragEnabled":{"type":"boolean","description":"Wird der Datensatz in den KI-Kontext indexiert?"},"categoryId":{"type":["string","null"],"description":"Kennung der Kundenkategorie"},"nameSuffix":{"type":["string","null"],"description":"Namenszusatz"},"debtorNumber":{"type":["string","null"],"description":"Debitorennummer der Buchhaltung"},"creditorNumber":{"type":["string","null"],"description":"Kreditorennummer der Buchhaltung"},"eInvoiceDefault":{"type":"boolean","description":"E-Rechnung als Vorgabe fuer diesen Kunden"},"taxExempt":{"type":"boolean","description":"Steuerbefreiung hinterlegt"},"taxExemptReason":{"type":["string","null"],"description":"Begruendung der Steuerbefreiung"},"iban":{"type":["string","null"],"description":"IBAN; unterhalb der Rolle accountant immer null"},"bic":{"type":["string","null"],"description":"BIC; unterhalb der Rolle accountant immer null"},"bankName":{"type":["string","null"],"description":"Bankname; unterhalb der Rolle accountant immer null"},"accountHolder":{"type":["string","null"],"description":"Kontoinhaber; unterhalb der Rolle accountant immer null"},"ansprechpartnerName":{"type":["string","null"],"description":"Name des Hauptansprechpartners"},"ansprechpartnerEmail":{"type":["string","null"],"description":"E-Mail des Hauptansprechpartners"},"ansprechpartnerTelefon":{"type":["string","null"],"description":"Telefon des Hauptansprechpartners"},"ansprechpartnerPosition":{"type":["string","null"],"description":"Position des Hauptansprechpartners"},"tags":{"type":["array","null"],"items":{},"description":"Freie Schlagworte"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"description":"Anlagezeitpunkt als ISO-Zeichenkette oder null"},"updatedAt":{"description":"Letzte Aenderung als ISO-Zeichenkette oder null"}},"required":["id","customerNumber","vatId","customerCategory","entityKind","salutation","mobile","phoneConsent","website","paymentTerms","defaultPaymentTermsDays","defaultCurrency","language","ragEnabled","categoryId","nameSuffix","debtorNumber","creditorNumber","eInvoiceDefault","taxExempt","taxExemptReason","iban","bic","bankName","accountHolder","ansprechpartnerName","ansprechpartnerEmail","ansprechpartnerTelefon","ansprechpartnerPosition"],"additionalProperties":false},"example":{"id":"string","customerNumber":"string","vatId":"string","customerCategory":"string","name":"string","entityKind":"organization","salutation":"string","email":"string","phone":"string","mobile":"string","phoneConsent":true,"website":"string","company":"string","type":"string","status":"string","addresses":[],"paymentTerms":"string","defaultPaymentTermsDays":0,"defaultCurrency":"string","language":"string","ragEnabled":true,"categoryId":"string","nameSuffix":"string","debtorNumber":"string","creditorNumber":"string","eInvoiceDefault":true,"taxExempt":true,"taxExemptReason":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","ansprechpartnerName":"string","ansprechpartnerEmail":"string","ansprechpartnerTelefon":"string","ansprechpartnerPosition":"string","tags":[],"notes":"string","customFields":{}}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Kunde nicht gefunden"}},"operationId":"getApiV1CustomersById","tags":["customers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get customer","description":"Liefert einen einzelnen Kunden anhand seiner ID"},"put":{"responses":{"200":{"description":"Kunde aktualisiert — dieselbe Form wie GET /:id, inklusive Maskierung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Kunden (UUID)"},"customerNumber":{"type":["string","null"],"description":"Kundennummer aus dem Nummernkreis des Mandanten"},"vatId":{"type":["string","null"],"description":"USt-IdNr.; unterhalb der Rolle accountant immer null"},"customerCategory":{"type":"string","description":"Kundenkategorie, Vorgabewert \"Interessent\""},"name":{"type":["string","null"],"description":"Anzeigename des Kunden"},"entityKind":{"type":"string","enum":["organization","person"],"description":"Firma oder natuerliche Person; Vorgabe \"organization\""},"salutation":{"type":["string","null"],"description":"Anrede (Herr, Frau, Divers, Firma)"},"email":{"type":["string","null"],"description":"E-Mail-Adresse"},"phone":{"type":["string","null"],"description":"Festnetznummer"},"mobile":{"type":["string","null"],"description":"Mobilnummer"},"phoneConsent":{"type":"boolean","description":"Einwilligung in telefonische Kontaktaufnahme; fehlt die Spalte, gilt false"},"website":{"type":["string","null"],"description":"Webadresse"},"company":{"type":["string","null"],"description":"Firmenname"},"type":{"type":["string","null"],"description":"Kundentyp, frei belegbar"},"status":{"type":["string","null"],"description":"Status: active, inactive oder blocked"},"address":{"description":"Hauptanschrift; Form haengt an den Mandantendaten"},"addresses":{"type":["array","null"],"items":{},"description":"Weitere Anschriften"},"paymentTerms":{"type":["string","null"],"description":"Zahlungsbedingung im Klartext"},"defaultPaymentTermsDays":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"null"}],"description":"Zahlungsziel in Tagen; je nach Zugriffsweg Zahl oder Zeichenkette"},"defaultCurrency":{"type":["string","null"],"description":"Bevorzugte Belegwaehrung (ISO-4217)"},"language":{"type":["string","null"],"description":"Belegsprache, Vorgabewert \"de\""},"ragEnabled":{"type":"boolean","description":"Wird der Datensatz in den KI-Kontext indexiert?"},"categoryId":{"type":["string","null"],"description":"Kennung der Kundenkategorie"},"nameSuffix":{"type":["string","null"],"description":"Namenszusatz"},"debtorNumber":{"type":["string","null"],"description":"Debitorennummer der Buchhaltung"},"creditorNumber":{"type":["string","null"],"description":"Kreditorennummer der Buchhaltung"},"eInvoiceDefault":{"type":"boolean","description":"E-Rechnung als Vorgabe fuer diesen Kunden"},"taxExempt":{"type":"boolean","description":"Steuerbefreiung hinterlegt"},"taxExemptReason":{"type":["string","null"],"description":"Begruendung der Steuerbefreiung"},"iban":{"type":["string","null"],"description":"IBAN; unterhalb der Rolle accountant immer null"},"bic":{"type":["string","null"],"description":"BIC; unterhalb der Rolle accountant immer null"},"bankName":{"type":["string","null"],"description":"Bankname; unterhalb der Rolle accountant immer null"},"accountHolder":{"type":["string","null"],"description":"Kontoinhaber; unterhalb der Rolle accountant immer null"},"ansprechpartnerName":{"type":["string","null"],"description":"Name des Hauptansprechpartners"},"ansprechpartnerEmail":{"type":["string","null"],"description":"E-Mail des Hauptansprechpartners"},"ansprechpartnerTelefon":{"type":["string","null"],"description":"Telefon des Hauptansprechpartners"},"ansprechpartnerPosition":{"type":["string","null"],"description":"Position des Hauptansprechpartners"},"tags":{"type":["array","null"],"items":{},"description":"Freie Schlagworte"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"description":"Anlagezeitpunkt als ISO-Zeichenkette oder null"},"updatedAt":{"description":"Letzte Aenderung als ISO-Zeichenkette oder null"}},"required":["id","customerNumber","vatId","customerCategory","entityKind","salutation","mobile","phoneConsent","website","paymentTerms","defaultPaymentTermsDays","defaultCurrency","language","ragEnabled","categoryId","nameSuffix","debtorNumber","creditorNumber","eInvoiceDefault","taxExempt","taxExemptReason","iban","bic","bankName","accountHolder","ansprechpartnerName","ansprechpartnerEmail","ansprechpartnerTelefon","ansprechpartnerPosition"],"additionalProperties":false},"example":{"id":"string","customerNumber":"string","vatId":"string","customerCategory":"string","name":"string","entityKind":"organization","salutation":"string","email":"string","phone":"string","mobile":"string","phoneConsent":true,"website":"string","company":"string","type":"string","status":"string","addresses":[],"paymentTerms":"string","defaultPaymentTermsDays":0,"defaultCurrency":"string","language":"string","ragEnabled":true,"categoryId":"string","nameSuffix":"string","debtorNumber":"string","creditorNumber":"string","eInvoiceDefault":true,"taxExempt":true,"taxExemptReason":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","ansprechpartnerName":"string","ansprechpartnerEmail":"string","ansprechpartnerTelefon":"string","ansprechpartnerPosition":"string","tags":[],"notes":"string","customFields":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Kunde nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"customer_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1CustomersById","tags":["customers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace customer","description":"Ersetzt einen Kunden vollständig durch die übergebenen Daten. Anders als PATCH /:id läuft dieser Weg NICHT durch die Entity-Rules und NICHT durch den custom_fields-Merge — mitgeschickte `customFields` ersetzen den bisherigen Stand. Unterhalb der Rolle `accountant` kommen vatId/iban/bic/bankName/accountHolder in der Antwort als `null` zurück.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entityKind":{"type":"string","enum":["organization","person"],"default":"organization"},"name":{"type":"string","minLength":2,"maxLength":255},"customerCategory":{"type":"string","enum":["Interessent","Geschäftskunde","Privatkunde","Sonstige"]},"salutation":{"type":"string","enum":["Herr","Frau","Divers","Firma"]},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string"},"mobile":{"type":"string"},"phoneConsent":{"type":"boolean"},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"company":{"type":"string"},"type":{"type":"string","minLength":1,"maxLength":120,"default":"Interessent"},"status":{"type":"string","enum":["active","inactive","blocked"],"default":"active"},"address":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":120,"default":"Rechnungsadresse"},"label":{"type":"string","maxLength":120},"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"},"email":{"type":"string","maxLength":200},"phone":{"type":"string","maxLength":60}}},"addresses":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":120,"default":"Rechnungsadresse"},"label":{"type":"string","maxLength":120},"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"},"email":{"type":"string","maxLength":200},"phone":{"type":"string","maxLength":60}}}},"paymentTerms":{"type":"string","maxLength":100},"defaultPaymentTermsDays":{"type":["integer","null"],"minimum":0,"maximum":365},"defaultCurrency":{"type":"string","enum":["EUR","USD","GBP","CHF","JPY","PLN","CZK","HUF"]},"language":{"type":"string","enum":["de","en","fr","es","pl","nl","da","cs","zh"]},"tags":{"type":"array","items":{"type":"string"}},"notes":{"type":"string"},"categoryId":{"type":"string","format":"uuid"},"nameSuffix":{"type":"string","maxLength":255},"customerNumber":{"type":"string","maxLength":20},"debtorNumber":{"type":"string","maxLength":20},"creditorNumber":{"type":"string","maxLength":20},"vatId":{"type":"string","maxLength":40},"eInvoiceDefault":{"type":"boolean"},"iban":{"type":["string","null"],"maxLength":40},"bic":{"type":["string","null"],"maxLength":20},"bankName":{"type":["string","null"],"maxLength":120},"accountHolder":{"type":["string","null"],"maxLength":255},"taxExempt":{"type":"boolean"},"taxExemptReason":{"type":["string","null"],"enum":["innergemeinschaftlich","drittland","kleinunternehmer","reverse_charge",null]},"ansprechpartnerName":{"type":"string","maxLength":255},"ansprechpartnerEmail":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"ansprechpartnerTelefon":{"type":"string","maxLength":50},"ansprechpartnerPosition":{"type":"string","maxLength":120},"customFields":{"type":"object","additionalProperties":{}}},"required":["name"]},"example":{"entityKind":"organization","name":"string","customerCategory":"Interessent","salutation":"Herr","email":"beispiel@example.com","phone":"string","mobile":"string","phoneConsent":true,"website":"https://example.com","company":"string","type":"string","status":"active","address":{"type":"string","label":"string","street":"string","city":"string","zip":"string","country":"string","email":"string","phone":"string"},"addresses":[{"type":"string","label":"string","street":"string","city":"string","zip":"string","country":"string","email":"string","phone":"string"}],"paymentTerms":"string","defaultPaymentTermsDays":0,"defaultCurrency":"EUR","language":"de","tags":["string"],"notes":"string","categoryId":"00000000-0000-4000-8000-000000000000","nameSuffix":"string","customerNumber":"string","debtorNumber":"string","creditorNumber":"string","vatId":"string","eInvoiceDefault":true,"iban":"string","bic":"string","bankName":"string","accountHolder":"string","taxExempt":true,"taxExemptReason":"innergemeinschaftlich","ansprechpartnerName":"string","ansprechpartnerEmail":"beispiel@example.com","ansprechpartnerTelefon":"string","ansprechpartnerPosition":"string","customFields":{}}}}}},"patch":{"responses":{"200":{"description":"Kunde aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Kunden (UUID)"},"customerNumber":{"type":["string","null"],"description":"Kundennummer aus dem Nummernkreis des Mandanten"},"vatId":{"type":["string","null"],"description":"USt-IdNr.; unterhalb der Rolle accountant immer null"},"customerCategory":{"type":"string","description":"Kundenkategorie, Vorgabewert \"Interessent\""},"name":{"type":["string","null"],"description":"Anzeigename des Kunden"},"entityKind":{"type":"string","enum":["organization","person"],"description":"Firma oder natuerliche Person; Vorgabe \"organization\""},"salutation":{"type":["string","null"],"description":"Anrede (Herr, Frau, Divers, Firma)"},"email":{"type":["string","null"],"description":"E-Mail-Adresse"},"phone":{"type":["string","null"],"description":"Festnetznummer"},"mobile":{"type":["string","null"],"description":"Mobilnummer"},"phoneConsent":{"type":"boolean","description":"Einwilligung in telefonische Kontaktaufnahme; fehlt die Spalte, gilt false"},"website":{"type":["string","null"],"description":"Webadresse"},"company":{"type":["string","null"],"description":"Firmenname"},"type":{"type":["string","null"],"description":"Kundentyp, frei belegbar"},"status":{"type":["string","null"],"description":"Status: active, inactive oder blocked"},"address":{"description":"Hauptanschrift; Form haengt an den Mandantendaten"},"addresses":{"type":["array","null"],"items":{},"description":"Weitere Anschriften"},"paymentTerms":{"type":["string","null"],"description":"Zahlungsbedingung im Klartext"},"defaultPaymentTermsDays":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"null"}],"description":"Zahlungsziel in Tagen; je nach Zugriffsweg Zahl oder Zeichenkette"},"defaultCurrency":{"type":["string","null"],"description":"Bevorzugte Belegwaehrung (ISO-4217)"},"language":{"type":["string","null"],"description":"Belegsprache, Vorgabewert \"de\""},"ragEnabled":{"type":"boolean","description":"Wird der Datensatz in den KI-Kontext indexiert?"},"categoryId":{"type":["string","null"],"description":"Kennung der Kundenkategorie"},"nameSuffix":{"type":["string","null"],"description":"Namenszusatz"},"debtorNumber":{"type":["string","null"],"description":"Debitorennummer der Buchhaltung"},"creditorNumber":{"type":["string","null"],"description":"Kreditorennummer der Buchhaltung"},"eInvoiceDefault":{"type":"boolean","description":"E-Rechnung als Vorgabe fuer diesen Kunden"},"taxExempt":{"type":"boolean","description":"Steuerbefreiung hinterlegt"},"taxExemptReason":{"type":["string","null"],"description":"Begruendung der Steuerbefreiung"},"iban":{"type":["string","null"],"description":"IBAN; unterhalb der Rolle accountant immer null"},"bic":{"type":["string","null"],"description":"BIC; unterhalb der Rolle accountant immer null"},"bankName":{"type":["string","null"],"description":"Bankname; unterhalb der Rolle accountant immer null"},"accountHolder":{"type":["string","null"],"description":"Kontoinhaber; unterhalb der Rolle accountant immer null"},"ansprechpartnerName":{"type":["string","null"],"description":"Name des Hauptansprechpartners"},"ansprechpartnerEmail":{"type":["string","null"],"description":"E-Mail des Hauptansprechpartners"},"ansprechpartnerTelefon":{"type":["string","null"],"description":"Telefon des Hauptansprechpartners"},"ansprechpartnerPosition":{"type":["string","null"],"description":"Position des Hauptansprechpartners"},"tags":{"type":["array","null"],"items":{},"description":"Freie Schlagworte"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"description":"Anlagezeitpunkt als ISO-Zeichenkette oder null"},"updatedAt":{"description":"Letzte Aenderung als ISO-Zeichenkette oder null"}},"required":["id","customerNumber","vatId","customerCategory","entityKind","salutation","mobile","phoneConsent","website","paymentTerms","defaultPaymentTermsDays","defaultCurrency","language","ragEnabled","categoryId","nameSuffix","debtorNumber","creditorNumber","eInvoiceDefault","taxExempt","taxExemptReason","iban","bic","bankName","accountHolder","ansprechpartnerName","ansprechpartnerEmail","ansprechpartnerTelefon","ansprechpartnerPosition"],"additionalProperties":false},"example":{"id":"string","customerNumber":"string","vatId":"string","customerCategory":"string","name":"string","entityKind":"organization","salutation":"string","email":"string","phone":"string","mobile":"string","phoneConsent":true,"website":"string","company":"string","type":"string","status":"string","addresses":[],"paymentTerms":"string","defaultPaymentTermsDays":0,"defaultCurrency":"string","language":"string","ragEnabled":true,"categoryId":"string","nameSuffix":"string","debtorNumber":"string","creditorNumber":"string","eInvoiceDefault":true,"taxExempt":true,"taxExemptReason":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","ansprechpartnerName":"string","ansprechpartnerEmail":"string","ansprechpartnerTelefon":"string","ansprechpartnerPosition":"string","tags":[],"notes":"string","customFields":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Kunde nicht gefunden"},"422":{"description":"Entity-Rule-Verletzung (mandantenspezifische Pflichtfeld-/Wertregel)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"customers.update","tags":["customers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update customer partially","description":"Aktualisiert einzelne Felder eines Kunden","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entityKind":{"type":"string","enum":["organization","person"],"default":"organization"},"name":{"type":"string","minLength":2,"maxLength":255},"customerCategory":{"type":"string","enum":["Interessent","Geschäftskunde","Privatkunde","Sonstige"]},"salutation":{"type":"string","enum":["Herr","Frau","Divers","Firma"]},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string"},"mobile":{"type":"string"},"phoneConsent":{"type":"boolean"},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"company":{"type":"string"},"type":{"type":"string","minLength":1,"maxLength":120,"default":"Interessent"},"status":{"type":"string","enum":["active","inactive","blocked"],"default":"active"},"address":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":120,"default":"Rechnungsadresse"},"label":{"type":"string","maxLength":120},"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"},"email":{"type":"string","maxLength":200},"phone":{"type":"string","maxLength":60}}},"addresses":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":120,"default":"Rechnungsadresse"},"label":{"type":"string","maxLength":120},"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"},"email":{"type":"string","maxLength":200},"phone":{"type":"string","maxLength":60}}}},"paymentTerms":{"type":"string","maxLength":100},"defaultPaymentTermsDays":{"type":["integer","null"],"minimum":0,"maximum":365},"defaultCurrency":{"type":"string","enum":["EUR","USD","GBP","CHF","JPY","PLN","CZK","HUF"]},"language":{"type":"string","enum":["de","en","fr","es","pl","nl","da","cs","zh"]},"tags":{"type":"array","items":{"type":"string"}},"notes":{"type":"string"},"categoryId":{"type":"string","format":"uuid"},"nameSuffix":{"type":"string","maxLength":255},"customerNumber":{"type":"string","maxLength":20},"debtorNumber":{"type":"string","maxLength":20},"creditorNumber":{"type":"string","maxLength":20},"vatId":{"type":"string","maxLength":40},"eInvoiceDefault":{"type":"boolean"},"iban":{"type":["string","null"],"maxLength":40},"bic":{"type":["string","null"],"maxLength":20},"bankName":{"type":["string","null"],"maxLength":120},"accountHolder":{"type":["string","null"],"maxLength":255},"taxExempt":{"type":"boolean"},"taxExemptReason":{"type":["string","null"],"enum":["innergemeinschaftlich","drittland","kleinunternehmer","reverse_charge",null]},"ansprechpartnerName":{"type":"string","maxLength":255},"ansprechpartnerEmail":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"ansprechpartnerTelefon":{"type":"string","maxLength":50},"ansprechpartnerPosition":{"type":"string","maxLength":120},"customFields":{"type":"object","additionalProperties":{}}}},"example":{"entityKind":"organization","name":"string","customerCategory":"Interessent","salutation":"Herr","email":"beispiel@example.com","phone":"string","mobile":"string","phoneConsent":true,"website":"https://example.com","company":"string","type":"string","status":"active","address":{"type":"string","label":"string","street":"string","city":"string","zip":"string","country":"string","email":"string","phone":"string"},"addresses":[{"type":"string","label":"string","street":"string","city":"string","zip":"string","country":"string","email":"string","phone":"string"}],"paymentTerms":"string","defaultPaymentTermsDays":0,"defaultCurrency":"EUR","language":"de","tags":["string"],"notes":"string","categoryId":"00000000-0000-4000-8000-000000000000","nameSuffix":"string","customerNumber":"string","debtorNumber":"string","creditorNumber":"string","vatId":"string","eInvoiceDefault":true,"iban":"string","bic":"string","bankName":"string","accountHolder":"string","taxExempt":true,"taxExemptReason":"innergemeinschaftlich","ansprechpartnerName":"string","ansprechpartnerEmail":"beispiel@example.com","ansprechpartnerTelefon":"string","ansprechpartnerPosition":"string","customFields":{}}}}}},"delete":{"responses":{"200":{"description":"Kunde gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","minLength":1,"description":"Erfolgsmeldung im Klartext, enthaelt die Kunden-Id"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb der Mindestrolle \"user\""},"404":{"description":"Kontakt nicht gefunden"},"409":{"description":"Kontakt hat verknüpfte Belege"},"503":{"description":"Datenbank nicht erreichbar, oder die Beleg-Prüfung vor dem Löschen ist fehlgeschlagen"}},"operationId":"customers.delete","tags":["customers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete customer (soft delete)","description":"Löscht (Soft-Delete) einen KUNDEN — nicht, wie hier früher stand, einen Ansprechpartner; die Ansprechpartner haben eine eigene Route unter /contacts. Verfuegbar fuer alle authentifizierten User; der Beleg-Check unten lehnt das Loeschen mit 409 ab, wenn Auftraege/Rechnungen/Bestellungen verknuepft sind."}},"/api/v1/customers/{id}/stats":{"get":{"responses":{"200":{"description":"Zähler je Beleggattung. ACHTUNG: lauter Nullen bedeutet auch dann 200, wenn die Datenbank nicht erreichbar war oder die Kunden-Id nicht existiert — ein Fehler ist von „nichts vorhanden\" nicht zu unterscheiden.","content":{"application/json":{"schema":{"type":"object","properties":{"invoices":{"type":"integer","minimum":0,"description":"Anzahl Rechnungen des Kunden"},"orders":{"type":"integer","minimum":0,"description":"Anzahl Auftraege des Kunden"},"quotes":{"type":"integer","minimum":0,"description":"KEINE Angebote: Auftraege im Status draft oder sent (Behelfszaehlung)"},"deliveries":{"type":"integer","minimum":0,"description":"Fest verdrahtete 0 — auch wenn der Kunde Lieferscheine hat"}},"required":["invoices","orders","quotes","deliveries"],"additionalProperties":false},"example":{"invoices":0,"orders":0,"quotes":0,"deliveries":0}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1CustomersByIdStats","tags":["customers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Count related records of a customer","description":"Zähler verwandter Datensätze für die SmartButtons der Kundenmaske. ACHTUNG — zwei der vier Zahlen sind nicht, was ihr Name sagt: `quotes` zählt keine Angebote, sondern Aufträge im Status `draft` oder `sent` (Behelfslösung im Handler), und `deliveries` ist ein fest verdrahtetes `0` und bleibt es auch dann, wenn der Kunde Lieferscheine hat. Wer echte Zahlen braucht, zählt über GET /quotes bzw. GET /deliveries mit `customerId`. Ist die Datenbank nicht erreichbar, antwortet der Aufruf mit 200 und lauter Nullen — ein Fehler ist von „nichts vorhanden\" also nicht zu unterscheiden. Eine unbekannte Kunden-ID ergibt ebenfalls Nullen, kein 404."}},"/api/v1/customers/{id}/default-currency":{"get":{"responses":{"200":{"description":"Standardwährung. `EUR` kommt in drei Fällen: hinterlegt, nicht hinterlegt, oder Datenbank stumm — die Antwort unterscheidet sie nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string","minLength":3,"maxLength":3,"description":"Waehrungscode (ISO-4217); \"EUR\" auch dann, wenn nichts hinterlegt oder die Datenbank stumm ist"}},"required":["currency"],"additionalProperties":false},"example":{"currency":"str"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kunde nicht gefunden — nur wenn die Datenbank antwortet und die Zeile fehlt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"customer_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1CustomersByIdDefault-currency","tags":["customers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get preferred invoice currency","description":"Gibt die bevorzugte Rechnungswährung des Kunden zurück. Hat der Kunde keine hinterlegt, kommt `EUR`. Dasselbe `EUR` kommt auch, wenn die Datenbank nicht erreichbar ist — die Antwort ist dann trotzdem 200. Ein `EUR` in dieser Antwort ist also keine Zusage, dass der Kunde wirklich in Euro fakturiert wird."}},"/api/v1/customers/{id}/churn-risk":{"get":{"responses":{"200":{"description":"Churn-Risikobewertung. `source` sagt, ob ein Modell beteiligt war — die Regelzahlen unter `stats` stehen in beiden Fällen.","content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid","description":"Kunden-Id aus dem Pfad"},"customerName":{"type":["string","null"],"description":"Name des Kunden zum Zeitpunkt der Auswertung"},"overallRisk":{"type":"string","enum":["low","medium","high"],"description":"Gesamtbewertung; im KI-Zweig kann das Modell die Regel-Einstufung ueberschreiben"},"signals":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["low","medium","high"],"description":"Gewicht des Signals"},"note":{"type":"string","minLength":1,"description":"Begruendung im Klartext, deutsch"}},"required":["severity","note"],"additionalProperties":false},"description":"Die einzelnen Abwanderungssignale"},"stats":{"type":"object","properties":{"totalInvoices":{"type":"integer","minimum":0,"description":"Betrachtete Rechnungen (hoechstens 100)"},"daysSinceLastInvoice":{"type":["integer","null"],"minimum":0,"description":"Tage seit der letzten Rechnung; null, wenn es keine gibt"},"recentVolume":{"type":"number","description":"Summe der letzten drei Rechnungen"},"previousVolume":{"type":"number","description":"Summe der drei davor"}},"required":["totalInvoices","daysSinceLastInvoice","recentVolume","previousVolume"],"additionalProperties":false,"description":"Die Rohzahlen hinter der Bewertung"},"analysedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Auswertung (ISO)"},"source":{"type":"string","enum":["rule-based","claude-haiku-4-5"],"description":"\"rule-based\" heisst: kein Modell befragt (nicht konfiguriert, keine Daten oder KI-Fehler)"},"summary":{"type":"string","description":"Gesamtbewertung in Worten"},"recommendations":{"type":"array","items":{"type":"string"},"description":"Naechste Schritte; im regelbasierten Zweig immer leer"}},"required":["customerId","overallRisk","signals","stats","analysedAt","source","summary","recommendations"],"additionalProperties":false},"example":{"customerId":"00000000-0000-4000-8000-000000000000","customerName":"string","overallRisk":"low","signals":[{"severity":"low","note":"string"}],"stats":{"totalInvoices":0,"daysSinceLastInvoice":0,"recentVolume":0,"previousVolume":0},"analysedAt":"2026-01-01T12:00:00.000Z","source":"rule-based","summary":"string","recommendations":["string"]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Kunde nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"customer_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CustomersByIdChurn-risk","tags":["customers","ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Assess churn risk of a customer","description":"W24-G: Abwanderungsrisiko eines Kunden. Die Signale entstehen regelbasiert aus Rechnungsalter und Umsatzverlauf; ist ein Sprachmodell konfiguriert, formuliert es zusätzlich Zusammenfassung und Empfehlungen und darf die Einstufung überschreiben. Ohne Modell — oder wenn der Modellaufruf scheitert — antwortet der Aufruf trotzdem 200, dann mit `source: \"rule-based\"` und leeren `recommendations`. Sind die Rechnungen nicht lesbar, wird ohne sie gerechnet, ohne dass die Antwort das meldet."}},"/api/v1/rag/{entity}/{id}":{"patch":{"responses":{"200":{"description":"Der neue Freigabestand. Belegt das Kennzeichen, nicht die Einbettung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"entity":{"type":"string"},"ragEnabled":{"type":"boolean"}},"required":["id","entity","ragEnabled"],"additionalProperties":false},"example":{"id":"string","entity":"string","ragEnabled":true}}}},"400":{"description":"Kein Mandant aufgeloest, unzulaessiger Schema-Name oder eine Entitaet ausserhalb der festen Liste — als `text/plain`, ohne JSON-Koerper."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Datensatz mit dieser Kennung — als `text/plain`, ohne JSON-Koerper."},"503":{"description":"Keine Datenbankverbindung oder die Abfrage ist gescheitert.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}}},"operationId":"patchApiV1RagByEntityById","tags":["KI"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen Datensatz fuer die Wissenssuche freigeben oder sperren","description":"Setzt die Freigabe eines einzelnen Datensatzes fuer die Wissenssuche der\nKI („In KI-Kontext aufnehmen\"). Geschrieben wird die Spalte\n`rag_enabled` der Mandantentabelle.\n\nDER AUFRUF WIRKT AUF DIE EINBETTUNGEN, nicht nur auf ein Kennzeichen.\nNach dem Schreiben wird nebenlaeufig `triggerEntityEmbed` angestossen:\n\n  · `ragEnabled: true` — der Datensatz wird eingebettet und dauerhaft im\n    Vektorspeicher abgelegt. Das ruft den Einbettungs-Anbieter auf und\n    kostet dort Geld. Es ist KEIN Sprachmodell-Aufruf und zaehlt nicht\n    gegen das monatliche KI-Kontingent.\n  · `ragEnabled: false` — die Einbettung des Datensatzes wird wieder\n    entfernt.\n\nBeide Richtungen sind umkehrbar: derselbe Aufruf mit dem anderen Wert\nstellt den vorigen Zustand her. Der Fachdatensatz selbst wird NICHT\nveraendert — ausser `updated_at`, das mitgezogen wird.\n\nDIE ANTWORT BELEGT NUR DAS KENNZEICHEN. Das Einbetten laeuft\n„abschicken und vergessen\" neben der Antwort; scheitert es, bleibt die\nAntwort 200 und `ragEnabled` steht trotzdem auf dem neuen Wert. Ob der\nDatensatz wirklich auffindbar ist, sagt dieser Aufruf nicht.\n\nDER SCHREIBWEG HEILT DAS SCHEMA. Fehlt die Spalte `rag_enabled`, legt\nder Handler sie einmal je Schema und Tabelle an\n(`ADD COLUMN IF NOT EXISTS`). Das nimmt kurz eine\nACCESS-EXCLUSIVE-Sperre auf `customers`, `products` oder `invoices` —\nalso auf den meistgelesenen Tabellen des Mandanten. Der Leseweg\n(`GET`) tut das bewusst NICHT.\n\n`entity` muss in der festen Liste stehen (`customers`, `products`,\n`invoices`), sonst 400. Der Tabellenname wird NICHT aus dem Aufruf\ngebildet, sondern aus dieser Liste geholt.\n\nEin unbekannter Datensatz ist 404 — die Freigabe wird also nicht\n„vorsorglich\" angelegt.\n\nDer Zwischenspeicher der Liste wird verworfen, damit ein folgendes GET\nden neuen Stand zeigt; das geschieht bestmoeglich und laesst den\nSchreibvorgang nie scheitern.\n\nFehler ausser 503 kommen als `text/plain` (HTTPException), nicht als\nJSON. Keine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ragEnabled":{"type":"boolean"}},"required":["ragEnabled"]},"example":{"ragEnabled":true}}}}},"get":{"responses":{"200":{"description":"Freigabestand des Datensatzes.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"entity":{"type":"string"},"ragEnabled":{"type":"boolean"}},"required":["id","entity","ragEnabled"]},"example":{"id":"string","entity":"string","ragEnabled":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}}},"operationId":"getApiV1RagByEntityById","tags":["KI"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ist ein Datensatz fuer die Wissenssuche freigegeben?","description":"Sagt fuer einen einzelnen Datensatz, ob er in die Wissenssuche der KI\neinfliessen darf.\n\nDIESER GET FASST DAS SCHEMA NICHT AN (geaendert 17.08.2026). Bis dahin\nfuehrte er vor jedem Lesen ein\n`ALTER TABLE … ADD COLUMN IF NOT EXISTS rag_enabled` aus — eine\nACCESS-EXCLUSIVE-Sperre auf `customers`, `products` oder `invoices`, also\nauf den meistgelesenen Tabellen des Mandanten. Fehlt die Spalte, gilt der\nDatensatz jetzt schlicht als nicht freigegeben; die Selbstheilung ist auf\nden Schreibweg beschraenkt, wo sie hingehoert.\n\n`entity` muss in der festen Liste stehen, sonst 400. Der Tabellenname\nwird NICHT aus dem Aufruf gebildet, sondern aus dieser Liste geholt.\n\n`ragEnabled: false` heisst „nicht freigegeben\"; einen unbekannten\nDatensatz beantwortet die Route mit 404, nicht mit `false`. Die beiden\nFaelle sind also unterscheidbar.\n\nFehler ausser 503 kommen als `text/plain` (HTTPException).\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."}},"/api/v1/contacts/import":{"post":{"responses":{"200":{"description":"Importergebnis: Anzahl importierter und übersprungener Zeilen, dazu je nicht importierter Zeile eine Fehlermeldung (auf 25 Einträge gekappt)","content":{"application/json":{"schema":{"type":"object","properties":{"imported":{"type":"integer"},"skipped":{"type":"integer"},"errors":{"type":"array","items":{"type":"string"}}},"required":["imported","skipped","errors"],"additionalProperties":false},"example":{"imported":0,"skipped":0,"errors":["string"]}}}},"400":{"description":"Ungültiger Mandanten-Schlüssel, unlesbares Formular, keine Datei oder ein nicht unterstütztes Dateiformat (.xlsx)"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb der Mindestrolle \"user\""},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"contacts.import","tags":["contacts"],"parameters":[],"summary":"Kontakt-Stammdaten aus einer CSV-Datei importieren","description":"Nimmt eine CSV-Datei als Multipart-Feld `file` entgegen und legt daraus KONTAKT-STAMMDATEN an — also Zeilen in der Kunden-Tabelle (Kunde, Lieferant oder Interessent, je nach Spalte), NICHT Ansprechpartner. Fehlt die Kundennummer in einer Zeile, vergibt der Server sie aus dem Nummernkreis; geht das schief, wird die Zeile uebersprungen statt einen Datensatz ohne Nummer anzulegen. Jede Zeile steht fuer sich: eine fehlerhafte bricht den Import nicht ab, sie landet mit ihrer Zeilennummer in `errors` (gekappt auf 25 Meldungen). Der Import kennt kein Zusammenfuehren — dieselbe Datei zweimal ergibt die Datensaetze zweimal. `.xlsx` wird nicht gelesen (400)."}},"/api/v1/contacts":{"get":{"responses":{"200":{"description":"Liste der Ansprechpartner mit Seiteninformation","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"customer_id":{"type":"string"},"person_number":{"type":["string","null"]},"salutation":{"type":["string","null"]},"title":{"type":["string","null"]},"first_name":{"type":["string","null"]},"last_name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"mobile":{"type":["string","null"]},"role":{"type":["string","null"]},"department":{"type":["string","null"]},"is_primary":{"type":["boolean","null"]},"opt_in":{"type":["boolean","null"]},"notes":{"type":["string","null"]},"custom_fields":{"type":"object","additionalProperties":{}},"category_id":{"type":["string","null"]},"deleted_at":{"type":"null"},"created_at":{},"updated_at":{},"customer_name":{"type":["string","null"]},"customer_number":{"type":["string","null"]}},"required":["id","customer_id","person_number","salutation","title","first_name","last_name","email","phone","mobile","role","department","is_primary","opt_in","notes","custom_fields","category_id","customer_name","customer_number"],"additionalProperties":false}},"meta":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"pages":{"type":"integer"}},"required":["page","limit","total","pages"]}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","customer_id":"string","person_number":"string","salutation":"string","title":"string","first_name":"string","last_name":"string","email":"string","phone":"string","mobile":"string","role":"string","department":"string","is_primary":true,"opt_in":true,"notes":"string","custom_fields":{},"category_id":"string","deleted_at":null,"customer_name":"string","customer_number":"string"}],"meta":{"page":0,"limit":0,"total":0,"pages":0}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb der Mindestrolle \"user\""},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"contacts.list","tags":["contacts"],"parameters":[{"name":"page","in":"query","required":false,"description":"Seitenzahl, 1-basiert","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","required":false,"description":"Einträge pro Seite, bis 1000 — die CRM-Übersicht und die Kunden-Detailseite holen die komplette Ansprechpartner-Liste in einem einzigen Aufruf.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":25}},{"name":"customerId","in":"query","required":false,"description":"Nur Ansprechpartner dieses Kunden (UUID)","schema":{"type":"string","format":"uuid"}},{"name":"search","in":"query","required":false,"description":"Teilstring-Suche über Vorname, Nachname und E-Mail","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Sortierspalte: person_number, name (= last_name), first_name, company, role, status, email, phone oder created_at. Unbekannte Werte fallen still auf last_name zurück.","schema":{"type":"string"}},{"name":"order","in":"query","required":false,"description":"Sortierrichtung","schema":{"type":"string","enum":["asc","desc"],"default":"asc"}}],"summary":"Ansprechpartner auflisten — geblaettert, such- und sortierbar","description":"Listet die nicht geloeschten Ansprechpartner des Mandanten, sortiert nach Nachname und dann Vorname. `customerId` grenzt auf einen Kunden ein, `search` sucht als Teilstring in Vorname, Nachname und E-Mail. Geblaettert wird ueber `page` und `limit` (bis 1000, Vorgabe 25); `meta.total` zaehlt die Treffer OHNE Limit, `meta.pages` die Seitenzahl. Jede Zeile traegt zusaetzlich Name und Nummer des zugehoerigen Kunden aus dem Lese-Join — bei einem Ansprechpartner ohne Kunden bleiben beide leer."},"post":{"responses":{"201":{"description":"Ansprechpartner angelegt — die neue Zeile der Ansprechpartner-Tabelle selbst, ohne den Kundenname/-nummer-Zusatz aus dem Lesepfad (das Anlegen liest den Kunden nicht mit).","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"customer_id":{"type":"string"},"person_number":{"type":["string","null"]},"salutation":{"type":["string","null"]},"title":{"type":["string","null"]},"first_name":{"type":["string","null"]},"last_name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"mobile":{"type":["string","null"]},"role":{"type":["string","null"]},"department":{"type":["string","null"]},"is_primary":{"type":["boolean","null"]},"opt_in":{"type":["boolean","null"]},"notes":{"type":["string","null"]},"custom_fields":{"type":"object","additionalProperties":{}},"category_id":{"type":["string","null"]},"deleted_at":{"type":"null"},"created_at":{},"updated_at":{}},"required":["id","customer_id","person_number","salutation","title","first_name","last_name","email","phone","mobile","role","department","is_primary","opt_in","notes","custom_fields","category_id"],"additionalProperties":false},"example":{"id":"string","customer_id":"string","person_number":"string","salutation":"string","title":"string","first_name":"string","last_name":"string","email":"string","phone":"string","mobile":"string","role":"string","department":"string","is_primary":true,"opt_in":true,"notes":"string","custom_fields":{},"category_id":"string","deleted_at":null}}}},"400":{"description":"Validierungsfehler (der Validator antwortet ohne eigenen Hook mit dem Zod-Fehlerobjekt und Status 400)"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb der Mindestrolle \"user\""},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"contacts.create","tags":["contacts"],"parameters":[],"description":"Legt einen Ansprechpartner zu einem Kunden an. Pflicht sind `customerId` und `lastName`. Trotz der alten Bezeichnung wird KEINE Personennummer mehr vergeben — das Feld bleibt leer und existiert nur noch fuer Altdatensaetze. Mit `isPrimary: true` wird die Hauptansprech-Kennzeichnung bei allen anderen Ansprechpartnern DESSELBEN Kunden vorher entfernt; es gibt also immer nur einen Hauptansprechpartner. Der Kunde wird nicht auf Existenz geprueft. Die Antwort traegt die neue Zeile ohne den Kundenname/-nummer-Zusatz des Lesepfads.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"salutation":{"type":"string","enum":["Herr","Frau","Divers"]},"title":{"type":"string","maxLength":50},"firstName":{"type":"string","maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string","maxLength":50},"mobile":{"type":"string","maxLength":50},"role":{"type":"string","maxLength":100},"department":{"type":"string","maxLength":100},"isPrimary":{"type":"boolean","default":false},"optIn":{"type":"boolean","default":false},"notes":{"type":"string"},"customFields":{"type":"object","additionalProperties":{}}},"required":["customerId","lastName"]},"example":{"customerId":"00000000-0000-4000-8000-000000000000","salutation":"Herr","title":"string","firstName":"string","lastName":"string","email":"beispiel@example.com","phone":"string","mobile":"string","role":"string","department":"string","isPrimary":true,"optIn":true,"notes":"string","customFields":{}}}}},"summary":"Legt einen Ansprechpartner zu einem Kunden an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/contacts/{id}":{"get":{"responses":{"200":{"description":"Ansprechpartner-Datensatz inklusive Kundenname/-nummer aus dem Lese-Join","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"customer_id":{"type":"string"},"person_number":{"type":["string","null"]},"salutation":{"type":["string","null"]},"title":{"type":["string","null"]},"first_name":{"type":["string","null"]},"last_name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"mobile":{"type":["string","null"]},"role":{"type":["string","null"]},"department":{"type":["string","null"]},"is_primary":{"type":["boolean","null"]},"opt_in":{"type":["boolean","null"]},"notes":{"type":["string","null"]},"custom_fields":{"type":"object","additionalProperties":{}},"category_id":{"type":["string","null"]},"deleted_at":{"type":"null"},"created_at":{},"updated_at":{},"customer_name":{"type":["string","null"]},"customer_number":{"type":["string","null"]}},"required":["id","customer_id","person_number","salutation","title","first_name","last_name","email","phone","mobile","role","department","is_primary","opt_in","notes","custom_fields","category_id","customer_name","customer_number"],"additionalProperties":false},"example":{"id":"string","customer_id":"string","person_number":"string","salutation":"string","title":"string","first_name":"string","last_name":"string","email":"string","phone":"string","mobile":"string","role":"string","department":"string","is_primary":true,"opt_in":true,"notes":"string","custom_fields":{},"category_id":"string","deleted_at":null,"customer_name":"string","customer_number":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb der Mindestrolle \"user\""},"404":{"description":"Ansprechpartner nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"contacts.get","tags":["contacts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ansprechpartner per ID lesen, mit Kundenbezug","description":"Liest einen Ansprechpartner ueber seine ID, mit Name und Nummer des zugehoerigen Kunden aus dem Lese-Join. Ein weich geloeschter Ansprechpartner (`deleted_at` gesetzt) gilt hier als nicht vorhanden und ergibt 404."},"patch":{"responses":{"200":{"description":"Ansprechpartner aktualisiert — die geänderte Zeile der Ansprechpartner-Tabelle selbst, ohne den Kundenname/-nummer-Zusatz aus dem Lesepfad (das Aktualisieren liest den Kunden nicht mit).","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"customer_id":{"type":"string"},"person_number":{"type":["string","null"]},"salutation":{"type":["string","null"]},"title":{"type":["string","null"]},"first_name":{"type":["string","null"]},"last_name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"mobile":{"type":["string","null"]},"role":{"type":["string","null"]},"department":{"type":["string","null"]},"is_primary":{"type":["boolean","null"]},"opt_in":{"type":["boolean","null"]},"notes":{"type":["string","null"]},"custom_fields":{"type":"object","additionalProperties":{}},"category_id":{"type":["string","null"]},"deleted_at":{"type":"null"},"created_at":{},"updated_at":{}},"required":["id","customer_id","person_number","salutation","title","first_name","last_name","email","phone","mobile","role","department","is_primary","opt_in","notes","custom_fields","category_id"],"additionalProperties":false},"example":{"id":"string","customer_id":"string","person_number":"string","salutation":"string","title":"string","first_name":"string","last_name":"string","email":"string","phone":"string","mobile":"string","role":"string","department":"string","is_primary":true,"opt_in":true,"notes":"string","custom_fields":{},"category_id":"string","deleted_at":null}}}},"400":{"description":"Validierungsfehler (der Validator antwortet ohne eigenen Hook mit dem Zod-Fehlerobjekt und Status 400)"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb der Mindestrolle \"user\""},"404":{"description":"Ansprechpartner nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"contacts.update","tags":["contacts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aendert einzelne Felder eines Ansprechpartners. Nur was im Rumpf steht, wird geschrieben; der Kunde (`customerId`) laesst sich hier NICHT umhaengen. Die eigenen Felder werden GEMISCHT statt ersetzt — ein Teilpatch mit einem einzigen Feld loescht die uebrigen nicht. Mit `isPrimary: true` verlieren die anderen Ansprechpartner desselben Kunden diese Kennzeichnung. Ein weich geloeschter Ansprechpartner ergibt 404 statt einer stillen Nicht-Aenderung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"salutation":{"type":"string","enum":["Herr","Frau","Divers"]},"title":{"type":"string","maxLength":50},"firstName":{"type":"string","maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string","maxLength":50},"mobile":{"type":"string","maxLength":50},"role":{"type":"string","maxLength":100},"department":{"type":"string","maxLength":100},"isPrimary":{"type":"boolean","default":false},"optIn":{"type":"boolean","default":false},"notes":{"type":"string"},"customFields":{"type":"object","additionalProperties":{}}}},"example":{"salutation":"Herr","title":"string","firstName":"string","lastName":"string","email":"beispiel@example.com","phone":"string","mobile":"string","role":"string","department":"string","isPrimary":true,"optIn":true,"notes":"string","customFields":{}}}}},"summary":"Aendert einzelne Felder eines Ansprechpartners","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"204":{"description":"Ansprechpartner gelöscht — Antwort ohne Rumpf"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb der Mindestrolle \"user\""},"404":{"description":"Ansprechpartner nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"contacts.delete","tags":["contacts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Loescht einen Ansprechpartner weich: die Zeile bleibt stehen und bekommt nur `deleted_at` gesetzt, womit sie aus Liste und Einzelabruf verschwindet. Verknuepfte Belege behalten ihren Bezug. Ein bereits geloeschter oder unbekannter Ansprechpartner ergibt 404 statt eines stillen 204. Eine Ruecknahme bietet die API nicht an. War der Geloeschte der Hauptansprechpartner, hat der Kunde danach KEINEN — nachgerueckt wird nicht.","summary":"Loescht einen Ansprechpartner weich","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/orders":{"get":{"responses":{"200":{"description":"Liste der Aufträge","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"description":"Die Auftraege dieser Seite"},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":1,"description":"Angeforderte Seite, 1-basiert"},"limit":{"type":"integer","minimum":1,"description":"Angewendete Seitengroesse"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer ueber alle Seiten"},"pages":{"type":"integer","minimum":1,"description":"Anzahl Seiten bei dieser Seitengroesse"}},"required":["page","limit","total","pages"],"description":"Seitenangaben; fehlen bei ungeblaetterten Abfragen"},"meta":{"type":"object","additionalProperties":{},"description":"Angaben zur Abfrage"}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}],"pagination":{"page":1,"limit":1,"total":0,"pages":1},"meta":{}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Orders","tags":["orders"],"parameters":[{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","confirmed","processing","shipped","invoiced","cancelled","sent","accepted","in_progress","completed"]}},{"in":"query","name":"customerId","schema":{"type":"string"}},{"in":"query","name":"projectId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"sort","schema":{"type":"string"}},{"in":"query","name":"order","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}],"summary":"List orders","description":"Listet alle Aufträge des Mandanten mit Filter und Paginierung. Antworten werden kurz zwischengespeichert und bei jeder Änderung verworfen. Fällt die Datenbank aus, kommt 503 — nie eine leere Liste."},"post":{"responses":{"201":{"description":"Auftrag angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"422":{"description":"Mandanten-Regel verletzt (entity_rule_violation)"},"503":{"description":"Datenbank nicht erreichbar (ORDER_CREATE_FAILED)"}},"operationId":"orders.create","tags":["orders"],"parameters":[],"summary":"Create order","description":"Legt einen neuen Auftrag an. Die Auftragsnummer vergibt der Server aus dem Nummernkreis des Mandanten, wenn keine mitgeschickt wird. Summen und Steuer werden serverseitig neu gerechnet — mitgeschickte Beträge zählen nicht. Ohne Kopftexte, Zahlungsziel oder Belegsprache im Rumpf greifen die Vorgaben von Kunde und Mandant, und ohne projectId wird ein Projekt angelegt und verknüpft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string"},"title":{"type":"string"},"status":{"type":"string","enum":["draft","confirmed","processing","shipped","invoiced","cancelled","sent","accepted","in_progress","completed"],"default":"draft"},"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"quantity":{"type":"number","minimum":0},"unitPrice":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"discount":{"type":"number","minimum":0,"maximum":100},"lineType":{"type":"string","enum":["standard","section","note"],"default":"standard"},"optional":{"type":"boolean"},"isAlternative":{"type":"boolean"},"articleId":{"type":["string","null"],"format":"uuid"}},"required":["description","quantity","unitPrice"]},"maxItems":1000},"discount":{"type":"number","minimum":0,"maximum":100},"notes":{"type":"string"},"dueDate":{"type":"string","format":"date-time"},"paymentTermsDays":{"type":"integer","minimum":0,"maximum":365},"skontoDays":{"type":["integer","null"],"minimum":0,"maximum":365},"skontoPercent":{"type":["number","null"],"minimum":0,"maximum":100},"customFields":{"type":"object","additionalProperties":{}},"projectId":{"type":["string","null"],"format":"uuid"},"customerName":{"type":"string"},"introText":{"type":"string"},"footerText":{"type":"string"},"priceMode":{"type":"string","enum":["net","gross"],"default":"net"},"salutation":{"type":"string"},"recipientName":{"type":"string"},"recipientStreet":{"type":"string"},"recipientZip":{"type":"string"},"recipientCity":{"type":"string"},"recipientCountry":{"type":"string"},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"assignedToUserId":{"type":["string","null"],"minLength":1},"leistungszeitraumVon":{"type":["string","null"],"format":"date"},"leistungszeitraumBis":{"type":["string","null"],"format":"date"},"lieferdatum":{"type":["string","null"],"format":"date"},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"],"format":"date"},"language":{"type":"string","enum":["de","en","fr","es","pl","nl","da","cs","zh"]}},"required":["customerId","positions"]},"example":{"customerId":"string","title":"string","status":"draft","positions":[{"title":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string","taxRate":0,"discount":0,"lineType":"standard","optional":true,"isAlternative":true,"articleId":"00000000-0000-4000-8000-000000000000"}],"discount":0,"notes":"string","dueDate":"2026-01-01T12:00:00.000Z","paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"customFields":{},"projectId":"00000000-0000-4000-8000-000000000000","customerName":"string","introText":"string","footerText":"string","priceMode":"net","salutation":"string","recipientName":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","assignedToUserId":"string","leistungszeitraumVon":"2026-01-01","leistungszeitraumBis":"2026-01-01","lieferdatum":"2026-01-01","leistungsTyp":"leistungsdatum","leistungsdatum":"2026-01-01","language":"de"}}}}}},"/api/v1/orders/stats":{"get":{"responses":{"200":{"description":"Auftrags-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{},"description":"Die Kennzahlen"},"meta":{"type":"object","additionalProperties":{},"description":"Angaben zur Abfrage"}},"required":["data","meta"]},"example":{"data":{},"meta":{}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1OrdersStats","tags":["orders"],"parameters":[],"summary":"Get order statistics","description":"KPIs, Tab-Zähler und Pipeline-Funnel für Aufträge (server-seitig aggregiert). Zählt über alle Aufträge des Mandanten ohne Seitenbegrenzung; gelöschte bleiben außen vor. Die Zähler überschneiden sich absichtlich — ein Auftrag steckt in mehreren Gruppen."}},"/api/v1/orders/{id}":{"get":{"responses":{"200":{"description":"Auftrags-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"},"customerName":{"type":["string","null"],"description":"Name des Kunden aus der Anreicherung"},"customerEmail":{"type":["string","null"],"description":"E-Mail des Kunden aus der Anreicherung"}},"required":["id","customerName","customerEmail"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null,"customerName":"string","customerEmail":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Auftrag nicht gefunden"}},"operationId":"getApiV1OrdersById","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get order","description":"Liefert einen einzelnen Auftrag anhand seiner ID, angereichert um Name und E-Mail des Kunden. Der Datensatz liegt direkt im Rumpf, nicht unter data. Eine ID, die keine UUID ist, ergibt 404."},"put":{"responses":{"200":{"description":"Auftrag aktualisiert — die volle Auftragsform wie bei GET /orders/{id}","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Nicht gespeichert — Datenbankfehler ODER unbekannte Auftrags-Id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"ORDER_UPDATE_FAILED","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"]}}}}},"operationId":"putApiV1OrdersById","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace order","description":"Ersetzt einen Auftrag. Die Positionsliste wird vollständig überschrieben und die Summen neu gerechnet. Für Kopftexte, Anrede, abweichende Empfänger- und Lieferadresse, Leistungszeitraum, Zahlungsziel und Skonto gilt dagegen: nicht mitgeschickt heißt unverändert — mit diesen Feldern lässt sich hier nichts leeren. Für ein Teil-Update: PATCH /orders/{id}. ACHTUNG: eine unbekannte Id ergibt hier KEIN 404 — der Repository-Fehler landet im allgemeinen catch und kommt als 503 ORDER_UPDATE_FAILED zurück, ununterscheidbar von einem echten Datenbankausfall.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string"},"title":{"type":"string"},"status":{"type":"string","enum":["draft","confirmed","processing","shipped","invoiced","cancelled","sent","accepted","in_progress","completed"],"default":"draft"},"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"quantity":{"type":"number","minimum":0},"unitPrice":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"discount":{"type":"number","minimum":0,"maximum":100},"lineType":{"type":"string","enum":["standard","section","note"],"default":"standard"},"optional":{"type":"boolean"},"isAlternative":{"type":"boolean"},"articleId":{"type":["string","null"],"format":"uuid"}},"required":["description","quantity","unitPrice"]},"maxItems":1000},"discount":{"type":"number","minimum":0,"maximum":100},"notes":{"type":"string"},"dueDate":{"type":"string","format":"date-time"},"paymentTermsDays":{"type":"integer","minimum":0,"maximum":365},"skontoDays":{"type":["integer","null"],"minimum":0,"maximum":365},"skontoPercent":{"type":["number","null"],"minimum":0,"maximum":100},"customFields":{"type":"object","additionalProperties":{}},"projectId":{"type":["string","null"],"format":"uuid"},"customerName":{"type":"string"},"introText":{"type":"string"},"footerText":{"type":"string"},"priceMode":{"type":"string","enum":["net","gross"],"default":"net"},"salutation":{"type":"string"},"recipientName":{"type":"string"},"recipientStreet":{"type":"string"},"recipientZip":{"type":"string"},"recipientCity":{"type":"string"},"recipientCountry":{"type":"string"},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"assignedToUserId":{"type":["string","null"],"minLength":1},"leistungszeitraumVon":{"type":["string","null"],"format":"date"},"leistungszeitraumBis":{"type":["string","null"],"format":"date"},"lieferdatum":{"type":["string","null"],"format":"date"},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"],"format":"date"},"language":{"type":"string","enum":["de","en","fr","es","pl","nl","da","cs","zh"]}},"required":["customerId","positions"]},"example":{"customerId":"string","title":"string","status":"draft","positions":[{"title":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string","taxRate":0,"discount":0,"lineType":"standard","optional":true,"isAlternative":true,"articleId":"00000000-0000-4000-8000-000000000000"}],"discount":0,"notes":"string","dueDate":"2026-01-01T12:00:00.000Z","paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"customFields":{},"projectId":"00000000-0000-4000-8000-000000000000","customerName":"string","introText":"string","footerText":"string","priceMode":"net","salutation":"string","recipientName":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","assignedToUserId":"string","leistungszeitraumVon":"2026-01-01","leistungszeitraumBis":"2026-01-01","lieferdatum":"2026-01-01","leistungsTyp":"leistungsdatum","leistungsdatum":"2026-01-01","language":"de"}}}}},"delete":{"responses":{"200":{"description":"Auftrag storniert (soft-delete)","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Ergebnis im Klartext"},"order":{"type":"object","additionalProperties":{},"description":"Der betroffene Auftrag"}},"required":["message","order"]},"example":{"message":"string","order":{}}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden"},"409":{"description":"Auftrag kann nicht storniert werden (z. B. bereits versandt)"}},"operationId":"deleteApiV1OrdersById","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Cancel order (soft delete)","description":"Storniert einen Auftrag (Soft-Delete: status=cancelled, GoBD-konform — kein Hard-Delete). Die Zeile bleibt vollständig erhalten und trägt danach einen Löschzeitpunkt; die Listen blenden sie aus. Umkehrbar über POST /orders/{id}/restore. Aus den Zuständen shipped und invoiced verweigert der Aufruf mit 409."},"patch":{"responses":{"200":{"description":"Auftrag aktualisiert. EINE Form, unabhängig davon, welche Felder im Body standen: der frisch geladene, serialisierte Auftrag (camelCase) — derselbe Schlüsselsatz wie Detailabruf und Liste. Bis 11.08.2026 kam bei status/due_date/notes/title/customerId stattdessen die rohe Datenbankzeile (snake_case) zurück.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}}}},"400":{"description":"Validierungsfehler (auch: leerer Patch ohne änderbare Felder)"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden (order_not_found)"},"422":{"description":"Mandanten-Regel verletzt (entity_rule_violation)"},"503":{"description":"Datenbank nicht erreichbar (ORDER_PATCH_FAILED)"}},"operationId":"orders.update","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update order fields","description":"Partielles Update eines Auftrags: status, due_date, notes, title, customerId, projectId (null löst die Projekt-Verknüpfung), discount, customFields. Nicht mitgeschickte Felder bleiben unverändert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","confirmed","processing","shipped","invoiced","cancelled","sent","accepted","in_progress","completed"]},"due_date":{"type":"string","format":"date"},"notes":{"type":"string"},"title":{"type":"string","maxLength":255},"customerId":{"type":"string","format":"uuid"},"projectId":{"type":["string","null"],"format":"uuid"},"discount":{"type":"number","minimum":0,"maximum":100},"customFields":{"type":"object","additionalProperties":{}}}},"example":{"status":"draft","due_date":"2026-01-01","notes":"string","title":"string","customerId":"00000000-0000-4000-8000-000000000000","projectId":"00000000-0000-4000-8000-000000000000","discount":0,"customFields":{}}}}}}},"/api/v1/orders/{id}/status":{"patch":{"responses":{"200":{"description":"Status aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden (order_not_found)"},"409":{"description":"Unzulässiger Statusübergang (invalid_status_transition) — Antwort nennt from/to und eine deutsche Klartext-Meldung mit den erlaubten Folgezuständen"},"503":{"description":"Datenbank nicht erreichbar (ORDER_STATUS_UPDATE_FAILED)"}},"operationId":"orders.updateStatus","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Set order status","description":"Setzt den Status eines Auftrags (State-Machine-Prüfung). Erlaubte Übergänge: draft→confirmed/sent/cancelled; confirmed→processing/shipped/invoiced/cancelled; processing→shipped/invoiced/cancelled; shipped→invoiced/completed (kein Storno mehr ab shipped); invoiced→completed; sent→accepted/confirmed/in_progress/processing/cancelled; accepted→in_progress/processing/shipped/confirmed/completed/cancelled; in_progress→completed/shipped/invoiced/processing/cancelled; completed→invoiced. from===to ist immer erlaubt (No-op).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","confirmed","processing","shipped","invoiced","cancelled","sent","accepted","in_progress","completed"]}},"required":["status"]},"example":{"status":"draft"}}}}}},"/api/v1/orders/{id}/restore":{"post":{"responses":{"200":{"description":"Auftrag wiederhergestellt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Aktion ausgefuehrt wurde"},"id":{"type":"string","description":"Id des betroffenen Auftrags"}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden oder nicht gelöscht"}},"operationId":"postApiV1OrdersByIdRestore","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Restore soft-deleted order","description":"Stellt einen zuvor stornierten/gelöschten (soft-delete) Auftrag wieder her. Der Auftrag kommt IMMER als Entwurf zurück — der Zustand vor dem Löschen wird nicht wiederhergestellt. Ein Auftrag ohne Löschzeitpunkt ergibt 404. Die Antwort ist eine kurze Quittung mit der Id, nicht der Auftrag."}},"/api/v1/orders/{id}/confirm":{"post":{"responses":{"200":{"description":"Auftrag bestätigt — der Auftrag selbst, NICHT ein {ok, order}-Umschlag. Ob die Reservierung geklappt hat, sagt die Antwort nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"order_not_found","description":"Fester Fehlerschluessel"}},"required":["error"]}}}},"409":{"description":"Ungültiger Statusübergang — die Antwort nennt Ist- und Zielstatus","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_transition","description":"Fester Fehlerschluessel"},"from":{"type":"string","description":"Aktueller Status des Auftrags"},"to":{"type":"string","description":"Angefragter Zielstatus"}},"required":["error","from","to"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1OrdersByIdConfirm","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Confirm order","description":"Bestätigt einen Auftrag (draft → confirmed) und reserviert Bestand. Die Reservierung ist nachrangig: scheitert sie, wird das nur protokolliert und der Auftrag gilt trotzdem als bestätigt (200). Die Antwort ist der Auftrag, nicht die Reservierung."}},"/api/v1/orders/{id}/processing":{"post":{"responses":{"200":{"description":"Status aktualisiert — der Auftrag selbst, kein {ok, order}-Umschlag","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"order_not_found","description":"Fester Fehlerschluessel"}},"required":["error"]}}}},"409":{"description":"Ungültiger Statusübergang — die Antwort nennt Ist- und Zielstatus","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_transition","description":"Fester Fehlerschluessel"},"from":{"type":"string","description":"Aktueller Status des Auftrags"},"to":{"type":"string","description":"Angefragter Zielstatus"}},"required":["error","from","to"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1OrdersByIdProcessing","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mark order in progress","description":"Setzt Auftrag auf \"In Bearbeitung\" (confirmed → processing). Reine Statusänderung, es wird kein Bestand bewegt. Die Antwort ist der Auftrag."}},"/api/v1/orders/{id}/ship":{"post":{"responses":{"200":{"description":"Status aktualisiert — der Auftrag selbst, kein {ok, order}-Umschlag","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"order_not_found","description":"Fester Fehlerschluessel"}},"required":["error"]}}}},"409":{"description":"Ungültiger Statusübergang — die Antwort nennt Ist- und Zielstatus","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_transition","description":"Fester Fehlerschluessel"},"from":{"type":"string","description":"Aktueller Status des Auftrags"},"to":{"type":"string","description":"Angefragter Zielstatus"}},"required":["error","from","to"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1OrdersByIdShip","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mark order shipped","description":"Markiert Auftrag als versandt (processing → shipped). Reine Statusänderung: es entsteht kein Lieferschein und es wird kein Bestand gebucht. Ab hier ist kein Storno mehr möglich."}},"/api/v1/orders/{id}/invoice":{"post":{"responses":{"200":{"description":"Status aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string","description":"Id der erzeugten Rechnung"},"message":{"type":"string","description":"Ergebnis im Klartext"}},"required":["invoiceId","message"]},"example":{"invoiceId":"string","message":"string"}}}},"201":{"description":"Status aktualisiert (legacy compat)"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden"},"409":{"description":"Ungültiger Statusübergang"}},"operationId":"postApiV1OrdersByIdInvoice","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mark order invoiced","description":"Setzt Auftrag auf \"Abgerechnet\" (shipped → invoiced). Für Belegerstellung: /convert-to-invoice. ACHTUNG, der Aufruf tut ZWEIERLEI, je nach aktuellem Status: aus draft/confirmed/processing/shipped/invoiced/cancelled ändert er nur den Status und antwortet 200 mit dem Auftrag. Aus jedem anderen Status (Altbestand, etwa sent, accepted, in_progress, completed) LEGT er eine Rechnung an und antwortet 201 mit {message, invoiceId} — ohne Doppelfaktura-Prüfung und mit fest 14 Tagen Zahlungsziel."}},"/api/v1/orders/{id}/cancel":{"post":{"responses":{"200":{"description":"Auftrag storniert — der Auftrag selbst, kein {ok, order}-Umschlag. Ob der reservierte Bestand wirklich freigegeben wurde, sagt die Antwort nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"order_not_found","description":"Fester Fehlerschluessel"}},"required":["error"]}}}},"409":{"description":"Stornierung aus aktuellem Status nicht möglich — die Antwort nennt Ist- und Zielstatus","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_transition","description":"Fester Fehlerschluessel"},"from":{"type":"string","description":"Aktueller Status des Auftrags"},"to":{"type":"string","description":"Angefragter Zielstatus"}},"required":["error","from","to"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1OrdersByIdCancel","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Cancel order","description":"Storniert einen Auftrag (erlaubt aus draft, confirmed, processing). Setzt nur den Status; anders als DELETE wird kein Löschzeitpunkt gesetzt, der Auftrag bleibt in den Listen. Reservierter Bestand wird freigegeben, sofern zuvor reserviert war — scheitert das, wird es nur protokolliert und der Storno gilt trotzdem."}},"/api/v1/orders/{id}/convert-to-invoice":{"post":{"responses":{"200":{"description":"Rechnung erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"invoice_id":{"type":"string","description":"Id der erzeugten Rechnung"},"invoice_number":{"type":"string","description":"Rechnungsnummer"}},"required":["invoice_id","invoice_number"]},"example":{"invoice_id":"string","invoice_number":"string"}}}},"400":{"description":"Auftrag muss in einem rechnungsfähigen Status sein"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden"},"409":{"description":"Bereits als Rechnung konvertiert"}},"operationId":"postApiV1OrdersByIdConvert-to-invoice","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert order to invoice","description":"Wandelt einen Auftrag in eine Rechnung um (erstellt ein Rechnungs-Dokument im Entwurf). Antwortet 200 mit {invoice_id, invoice_number}, nicht 201. Der Auftrag bleibt bestehen und wechselt aus shipped nach invoiced. Gegen Doppelfaktura gesperrt: existiert bereits eine Rechnung zum Auftrag oder ist die Menge voll fakturiert, kommt 409 (already_invoiced); liegen Abschlagsrechnungen vor, ebenfalls 409 (abschlag_requires_schlussrechnung) — dann ist /schlussrechnung der richtige Weg. Zeigt der Auftrag auf einen gelöschten Kunden, entsteht die Rechnung ohne Kundenverknüpfung statt zu scheitern."}},"/api/v1/orders/{id}/convert/invoice":{"post":{"responses":{"200":{"description":"Rechnung erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"invoice_id":{"type":"string","description":"Id der erzeugten Rechnung"},"invoice_number":{"type":"string","description":"Rechnungsnummer"}},"required":["invoice_id","invoice_number"]},"example":{"invoice_id":"string","invoice_number":"string"}}}},"400":{"description":"Auftrag muss in einem rechnungsfähigen Status sein"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden"},"409":{"description":"Bereits als Rechnung konvertiert"}},"operationId":"postApiV1OrdersByIdConvertInvoice","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert order to invoice","description":"Wandelt einen Auftrag in eine Rechnung um (erstellt ein Rechnungs-Dokument im Entwurf). Antwortet 200 mit {invoice_id, invoice_number}, nicht 201. Der Auftrag bleibt bestehen und wechselt aus shipped nach invoiced. Gegen Doppelfaktura gesperrt: existiert bereits eine Rechnung zum Auftrag oder ist die Menge voll fakturiert, kommt 409 (already_invoiced); liegen Abschlagsrechnungen vor, ebenfalls 409 (abschlag_requires_schlussrechnung) — dann ist /schlussrechnung der richtige Weg. Zeigt der Auftrag auf einen gelöschten Kunden, entsteht die Rechnung ohne Kundenverknüpfung statt zu scheitern."}},"/api/v1/orders/{id}/convert-to-delivery":{"post":{"responses":{"200":{"description":"Lieferschein erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"delivery_id":{"type":"string","description":"Id des erzeugten Lieferscheins"},"delivery_number":{"type":"string","description":"Lieferscheinnummer"},"projectId":{"type":["string","null"],"description":"Projekt, falls zugeordnet"}},"required":["delivery_id","delivery_number","projectId"]},"example":{"delivery_id":"string","delivery_number":"string","projectId":"string"}}}},"400":{"description":"Auftrag muss bestätigt oder in Bearbeitung sein"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden"},"422":{"description":"Teilmenge überschreitet offene Restmenge"}},"operationId":"postApiV1OrdersByIdConvert-to-delivery","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert order to delivery note","description":"Erstellt einen Lieferschein aus dem Auftrag (mit optionaler Teilmenge je Position). Antwortet 200 mit {delivery_id, delivery_number, projectId}, nicht 201. Ohne Rumpf wird die gesamte noch offene Restmenge geliefert; mehrere Teillieferungen sind möglich, bis alles geliefert ist. Ist nichts mehr offen: 422 (nothing_to_deliver). Hat der Auftrag keinen gültigen Kunden: 409 (no_customer). Der Lieferschein entsteht im Entwurf, es wird kein Bestand gebucht."}},"/api/v1/orders/{id}/convert/delivery":{"post":{"responses":{"200":{"description":"Lieferschein erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"delivery_id":{"type":"string","description":"Id des erzeugten Lieferscheins"},"delivery_number":{"type":"string","description":"Lieferscheinnummer"},"projectId":{"type":["string","null"],"description":"Projekt, falls zugeordnet"}},"required":["delivery_id","delivery_number","projectId"]},"example":{"delivery_id":"string","delivery_number":"string","projectId":"string"}}}},"400":{"description":"Auftrag muss bestätigt oder in Bearbeitung sein"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden"},"422":{"description":"Teilmenge überschreitet offene Restmenge"}},"operationId":"postApiV1OrdersByIdConvertDelivery","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert order to delivery note","description":"Erstellt einen Lieferschein aus dem Auftrag (mit optionaler Teilmenge je Position). Antwortet 200 mit {delivery_id, delivery_number, projectId}, nicht 201. Ohne Rumpf wird die gesamte noch offene Restmenge geliefert; mehrere Teillieferungen sind möglich, bis alles geliefert ist. Ist nichts mehr offen: 422 (nothing_to_deliver). Hat der Auftrag keinen gültigen Kunden: 409 (no_customer). Der Lieferschein entsteht im Entwurf, es wird kein Bestand gebucht."}},"/api/v1/orders/{id}/abschlagsrechnung":{"post":{"responses":{"200":{"description":"Abschlagsrechnung erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"invoice_id":{"type":"string"},"invoice_number":{"type":"string"},"invoice_type":{"type":"string","description":"Belegart der erzeugten Rechnung"},"net":{"type":"number","description":"Nettobetrag"},"tax":{"type":"number","description":"Steuerbetrag"},"total":{"type":"number","description":"Bruttobetrag"}},"required":["invoice_id","invoice_number","invoice_type","net","tax","total"]},"example":{"invoice_id":"string","invoice_number":"string","invoice_type":"string","net":0,"tax":0,"total":0}}}},"400":{"description":"pct oder betrag erforderlich"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Auftrag nicht gefunden"}},"operationId":"postApiV1OrdersByIdAbschlagsrechnung","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create advance payment invoice","description":"Erstellt eine Abschlagsrechnung (Teilbetrag oder Prozent vom Auftragswert). Antwortet 200 mit {invoice_id, invoice_number, invoice_type, net, tax, total}, nicht 201. Drei Modi: pct, betrag oder eine Auswahl von Auftragspositionen (dann werden diese Zeilen abgerechnet, sonst entsteht eine Pauschalzeile). Der Betrag wird auf den noch offenen Rest gedeckelt, statt abgelehnt zu werden — die Antwort kann also einen kleineren Wert nennen als angefragt. Ist nichts mehr offen: 400 (nothing_to_invoice). Die Rechnung entsteht im Entwurf; der Auftragsstatus bleibt unverändert. Zum Abschluss danach /schlussrechnung, NICHT /convert-to-invoice.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"pct":{"type":"number","minimum":0,"maximum":100},"betrag":{"type":"number","minimum":0},"positions":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","minimum":0},"quantity":{"type":"number","exclusiveMinimum":0}},"required":["index"]}},"notes":{"type":"string"}}},"example":{"pct":0,"betrag":0,"positions":[{"index":0,"quantity":1}],"notes":"string"}}}}}},"/api/v1/orders/{id}/schlussrechnung":{"post":{"responses":{"200":{"description":"Schlussrechnung erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"invoice_id":{"type":"string"},"invoice_number":{"type":"string"},"invoice_type":{"type":"string","description":"Belegart der erzeugten Rechnung"},"net":{"type":"number","description":"Nettobetrag"},"tax":{"type":"number","description":"Steuerbetrag"},"total":{"type":"number","description":"Bruttobetrag"},"priorAbschlagNet":{"type":"number","description":"Summe der bereits gestellten Abschlaege, netto"}},"required":["invoice_id","invoice_number","invoice_type","net","tax","total","priorAbschlagNet"]},"example":{"invoice_id":"string","invoice_number":"string","invoice_type":"string","net":0,"tax":0,"total":0,"priorAbschlagNet":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Auftrag nicht gefunden"}},"operationId":"postApiV1OrdersByIdSchlussrechnung","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create final invoice","description":"Erstellt eine Schlussrechnung (Gesamtbetrag abzüglich bisheriger Abschläge). Antwortet 200 mit {invoice_id, invoice_number, invoice_type, net, tax, total, priorAbschlagNet}, nicht 201. Die Rechnung enthält alle Auftragspositionen plus eine Abzugszeile über die Summe der bisherigen Abschläge. Nur EINMAL je Auftrag möglich: existiert bereits eine, kommt 409 (already_finalized) mit der Nummer der vorhandenen. Die Rechnung entsteht im Entwurf; der Auftragsstatus bleibt unverändert."}},"/api/v1/orders/{id}/belegkette":{"get":{"responses":{"200":{"description":"Belegkette erfolgreich geladen","content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"object","additionalProperties":{},"description":"Der Auftrag selbst"},"quotes":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Zugehoerige Angebote"},"deliveries":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Lieferscheine"},"invoices":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Rechnungen"},"creditNotes":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Gutschriften"}},"required":["order","quotes","deliveries","invoices","creditNotes"]},"example":{"order":{},"quotes":[{}],"deliveries":[{}],"invoices":[{}],"creditNotes":[{}]}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Auftrag nicht gefunden"}},"operationId":"getApiV1OrdersByIdBelegkette","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get order document chain","description":"Liefert die Belegkette eines Auftrags: Angebot, Lieferscheine, Rechnungen, Gutschriften. Nur der Auftrag selbst ist garantiert — jede der vier Nachschlagen ist einzeln fehlertolerant: fehlt die Tabelle oder scheitert die Abfrage, kommt der Abschnitt als LEERE Liste unter 200 zurück, nicht als Fehler. Eine leere Liste heißt also nicht zwingend, dass es keinen solchen Beleg gibt."}},"/api/v1/orders/{id}/duplicate":{"post":{"responses":{"200":{"description":"Auftrag dupliziert — der NEUE Auftrag im Status draft, mit eigener Id und eigener Nummer. Die nicht übernommenen Felder stehen darin auf null.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Auftrags (UUID)"},"number":{"type":["string","null"],"description":"Auftragsnummer aus dem Nummernkreis des Mandanten"},"customerId":{"type":["string","null"],"description":"Kennung des Kunden"},"projectId":{"type":["string","null"],"description":"Kennung des zugeordneten Projekts"},"title":{"type":["string","null"],"description":"Betreff des Auftrags"},"status":{"type":["string","null"],"description":"Status; zwei Vokabulare laufen parallel (draft…invoiced und sent…completed)"},"leistungsTyp":{"type":["string","null"],"description":"Art der Leistungsangabe: leistungsdatum, leistungszeitraum oder keine"},"positions":{"type":["array","null"],"items":{},"description":"Die Belegzeilen, inklusive Struktur-, Alternativ- und Optionalzeilen"},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Nettosumme; Postgres liefert NUMERIC als Zeichenkette"},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Steuerbetrag; Postgres liefert NUMERIC als Zeichenkette"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Bruttosumme; Postgres liefert NUMERIC als Zeichenkette"},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Beleg-Rabatt in Prozent, bereits in den Summen verrechnet"},"notes":{"type":["string","null"],"description":"Interne Notiz"},"customFields":{"type":["object","null"],"additionalProperties":{},"description":"Mandantenspezifische Zusatzfelder"},"createdAt":{"type":"null","description":"Anlagezeitpunkt (ISO) oder null"},"updatedAt":{"type":"null","description":"Letzte Aenderung (ISO) oder null"},"introText":{"type":"null","description":"Kopftext ueber den Positionen"},"footerText":{"type":"null","description":"Schlusstext unter den Positionen"},"salutation":{"type":"null","description":"Anrede auf dem Beleg"},"recipientName":{"type":"null","description":"Belegabweichender Empfaengername"},"recipientStreet":{"type":"null","description":"Belegabweichende Strasse"},"recipientZip":{"type":"null","description":"Belegabweichende Postleitzahl"},"recipientCity":{"type":"null","description":"Belegabweichender Ort"},"recipientCountry":{"type":"null","description":"Belegabweichendes Land"},"deliveryName":{"type":"null","description":"Abweichende Lieferadresse: Name"},"deliveryCompany":{"type":"null","description":"Abweichende Lieferadresse: Firma"},"deliveryStreet":{"type":"null","description":"Abweichende Lieferadresse: Strasse"},"deliveryZip":{"type":"null","description":"Abweichende Lieferadresse: Postleitzahl"},"deliveryCity":{"type":"null","description":"Abweichende Lieferadresse: Ort"},"deliveryCountry":{"type":"null","description":"Abweichende Lieferadresse: Land"},"assignedToUserId":{"type":"null","description":"Zugewiesener Mitarbeiter, technische Kennung"},"assignedToName":{"type":"null","description":"Zugewiesener Mitarbeiter, Anzeigename"},"leistungszeitraumVon":{"type":"null","description":"Leistungszeitraum von (ISO-Datum)"},"leistungszeitraumBis":{"type":"null","description":"Leistungszeitraum bis (ISO-Datum)"},"lieferdatum":{"type":"null","description":"Lieferdatum (ISO-Datum)"},"leistungsdatum":{"type":"null","description":"Einzelnes Leistungsdatum (ISO-Datum)"},"language":{"type":"null","description":"Belegsprache als Zweibuchstaben-Code; null = aus dem Kunden ableiten"},"sourceDocumentId":{"type":"null","description":"Herkunftsbeleg der Belegkette"},"sourceDocumentType":{"type":"null","description":"Belegart der Herkunft, etwa \"quote\""},"dueDate":{"type":"null","description":"Faelligkeit (ISO) oder null"},"paymentTermsDays":{"type":"null","description":"Zahlungsziel in Tagen"},"skontoDays":{"type":"null","description":"Skontofrist in Tagen"},"skontoPercent":{"type":"null","description":"Skontosatz in Prozent"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","projectId":"string","title":"string","status":"string","leistungsTyp":"string","positions":[],"subtotal":"string","tax":"string","total":"string","discount":"string","notes":"string","customFields":{},"createdAt":null,"updatedAt":null,"introText":null,"footerText":null,"salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"language":null,"sourceDocumentId":null,"sourceDocumentType":null,"dueDate":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null}}}},"400":{"description":"Die Vorlage hat keinen Kunden — die Kopie braucht einen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"source_order_missing_customer","description":"Fester Fehlerschluessel"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"order_not_found","description":"Fester Fehlerschluessel"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1OrdersByIdDuplicate","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Duplicate order","description":"Dupliziert einen Auftrag als neuen Entwurf mit eigener Auftragsnummer. Antwortet 200 mit dem neuen Auftrag, nicht 201. Es wird bewusst nur ein Teil übernommen: Kunde, Titel (mit Zusatz \"(Kopie)\"), Positionen, Kopf-Rabatt, Notizen und eigene Felder. NICHT übernommen werden Kopftexte, Anrede, abweichende Adressen, Termine, Zahlungsziel, Skonto, Belegsprache und Projekt. Auch je Position bleibt nur Beschreibung, Menge, Einheit, Einzelpreis und Steuersatz erhalten — Positions-Titel, Artikelbezug, Positions-Rabatt sowie die Kennzeichen optional/Alternative gehen verloren. Ein Auftrag ohne Kunden lässt sich nicht duplizieren (400)."}},"/api/v1/orders/{id}/pdf":{"get":{"responses":{"200":{"description":"Die PDF-Datei als Datenstrom, KEIN JSON. Der Kopf `X-PDF-Engine` sagt, woher sie kommt: `cache` aus dem Zwischenspeicher, `fallback` aus dem Notbetrieb — letzteres ohne ETag und ohne Zwischenspeicherung.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"304":{"description":"Not modified — ETag matches"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Auftrag nicht gefunden"},"500":{"description":"`pdf_generation_failed` — auch der Notbetrieb hat kein PDF geliefert"}},"operationId":"getApiV1OrdersByIdPdf","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Download order confirmation PDF","description":"Liefert die Auftragsbestaetigung als PDF (gecacht, ETag). Antwortet mit dem Datenstrom, nicht mit JSON. Die Zielsprache bestimmt ?lang, sonst die Belegsprache, sonst die Sprache des Kunden. Läuft der Renderer im Notbetrieb, kommt trotzdem 200 mit einem vereinfachten PDF — dieses wird bewusst weder zwischengespeichert noch mit ETag ausgeliefert; erkennbar am Kopf X-PDF-Engine (cache | fallback). Schlägt die Erzeugung ganz fehl: 500 (pdf_generation_failed)."}},"/api/v1/orders/{id}/send":{"post":{"responses":{"200":{"description":"Auftrag versendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"messageId":{"type":["string","null"],"description":"Id beim Mailversender"},"recipients":{"type":"array","items":{"type":"string"},"description":"Tatsaechliche Empfaenger"},"sentAt":{"type":"string","description":"Zeitpunkt des Versands (ISO)"},"simuliert":{"type":"boolean","description":"true = nur simuliert, es ging keine Mail raus"}},"required":["ok","messageId","recipients","sentAt","simuliert"]},"example":{"ok":true,"messageId":"string","recipients":["string"],"sentAt":"string","simuliert":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden"},"502":{"description":"E-Mail-Versand fehlgeschlagen"}},"operationId":"postApiV1OrdersByIdSend","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Send order confirmation by e-mail","description":"Versendet eine Auftragsbestaetigung per E-Mail (mit PDF-Anhang). Auf den Entwicklungsumgebungen ist der Versand SIMULIERT: der Aufruf meldet Erfolg, es geht aber nichts hinaus — erkennbar am Feld simuliert in der Antwort. Ein Auftrag im Entwurf wechselt dabei auf \"versendet\". Ist das E-Mail-Kontingent des Tarifs erschöpft: 402. Läuft der PDF-Renderer im Notbetrieb, wird der Versand abgebrochen (503) statt ein vereinfachtes PDF an Kunden zu schicken. Lehnt der Mailversand ab: 502, und das Kontingent wird zurückgebucht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"cc":{"type":"array","items":{"type":"string","format":"email"}},"bcc":{"type":"array","items":{"type":"string","format":"email"}},"subject":{"type":"string","minLength":1,"maxLength":255},"message":{"type":"string","maxLength":4000},"attachPdf":{"type":"boolean"},"includePortalLink":{"type":"boolean","default":true},"lang":{"type":"string","enum":["de","en","fr","es","nl","da","pl","cs","zh"]},"template":{"type":"string","enum":["doc-quote","doc-order","doc-delivery","doc-invoice","dunning-level1","dunning-level2","dunning-level3"]},"extraAttachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"contentBase64":{"type":"string","minLength":1},"contentType":{"type":"string","maxLength":100}},"required":["filename","contentBase64"]},"maxItems":10},"attachmentMode":{"type":"string","enum":["separate","merge"]}},"required":["to"]},"example":{"to":["beispiel@example.com"],"cc":["beispiel@example.com"],"bcc":["beispiel@example.com"],"subject":"string","message":"string","attachPdf":true,"includePortalLink":true,"lang":"de","template":"doc-quote","extraAttachments":[{"filename":"string","contentBase64":"string","contentType":"string"}],"attachmentMode":"separate"}}}}}},"/api/v1/orders/{id}/positions":{"patch":{"responses":{"200":{"description":"Positionen aktualisiert — ein Auszug, nicht der ganze Auftrag. `positions` sind die GESPEICHERTEN Zeilen mit aufgelöstem Steuersatz, nicht die geschickten.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Auftrags"},"number":{"type":["string","null"],"description":"Auftragsnummer"},"status":{"type":["string","null"],"description":"Status nach der Aenderung — unveraendert"},"positions":{"type":"array","items":{},"description":"Die gespeicherten Zeilen mit aufgeloestem Steuersatz"},"subtotal":{"type":"number","description":"Neu gerechnete Nettosumme"},"tax":{"type":"number","description":"Neu gerechneter Steuerbetrag"},"total":{"type":"number","description":"Neu gerechnete Bruttosumme"},"taxNote":{"type":["string","null"],"description":"Steuerhinweis des Belegs, etwa §13b oder §19"},"priceMode":{"type":"string","enum":["net","gross"],"description":"Netto- oder Bruttopreise; unveraendert uebernommen"},"discount":{"type":"number","description":"Beleg-Rabatt in Prozent; unveraendert uebernommen"},"updatedAt":{"description":"Zeitpunkt der Aenderung, wie die Datenbank ihn liefert"},"position_count":{"type":"integer","minimum":0,"description":"Anzahl gespeicherter Zeilen"}},"required":["id","number","status","positions","subtotal","tax","total","taxNote","priceMode","discount","position_count"]},"example":{"id":"string","number":"string","status":"string","positions":[],"subtotal":0,"tax":0,"total":0,"taxNote":"string","priceMode":"net","discount":0,"position_count":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"order_not_found","description":"Fester Fehlerschluessel"}},"required":["error"]}}}},"409":{"description":"Auftrag gesperrt (abgerechnet/storniert)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"order_locked","description":"Fester Fehlerschluessel"},"status":{"type":"string","description":"Der sperrende Status, invoiced oder cancelled"},"message":{"type":"string","minLength":1,"description":"Begruendung fuer die Oberflaeche, deutsch"}},"required":["error","status","message"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"patchApiV1OrdersByIdPositions","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace order positions","description":"Ersetzt die Positionsliste eines Auftrags vollständig und rechnet die Summen neu — nicht mitgeschickte Positionen sind danach weg. Kopf-Rabatt, Netto/Brutto-Modus und Kunde bleiben unverändert und gehen in die neue Summe ein. Die Antwort ist ein kurzer Auszug (Id, Nummer, Status, Positionen, Summen), nicht der ganze Auftrag. Ein abgerechneter oder stornierter Auftrag ist gesperrt: 409 (order_locked). Das zwischengespeicherte PDF wird verworfen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"quantity":{"type":"number","minimum":0},"unitPrice":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"discount":{"type":"number","minimum":0,"maximum":100},"lineType":{"type":"string","enum":["standard","section","note"],"default":"standard"},"optional":{"type":"boolean"},"isAlternative":{"type":"boolean"},"articleId":{"type":["string","null"],"format":"uuid"}},"required":["description","quantity","unitPrice"]},"maxItems":1000}},"required":["positions"]},"example":{"positions":[{"title":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string","taxRate":0,"discount":0,"lineType":"standard","optional":true,"isAlternative":true,"articleId":"00000000-0000-4000-8000-000000000000"}]}}}}}},"/api/v1/orders/{orderId}/milestones":{"get":{"responses":{"200":{"description":"Meilensteine des Auftrags; leer heisst auch „Auftrag unbekannt\".","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"orderId":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":"string","description":"Leerer Text statt null, wenn nichts hinterlegt ist."},"dueDate":{"type":["string","null"],"description":"YYYY-MM-DD, aus einer DATE-Spalte umgesetzt."},"status":{"type":"string","enum":["offen","in_arbeit","erledigt"]},"completedAt":{"type":["string","null"]},"sortOrder":{"type":"integer"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","orderId","title","description","dueDate","status","completedAt","sortOrder","createdAt","updatedAt"]}}},"required":["data"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","orderId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","dueDate":"string","status":"offen","completedAt":"string","sortOrder":0,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer","description":"Sekunden; nur bei 503."}},"required":["error"]}}}}},"operationId":"getApiV1OrdersByOrderIdMilestones","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orderId","required":true}],"summary":"Meilensteine eines Auftrags","description":"Listet die Meilensteine eines Auftrags, sortiert nach Reihenfolge, dann\nFaelligkeit (leere zuletzt), dann Anlagezeitpunkt.\n\nDer Auftrag selbst wird NICHT geprueft: eine unbekannte Auftrags-Kennung\nergibt eine leere Liste unter 200, keinen 404. Wer wissen will, ob es den\nAuftrag gibt, fragt `GET /orders/{id}`.\n\nDie Tabelle wird bei Bedarf angelegt; ein Mandant, der noch nie einen\nMeilenstein hatte, bekommt eine leere Liste statt eines Fehlers.\n\nUngegatet — jeder angemeldete Benutzer des Mandanten darf lesen."},"post":{"responses":{"201":{"description":"Der angelegte Meilenstein.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"orderId":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":"string","description":"Leerer Text statt null, wenn nichts hinterlegt ist."},"dueDate":{"type":["string","null"],"description":"YYYY-MM-DD, aus einer DATE-Spalte umgesetzt."},"status":{"type":"string","enum":["offen","in_arbeit","erledigt"]},"completedAt":{"type":["string","null"]},"sortOrder":{"type":"integer"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","orderId","title","description","dueDate","status","completedAt","sortOrder","createdAt","updatedAt"]}},"required":["data"]},"example":{"data":{"id":"00000000-0000-4000-8000-000000000000","orderId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","dueDate":"string","status":"offen","completedAt":"string","sortOrder":0,"createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Kein Schreibrecht im Modul `sales`."},"503":{"description":"Keine Datenbankverbindung — ODER ein anderer Fehler beim Schreiben, etwa ein unlesbares `dueDate`. Beide Faelle sehen gleich aus.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer","description":"Sekunden; nur bei 503."}},"required":["error"]}}}}},"operationId":"postApiV1OrdersByOrderIdMilestones","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orderId","required":true}],"summary":"Meilenstein an einem Auftrag anlegen","description":"Legt einen Meilenstein an und haengt ihn ans Ende: `sortOrder` wird\nautomatisch vergeben (hoechster Wert dieses Auftrags plus 1) und laesst\nsich beim Anlegen nicht mitgeben. Verschieben geht nachtraeglich per PATCH.\n\nDER AUFTRAG WIRD NICHT GEPRUEFT. Eine unbekannte Auftrags-Kennung ergibt\nkeinen 404, sondern einen Meilenstein, der an keinem Auftrag haengt und\nnur ueber genau dieselbe Kennung wiederzufinden ist.\n\n`status: \"erledigt\"` setzt `completedAt` auf die Serverzeit, jeder andere\nStatus laesst es leer. Ohne Angabe gilt `offen`.\n\n`dueDate` wird als ZEICHENKETTE durchgereicht und nicht auf ein Format\ngeprueft. Leer oder nur Leerzeichen heisst „kein Datum\". Ein Wert, den die\nDatenbank nicht als Datum lesen kann, landet im Sammel-Fang und kommt als\n503 zurueck, nicht als 400 — der Fehler sieht dann aus wie ein\nDatenbankausfall.\n\nDie Tabelle wird bei Bedarf angelegt.\n\nSchreibend, deshalb greift die Modul-Wache `sales`: eine Rolle unter\n`manager` ohne ausdrueckliches Schreibrecht bekommt 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":4000},"dueDate":{"type":"string"},"status":{"type":"string","enum":["offen","in_arbeit","erledigt"],"default":"offen"}},"required":["title"]},"example":{"title":"string","description":"string","dueDate":"string","status":"offen"}}}}}},"/api/v1/orders/{orderId}/milestones/{id}":{"patch":{"responses":{"200":{"description":"Der geaenderte Meilenstein.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"orderId":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":"string","description":"Leerer Text statt null, wenn nichts hinterlegt ist."},"dueDate":{"type":["string","null"],"description":"YYYY-MM-DD, aus einer DATE-Spalte umgesetzt."},"status":{"type":"string","enum":["offen","in_arbeit","erledigt"]},"completedAt":{"type":["string","null"]},"sortOrder":{"type":"integer"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","orderId","title","description","dueDate","status","completedAt","sortOrder","createdAt","updatedAt"]}},"required":["data"]},"example":{"data":{"id":"00000000-0000-4000-8000-000000000000","orderId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","dueDate":"string","status":"offen","completedAt":"string","sortOrder":0,"createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"Kein einziges aenderbares Feld im Rumpf — `error: \"no_fields\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer","description":"Sekunden; nur bei 503."}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Kein Schreibrecht im Modul `sales`."},"404":{"description":"Kein Meilenstein mit dieser Kennung AN DIESEM Auftrag.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer","description":"Sekunden; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung oder ein anderer Fehler beim Schreiben.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer","description":"Sekunden; nur bei 503."}},"required":["error"]}}}}},"operationId":"patchApiV1OrdersByOrderIdMilestonesById","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orderId","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Meilenstein aendern","description":"Echtes Teil-Update: nur die MITGESCHICKTEN Felder werden geschrieben,\nalles andere bleibt stehen. Ein leerer Rumpf `{}` aendert deshalb nichts\nund ergibt 400 `no_fields`.\n\n`status` zieht `completedAt` mit: `erledigt` setzt es auf die Serverzeit,\nJEDER andere Status setzt es zurueck auf leer. Wer einen erledigten\nMeilenstein wieder auf `in_arbeit` stellt, verliert damit den\nErledigungszeitpunkt endgueltig.\n\n`description` und `dueDate` duerfen ausdruecklich `null` sein — das\nLEERT das Feld, im Unterschied zum Weglassen.\n\nBeide Kennungen muessen zusammenpassen: ein Meilenstein, den es gibt,\nder aber zu einem anderen Auftrag gehoert, ergibt 404 und wird nicht\ngeaendert.\n\nSchreibend, deshalb greift die Modul-Wache `sales`: eine Rolle unter\n`manager` ohne ausdrueckliches Schreibrecht bekommt 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":4000},"dueDate":{"type":["string","null"]},"status":{"type":"string","enum":["offen","in_arbeit","erledigt"]},"sortOrder":{"type":"integer"}}},"example":{"title":"string","description":"string","dueDate":"string","status":"offen","sortOrder":0}}}}},"delete":{"responses":{"200":{"description":"Geloescht; genau eine Zeile war betroffen.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Kein Loeschrecht im Modul `sales`."},"404":{"description":"Kein Meilenstein mit dieser Kennung AN DIESEM Auftrag.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer","description":"Sekunden; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer","description":"Sekunden; nur bei 503."}},"required":["error"]}}}}},"operationId":"deleteApiV1OrdersByOrderIdMilestonesById","tags":["orders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orderId","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Meilenstein loeschen","description":"Entfernt den Meilenstein ENDGUELTIG — kein Papierkorb, kein\n`deleted_at`, kein Weg zurueck.\n\nBeide Kennungen muessen zusammenpassen: ein Meilenstein, den es gibt,\nder aber zu einem anderen Auftrag gehoert, ergibt 404. Und die Antwort\nsagt die Wahrheit — `{ ok: true }` kommt NUR, wenn wirklich eine Zeile\nentfernt wurde. Ein Aufruf ins Leere ist als 404 erkennbar, nicht als\nstiller Erfolg.\n\nLoeschend, deshalb greift die Modul-Wache `sales`: eine Rolle unter\n`manager` ohne ausdrueckliches Loeschrecht bekommt 403. Nur das Lesen\ndaneben ist ungegatet."}},"/api/v1/immo/properties":{"get":{"responses":{"200":{"description":"Immobilien der aktuellen Seite plus Blätter-Angaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"typ":{"type":"string"},"adresse":{"type":"object","properties":{"strasse":{"type":"string","maxLength":255},"hausnummer":{"type":"string","maxLength":20},"plz":{"type":"string","maxLength":10},"ort":{"type":"string","maxLength":120},"land":{"type":"string","maxLength":60,"default":"Deutschland"}},"additionalProperties":true},"kaufpreis":{"type":["number","null"]},"kaufdatum":{"type":["string","null"]},"aktueller_wert":{"type":["number","null"]},"wert_stand":{"type":["string","null"]},"eigentumsanteil":{"type":["number","null"]},"tags":{"type":["array","null"],"items":{"type":"string"}},"notizen":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","typ","adresse","kaufpreis","kaufdatum","aktueller_wert","wert_stand","eigentumsanteil","tags","notizen","created_at","updated_at"]}},"meta":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer","description":"Gesamtzahl der Treffer, ohne Blätterung"},"pages":{"type":"integer"}},"required":["page","limit","total","pages"]}},"required":["data","meta"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","name":"string","typ":"string","adresse":{"strasse":"string","hausnummer":"string","plz":"string","ort":"string","land":"string"},"kaufpreis":0,"kaufdatum":"string","aktueller_wert":0,"wert_stand":"string","eigentumsanteil":0,"tags":["string"],"notizen":"string","created_at":"string","updated_at":"string"}],"meta":{"page":0,"limit":0,"total":0,"pages":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoProperties","tags":["immo"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"typ","schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"Immobilien auflisten — geblättert, nach Typ und Suchtext filterbar","description":"Liest die Stammdaten aus `immobilien` im Schema des Mandanten, nur Zeilen ohne `deleted_at`, sortiert nach Name. Geblättert wird über `page` und `limit` (Standard 50, Obergrenze 200); `meta.total` nennt die Gesamtzahl der Treffer. Optional filterbar nach `typ` (exakter Vergleich) und `search` (Teiltreffer im Namen oder im Adress-JSON)."},"post":{"responses":{"201":{"description":"Die angelegte Immobilie","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"typ":{"type":"string"},"adresse":{"type":"object","properties":{"strasse":{"type":"string","maxLength":255},"hausnummer":{"type":"string","maxLength":20},"plz":{"type":"string","maxLength":10},"ort":{"type":"string","maxLength":120},"land":{"type":"string","maxLength":60,"default":"Deutschland"}},"additionalProperties":true},"kaufpreis":{"type":["string","null"],"description":"NUMERIC(14,2) als Zeichenkette"},"kaufdatum":{"type":["string","null"]},"aktueller_wert":{"type":["string","null"],"description":"NUMERIC(14,2) als Zeichenkette"},"wert_stand":{"type":["string","null"]},"eigentumsanteil":{"type":["string","null"],"description":"NUMERIC(5,2) als Zeichenkette"},"tags":{"type":["array","null"],"items":{"type":"string"}},"notizen":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","name","typ","adresse","kaufpreis","kaufdatum","aktueller_wert","wert_stand","eigentumsanteil","tags","notizen","created_at","updated_at","deleted_at"]},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","typ":"string","adresse":{"strasse":"string","hausnummer":"string","plz":"string","ort":"string","land":"string"},"kaufpreis":"string","kaufdatum":"string","aktueller_wert":"string","wert_stand":"string","eigentumsanteil":"string","tags":["string"],"notizen":"string","created_at":"string","updated_at":"string","deleted_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Validierung"}},"operationId":"postApiV1ImmoProperties","tags":["immo"],"parameters":[],"summary":"Immobilie anlegen","description":"Legt eine Zeile in `immobilien` an und gibt sie vollständig zurück (`RETURNING *`), inklusive `deleted_at`. Ohne Angabe gelten die Vorgaben `typ` = wohnung, `eigentumsanteil` = 100, `tags` = leere Liste und `adresse` = leeres JSON-Objekt; `kaufpreis`, `kaufdatum`, `aktueller_wert`, `wert_stand` und `notizen` bleiben leer.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"typ":{"type":"string","enum":["wohnung","haus","gewerbe","grundstueck","wohnung_anteil"],"default":"wohnung"},"adresse":{"type":"object","properties":{"strasse":{"type":"string","maxLength":255},"hausnummer":{"type":"string","maxLength":20},"plz":{"type":"string","maxLength":10},"ort":{"type":"string","maxLength":120},"land":{"type":"string","maxLength":60,"default":"Deutschland"}}},"kaufpreis":{"type":"number","minimum":0},"kaufdatum":{"type":"string"},"aktueller_wert":{"type":"number","minimum":0},"wert_stand":{"type":"string"},"eigentumsanteil":{"type":"number","minimum":0,"maximum":100,"default":100},"tags":{"type":"array","items":{"type":"string"}},"notizen":{"type":"string"}},"required":["name"]},"example":{"name":"string","typ":"wohnung","adresse":{"strasse":"string","hausnummer":"string","plz":"string","ort":"string","land":"string"},"kaufpreis":0,"kaufdatum":"string","aktueller_wert":0,"wert_stand":"string","eigentumsanteil":0,"tags":["string"],"notizen":"string"}}}}}},"/api/v1/immo/properties/{id}":{"get":{"responses":{"200":{"description":"Immobilie mit ihren Einheiten und Darlehen","content":{"application/json":{"schema":{"type":"object","properties":{"immobilie":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"typ":{"type":"string"},"adresse":{"type":"object","properties":{"strasse":{"type":"string","maxLength":255},"hausnummer":{"type":"string","maxLength":20},"plz":{"type":"string","maxLength":10},"ort":{"type":"string","maxLength":120},"land":{"type":"string","maxLength":60,"default":"Deutschland"}},"additionalProperties":true},"kaufpreis":{"type":["string","null"],"description":"NUMERIC(14,2) als Zeichenkette"},"kaufdatum":{"type":["string","null"]},"aktueller_wert":{"type":["string","null"],"description":"NUMERIC(14,2) als Zeichenkette"},"wert_stand":{"type":["string","null"]},"eigentumsanteil":{"type":["string","null"],"description":"NUMERIC(5,2) als Zeichenkette"},"tags":{"type":["array","null"],"items":{"type":"string"}},"notizen":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","name","typ","adresse","kaufpreis","kaufdatum","aktueller_wert","wert_stand","eigentumsanteil","tags","notizen","created_at","updated_at","deleted_at"]},"einheiten":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"immobilie_id":{"type":"string","format":"uuid"},"bezeichnung":{"type":"string"},"qm":{"type":["number","null"]},"zimmer":{"type":["number","null"]},"etage":{"type":["string","null"]},"status":{"type":["string","null"]},"notizen":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","immobilie_id","bezeichnung","qm","zimmer","etage","status","notizen","created_at","updated_at","deleted_at"]}},"darlehen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"immobilie_id":{"type":"string","format":"uuid"},"bank":{"type":["string","null"]},"vertragsnummer":{"type":["string","null"]},"darlehensart":{"type":"string"},"betrag":{"type":"number"},"sollzins_pct":{"type":"number"},"zinsbindung_bis":{"type":["string","null"]},"sondertilgung_jahres_pct":{"type":["number","null"]},"monatliche_rate":{"type":["number","null"]},"start_datum":{"type":"string"},"restschuld_aktuell":{"type":["number","null"]},"restschuld_stand":{"type":["string","null"]},"status":{"type":["string","null"]},"notizen":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","immobilie_id","bank","vertragsnummer","darlehensart","betrag","sollzins_pct","zinsbindung_bis","sondertilgung_jahres_pct","monatliche_rate","start_datum","restschuld_aktuell","restschuld_stand","status","notizen","created_at","updated_at","deleted_at"]},"description":"Nur nicht gelöschte Darlehen des Objekts"}},"required":["immobilie","einheiten","darlehen"]},"example":{"immobilie":{"id":"00000000-0000-4000-8000-000000000000","name":"string","typ":"string","adresse":{"strasse":"string","hausnummer":"string","plz":"string","ort":"string","land":"string"},"kaufpreis":"string","kaufdatum":"string","aktueller_wert":"string","wert_stand":"string","eigentumsanteil":"string","tags":["string"],"notizen":"string","created_at":"string","updated_at":"string","deleted_at":"string"},"einheiten":[{"id":"00000000-0000-4000-8000-000000000000","immobilie_id":"00000000-0000-4000-8000-000000000000","bezeichnung":"string","qm":0,"zimmer":0,"etage":"string","status":"string","notizen":"string","created_at":"string","updated_at":"string","deleted_at":"string"}],"darlehen":[{"id":"00000000-0000-4000-8000-000000000000","immobilie_id":"00000000-0000-4000-8000-000000000000","bank":"string","vertragsnummer":"string","darlehensart":"string","betrag":0,"sollzins_pct":0,"zinsbindung_bis":"string","sondertilgung_jahres_pct":0,"monatliche_rate":0,"start_datum":"string","restschuld_aktuell":0,"restschuld_stand":"string","status":"string","notizen":"string","created_at":"string","updated_at":"string","deleted_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"nicht gefunden"}},"operationId":"getApiV1ImmoPropertiesById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Eine Immobilie samt ihren Einheiten und Darlehen, jeweils ohne gelöschte Zeilen. Kennzahlen enthält diese Antwort nicht, die liefert `/:id/kpis`.","summary":"Eine Immobilie samt ihren Einheiten und Darlehen, jeweils ohne gelöschte Zeilen","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Die aktualisierte Immobilie, oder die Noop-Antwort bei leerem Rumpf","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"typ":{"type":"string"},"adresse":{"type":"object","properties":{"strasse":{"type":"string","maxLength":255},"hausnummer":{"type":"string","maxLength":20},"plz":{"type":"string","maxLength":10},"ort":{"type":"string","maxLength":120},"land":{"type":"string","maxLength":60,"default":"Deutschland"}},"additionalProperties":true},"kaufpreis":{"type":["string","null"],"description":"NUMERIC(14,2) als Zeichenkette"},"kaufdatum":{"type":["string","null"]},"aktueller_wert":{"type":["string","null"],"description":"NUMERIC(14,2) als Zeichenkette"},"wert_stand":{"type":["string","null"]},"eigentumsanteil":{"type":["string","null"],"description":"NUMERIC(5,2) als Zeichenkette"},"tags":{"type":["array","null"],"items":{"type":"string"}},"notizen":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","name","typ","adresse","kaufpreis","kaufdatum","aktueller_wert","wert_stand","eigentumsanteil","tags","notizen","created_at","updated_at","deleted_at"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok","noop"]}]},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","typ":"string","adresse":{"strasse":"string","hausnummer":"string","plz":"string","ort":"string","land":"string"},"kaufpreis":"string","kaufdatum":"string","aktueller_wert":"string","wert_stand":"string","eigentumsanteil":"string","tags":["string"],"notizen":"string","created_at":"string","updated_at":"string","deleted_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"nicht gefunden"}},"operationId":"putApiV1ImmoPropertiesById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Immobilie ändern, nur übergebene Felder","description":"Schreibt nur die im Rumpf übergebenen Felder und setzt `updated_at` neu; die geänderte Zeile kommt vollständig zurück. Enthält der Rumpf kein änderbares Feld, antwortet die Route mit `{ \"ok\": true, \"noop\": true }` und fasst die Datenbank nicht an. Trifft die id keine Zeile ohne `deleted_at`, kommt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"typ":{"type":"string","enum":["wohnung","haus","gewerbe","grundstueck","wohnung_anteil"],"default":"wohnung"},"adresse":{"type":"object","properties":{"strasse":{"type":"string","maxLength":255},"hausnummer":{"type":"string","maxLength":20},"plz":{"type":"string","maxLength":10},"ort":{"type":"string","maxLength":120},"land":{"type":"string","maxLength":60,"default":"Deutschland"}}},"kaufpreis":{"type":"number","minimum":0},"kaufdatum":{"type":"string"},"aktueller_wert":{"type":"number","minimum":0},"wert_stand":{"type":"string"},"eigentumsanteil":{"type":"number","minimum":0,"maximum":100,"default":100},"tags":{"type":"array","items":{"type":"string"}},"notizen":{"type":"string"}}},"example":{"name":"string","typ":"wohnung","adresse":{"strasse":"string","hausnummer":"string","plz":"string","ort":"string","land":"string"},"kaufpreis":0,"kaufdatum":"string","aktueller_wert":0,"wert_stand":"string","eigentumsanteil":0,"tags":["string"],"notizen":"string"}}}}},"delete":{"responses":{"200":{"description":"Löschen angenommen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1ImmoPropertiesById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt `deleted_at` auf NOW(). Die Zeile bleibt in der Tabelle und verschwindet aus Liste und Detailabruf, verknüpfte Einheiten, Darlehen und Kosten bleiben unberührt. Die Route prüft nicht, ob eine Zeile getroffen wurde, und antwortet auch bei unbekannter id mit `{ \"ok\": true }`.","summary":"Setzt `deleted_at` auf NOW()","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/properties/{id}/kpis":{"get":{"responses":{"200":{"description":"Kennzahlen des Objekts","content":{"application/json":{"schema":{"type":"object","properties":{"mieteinnahmen_ytd":{"type":"number"},"kosten_ytd":{"type":"number"},"cashflow_ytd":{"type":"number"},"restschuld_aktuell":{"type":"number"},"mietrendite_pct":{"type":["number","null"],"description":"null ohne hinterlegten Kaufpreis"},"vermietungsquote_pct":{"type":["number","null"],"description":"null ohne angelegte Einheit"},"einheiten_gesamt":{"type":"integer"},"einheiten_vermietet":{"type":"integer"}},"required":["mieteinnahmen_ytd","kosten_ytd","cashflow_ytd","restschuld_aktuell","mietrendite_pct","vermietungsquote_pct","einheiten_gesamt","einheiten_vermietet"]},"example":{"mieteinnahmen_ytd":0,"kosten_ytd":0,"cashflow_ytd":0,"restschuld_aktuell":0,"mietrendite_pct":0,"vermietungsquote_pct":0,"einheiten_gesamt":0,"einheiten_vermietet":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoPropertiesByIdKpis","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aggregiert seit Jahresbeginn: tatsächlich eingegangene Mieten aus `mietzahlungen`, Kosten aus `immobilien_kosten` und deren Differenz als Cashflow, dazu die Restschuld aller aktiven Darlehen und die Vermietungsquote der Einheiten. `mietrendite_pct` bleibt ohne hinterlegten Kaufpreis leer, `vermietungsquote_pct` ohne angelegte Einheit. Eine unbekannte oder gelöschte id ergibt kein 404, sondern eine Antwort mit Nullwerten.","summary":"Aggregiert seit Jahresbeginn","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/properties/{id}/rendite":{"get":{"responses":{"200":{"description":"Rendite-Analyse, oder die Hinweis-Antwort ohne hinterlegten Kaufpreis","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"eingang":{"type":"object","properties":{"kaufpreis":{"type":"number"},"eigenkapital":{"type":"number"},"darlehensumme":{"type":"number"},"jahres_kaltmiete":{"type":"number"},"jahres_bewirtschaftungskosten":{"type":"number"},"jahres_kapitaldienst":{"type":"number"}},"required":["kaufpreis","eigenkapital","darlehensumme","jahres_kaltmiete","jahres_bewirtschaftungskosten","jahres_kapitaldienst"]},"rendite":{"type":"object","properties":{"brutto_rendite_pct":{"type":"number"},"netto_rendite_pct":{"type":"number"},"jahres_cashflow":{"type":"number"},"eigenkapital_rendite_pct":{"type":["number","null"],"description":"null ohne Eigenkapital"},"gesamt_investition":{"type":"number"}},"required":["brutto_rendite_pct","netto_rendite_pct","jahres_cashflow","eigenkapital_rendite_pct","gesamt_investition"]}},"required":["eingang","rendite"]},{"type":"object","properties":{"hinweis":{"type":"string"},"jahres_kaltmiete":{"type":"number"},"jahres_bewirtschaftungskosten":{"type":"number"},"jahres_kapitaldienst":{"type":"number"}},"required":["hinweis","jahres_kaltmiete","jahres_bewirtschaftungskosten","jahres_kapitaldienst"]}]},"example":{"eingang":{"kaufpreis":0,"eigenkapital":0,"darlehensumme":0,"jahres_kaltmiete":0,"jahres_bewirtschaftungskosten":0,"jahres_kapitaldienst":0},"rendite":{"brutto_rendite_pct":0,"netto_rendite_pct":0,"jahres_cashflow":0,"eigenkapital_rendite_pct":0,"gesamt_investition":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"nicht gefunden"}},"operationId":"getApiV1ImmoPropertiesByIdRendite","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rendite und Jahres-Cashflow einer Immobilie berechnen","description":"Rechnet Brutto-, Netto- und Eigenkapitalrendite sowie Jahres-Cashflow aus den verknüpften Daten: Jahres-Kaltmiete als Monatssumme der aktiven Mietverträge mal zwölf, Bewirtschaftungskosten der letzten zwölf Monate und Kapitaldienst aus den aktiven Darlehen. Das Eigenkapital wird vereinfacht als Kaufpreis minus aufgenommene Darlehenssumme angesetzt. Ohne hinterlegten Kaufpreis antwortet die Route bewusst mit 200 und liefert statt der Rendite nur die drei Jahreswerte samt `hinweis`; eine unbekannte id ergibt 404."}},"/api/v1/immo/loans":{"get":{"responses":{"200":{"description":"Alle Darlehen des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Darlehens"},"immobilie_id":{"type":"string","format":"uuid","description":"Beliehene Immobilie"},"bank":{"type":["string","null"],"description":"Kreditinstitut; null wenn nicht erfasst"},"vertragsnummer":{"type":["string","null"],"description":"Vertragsnummer bei der Bank; null wenn nicht erfasst"},"darlehensart":{"type":"string","description":"annuitaet, tilgung oder endfaellig"},"betrag":{"type":"number","description":"Darlehensbetrag in EUR"},"sollzins_pct":{"type":"number","description":"Sollzins in Prozent pro Jahr"},"zinsbindung_bis":{"type":["string","null"],"description":"Ende der Zinsbindung; null wenn keine erfasst ist"},"sondertilgung_jahres_pct":{"type":["number","null"],"description":"Jaehrlich zulaessige Sondertilgung in Prozent"},"monatliche_rate":{"type":["number","null"],"description":"Vereinbarte Monatsrate in EUR"},"start_datum":{"type":"string","description":"Beginn des Darlehens"},"restschuld_aktuell":{"type":["number","null"],"description":"Restschuld in EUR. Beim Anlegen gleich `betrag`; erst `POST /{id}/tilgungsplan/regenerate` schreibt sie fort"},"restschuld_stand":{"type":["string","null"],"description":"Stichtag der Restschuld"},"status":{"type":["string","null"],"description":"aktiv, abgeloest oder gekuendigt"},"notizen":{"type":["string","null"],"description":"Bemerkung; null wenn keine erfasst ist"},"created_at":{"type":"string","description":"Anlagezeitpunkt"},"updated_at":{"type":"string","description":"Letzte Aenderung"},"deleted_at":{"type":["string","null"],"description":"Zeitpunkt des Loeschens; null bei bestehenden Darlehen"}},"required":["id","immobilie_id","bank","vertragsnummer","darlehensart","betrag","sollzins_pct","zinsbindung_bis","sondertilgung_jahres_pct","monatliche_rate","start_datum","restschuld_aktuell","restschuld_stand","status","notizen","created_at","updated_at","deleted_at"]},"description":"Alle nicht geloeschten Darlehen, neuestes Startdatum zuerst"}},"required":["data"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","immobilie_id":"00000000-0000-4000-8000-000000000000","bank":"string","vertragsnummer":"string","darlehensart":"string","betrag":0,"sollzins_pct":0,"zinsbindung_bis":"string","sondertilgung_jahres_pct":0,"monatliche_rate":0,"start_datum":"string","restschuld_aktuell":0,"restschuld_stand":"string","status":"string","notizen":"string","created_at":"string","updated_at":"string","deleted_at":"string"}]}}}},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"`database_unavailable`"}},"operationId":"getApiV1ImmoLoans","tags":["immo"],"parameters":[],"summary":"Alle Darlehen des Mandanten, ohne Blaetterung","description":"Liest alle nicht geloeschten Darlehen des Mandanten aus `darlehen`, neuestes Startdatum zuerst. Es wird NICHT geblaettert und nicht gefiltert — es kommen immer alle, und es kommt keine Gesamtzahl. Betragsfelder kommen als Zahl, nicht als Zeichenkette. Der Tilgungsplan ist nicht dabei; den liefert `GET /{id}/tilgungsplan`."},"post":{"responses":{"201":{"description":"Das angelegte Darlehen, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Darlehens"},"immobilie_id":{"type":"string","format":"uuid","description":"Beliehene Immobilie"},"bank":{"type":["string","null"],"description":"Kreditinstitut; null wenn nicht erfasst"},"vertragsnummer":{"type":["string","null"],"description":"Vertragsnummer bei der Bank; null wenn nicht erfasst"},"darlehensart":{"type":"string","description":"annuitaet, tilgung oder endfaellig"},"betrag":{"type":"number","description":"Darlehensbetrag in EUR"},"sollzins_pct":{"type":"number","description":"Sollzins in Prozent pro Jahr"},"zinsbindung_bis":{"type":["string","null"],"description":"Ende der Zinsbindung; null wenn keine erfasst ist"},"sondertilgung_jahres_pct":{"type":["number","null"],"description":"Jaehrlich zulaessige Sondertilgung in Prozent"},"monatliche_rate":{"type":["number","null"],"description":"Vereinbarte Monatsrate in EUR"},"start_datum":{"type":"string","description":"Beginn des Darlehens"},"restschuld_aktuell":{"type":["number","null"],"description":"Restschuld in EUR. Beim Anlegen gleich `betrag`; erst `POST /{id}/tilgungsplan/regenerate` schreibt sie fort"},"restschuld_stand":{"type":["string","null"],"description":"Stichtag der Restschuld"},"status":{"type":["string","null"],"description":"aktiv, abgeloest oder gekuendigt"},"notizen":{"type":["string","null"],"description":"Bemerkung; null wenn keine erfasst ist"},"created_at":{"type":"string","description":"Anlagezeitpunkt"},"updated_at":{"type":"string","description":"Letzte Aenderung"},"deleted_at":{"type":["string","null"],"description":"Zeitpunkt des Loeschens; null bei bestehenden Darlehen"}},"required":["id","immobilie_id","bank","vertragsnummer","darlehensart","betrag","sollzins_pct","zinsbindung_bis","sondertilgung_jahres_pct","monatliche_rate","start_datum","restschuld_aktuell","restschuld_stand","status","notizen","created_at","updated_at","deleted_at"]},"example":{"id":"00000000-0000-4000-8000-000000000000","immobilie_id":"00000000-0000-4000-8000-000000000000","bank":"string","vertragsnummer":"string","darlehensart":"string","betrag":0,"sollzins_pct":0,"zinsbindung_bis":"string","sondertilgung_jahres_pct":0,"monatliche_rate":0,"start_datum":"string","restschuld_aktuell":0,"restschuld_stand":"string","status":"string","notizen":"string","created_at":"string","updated_at":"string","deleted_at":"string"}}}},"400":{"description":"Eingabe ungültig oder Mandanten-Slug unzulaessig"},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"`database_unavailable`"}},"operationId":"postApiV1ImmoLoans","tags":["immo"],"parameters":[],"description":"Legt ein Darlehen an. `restschuld_aktuell` wird auf den vollen Betrag und `restschuld_stand` auf das Startdatum gesetzt; ein Tilgungsplan entsteht dabei NICHT — dafuer anschliessend `POST /{id}/tilgungsplan/regenerate` aufrufen. Ohne `darlehensart` gilt `annuitaet`, ohne `status` gilt `aktiv`, ohne `sondertilgung_jahres_pct` und `monatliche_rate` jeweils 0. Ob es die `immobilie_id` gibt, wird NICHT geprueft. Die Antwort ist das Darlehen SELBST, ohne umschliessendes Feld.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"immobilie_id":{"type":"string","format":"uuid"},"bank":{"type":"string","maxLength":255},"vertragsnummer":{"type":"string","maxLength":120},"darlehensart":{"type":"string","enum":["annuitaet","tilgung","endfaellig"],"default":"annuitaet"},"betrag":{"type":"number","exclusiveMinimum":0},"sollzins_pct":{"type":"number","minimum":0,"maximum":50},"zinsbindung_bis":{"type":"string"},"sondertilgung_jahres_pct":{"type":"number","minimum":0,"maximum":100},"monatliche_rate":{"type":"number","minimum":0},"start_datum":{"type":"string"},"status":{"type":"string","enum":["aktiv","abgeloest","gekuendigt"],"default":"aktiv"},"notizen":{"type":"string"}},"required":["immobilie_id","betrag","sollzins_pct","start_datum"]},"example":{"immobilie_id":"00000000-0000-4000-8000-000000000000","bank":"string","vertragsnummer":"string","darlehensart":"annuitaet","betrag":1,"sollzins_pct":0,"zinsbindung_bis":"string","sondertilgung_jahres_pct":0,"monatliche_rate":0,"start_datum":"string","status":"aktiv","notizen":"string"}}}},"summary":"Legt ein Darlehen an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/loans/{id}/tilgungsplan/regenerate":{"post":{"responses":{"200":{"description":"Bilanz des Laufs: Monatszahl, Laufzeit und Summen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"monatscount":{"type":"integer","minimum":0,"description":"Anzahl der geschriebenen Monatszeilen"},"laufzeit_jahre":{"type":"integer","minimum":0,"description":"Monatszahl auf volle Jahre aufgerundet"},"summe_zinsen":{"type":"number","description":"Summe aller Zinsanteile in EUR"},"summe_tilgung":{"type":"number","description":"Summe aller Tilgungsanteile in EUR"},"summe_sondertilgung":{"type":"number","description":"Summe aller verrechneten Sondertilgungen in EUR"},"restschuld_ende":{"type":"number","description":"Restschuld nach der letzten Zeile in EUR; ohne Plan der volle Darlehensbetrag"}},"required":["ok","monatscount","laufzeit_jahre","summe_zinsen","summe_tilgung","summe_sondertilgung","restschuld_ende"]},"example":{"ok":true,"monatscount":0,"laufzeit_jahre":0,"summe_zinsen":0,"summe_tilgung":0,"summe_sondertilgung":0,"restschuld_ende":0}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"Kein Mandantenkontext"},"404":{"description":"`darlehen_not_found`"},"503":{"description":"`database_unavailable`"}},"operationId":"postApiV1ImmoLoansByIdTilgungsplanRegenerate","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Rechnet den Tilgungsplan komplett neu und ERSETZT den gespeicherten: der alte Plan wird geloescht, der neue eingefuegt, und `restschuld_aktuell` auf die Restschuld der letzten Zeile fortgeschrieben (`restschuld_stand` auf heute). Alle gebuchten Sondertilgungen des Darlehens gehen in die Rechnung ein — dies ist der einzige Ort, an dem sie wirksam werden. `max_monate` begrenzt die Laufzeit (Vorgabe 360, hoechstens 600); `anfangs_tilgung_pct` gilt nur bei `annuitaet`, `monatliche_tilgung_eur` nur bei `tilgung`. Loeschen und Einfuegen laufen OHNE Transaktion: bricht der Lauf dazwischen ab, hat das Darlehen gar keinen Plan mehr. Der Aufruf ist wiederholbar. Zurueck kommt die Bilanz des Laufs, NICHT der Plan selbst — den liefert `GET /{id}/tilgungsplan`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"anfangs_tilgung_pct":{"type":"number","minimum":0.5,"maximum":20},"monatliche_tilgung_eur":{"type":"number","exclusiveMinimum":0},"max_monate":{"type":"integer","exclusiveMinimum":0,"maximum":600,"default":360}}},"example":{"anfangs_tilgung_pct":0.5,"monatliche_tilgung_eur":1,"max_monate":1}}}},"summary":"Rechnet den Tilgungsplan komplett neu und ERSETZT den gespeicherten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/loans/{id}/tilgungsplan":{"get":{"responses":{"200":{"description":"Monatszeilen und Jahres-Zusammenfassung; beide leer wenn kein Plan vorliegt","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"array","items":{"type":"object","properties":{"monat":{"type":"string","description":"Monat der Rate"},"rate":{"type":"number","description":"Rate des Monats in EUR"},"zinsanteil":{"type":"number","description":"Zinsanteil der Rate in EUR"},"tilgungsanteil":{"type":"number","description":"Tilgungsanteil der Rate in EUR"},"restschuld_nach":{"type":"number","description":"Restschuld nach diesem Monat in EUR"},"sondertilgung":{"type":"number","description":"In diesem Monat verrechnete Sondertilgung in EUR"}},"required":["monat","rate","zinsanteil","tilgungsanteil","restschuld_nach","sondertilgung"]},"description":"Die gespeicherten Monatszeilen, chronologisch; leer solange kein Plan gerechnet wurde"},"summary_pro_jahr":{"type":"array","items":{"type":"object","properties":{"jahr":{"type":"integer","description":"Kalenderjahr"},"rate":{"type":"number","description":"Summe der Raten des Jahres in EUR"},"zinsanteil":{"type":"number","description":"Summe der Zinsen des Jahres in EUR"},"tilgungsanteil":{"type":"number","description":"Summe der Tilgung des Jahres in EUR"},"sondertilgung":{"type":"number","description":"Summe der Sondertilgungen des Jahres in EUR"},"restschuld_ende":{"type":"number","description":"Restschuld nach der LETZTEN Zeile des Jahres in EUR"}},"required":["jahr","rate","zinsanteil","tilgungsanteil","sondertilgung","restschuld_ende"]},"description":"Der Plan zu Jahren zusammengefasst, aufsteigend"}},"required":["plan","summary_pro_jahr"]},"example":{"plan":[{"monat":"string","rate":0,"zinsanteil":0,"tilgungsanteil":0,"restschuld_nach":0,"sondertilgung":0}],"summary_pro_jahr":[{"jahr":0,"rate":0,"zinsanteil":0,"tilgungsanteil":0,"sondertilgung":0,"restschuld_ende":0}]}}}},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"`database_unavailable`"}},"operationId":"getApiV1ImmoLoansByIdTilgungsplan","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Gespeicherten Tilgungsplan samt Jahres-Zusammenfassung lesen","description":"Liefert den GESPEICHERTEN Tilgungsplan chronologisch, dazu eine Zusammenfassung je Kalenderjahr. Es wird NICHTS neu gerechnet — solange kein Plan erzeugt wurde, sind `plan` und `summary_pro_jahr` leer. Ein unbekanntes Darlehen ergibt KEIN 404, sondern dieselbe leere Antwort: von einem Darlehen ohne Plan ist es hier nicht zu unterscheiden. Es wird nicht geblaettert."}},"/api/v1/immo/loans/{id}/sondertilgung":{"post":{"responses":{"200":{"description":"Sondertilgung gebucht; der Plan bleibt bis zur Neuberechnung unveraendert","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"hint":{"type":"string","description":"Hinweis, dass der Tilgungsplan NICHT neu gerechnet wurde und dafuer ein eigener Aufruf noetig ist"}},"required":["ok","hint"]},"example":{"ok":true,"hint":"string"}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"`database_unavailable`"}},"operationId":"postApiV1ImmoLoansByIdSondertilgung","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Bucht eine Sondertilgung in `darlehen_sondertilgungen`. Der Tilgungsplan wird dabei NICHT neu gerechnet und `restschuld_aktuell` NICHT fortgeschrieben — die Antwort sagt das im Feld `hint`, und bis `POST /{id}/tilgungsplan/regenerate` gelaufen ist, zeigen Plan und Restschuld den Stand VOR der Sondertilgung. Weder die Existenz des Darlehens noch die jaehrlich zulaessige Sondertilgung (`sondertilgung_jahres_pct`) werden geprueft: eine Buchung auf eine unbekannte Kennung wird angenommen und ergibt kein 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"datum":{"type":"string"},"betrag":{"type":"number","exclusiveMinimum":0},"anlass":{"type":"string","maxLength":255}},"required":["datum","betrag"]},"example":{"datum":"string","betrag":1,"anlass":"string"}}}},"summary":"Bucht eine Sondertilgung in `darlehen_sondertilgungen`","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/afa/{immobilie_id}":{"get":{"responses":{"200":{"description":"AfA-Jahre, aufsteigend","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"immobilie_id":{"type":"string"},"jahr":{"type":"integer"},"afa_basis":{"type":"number"},"afa_satz_pct":{"type":"number"},"afa_betrag":{"type":"number"},"kumulierte_afa":{"type":"number"},"restwert":{"type":["number","null"]},"methode":{"type":["string","null"]},"notizen":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","immobilie_id","jahr","afa_basis","afa_satz_pct","afa_betrag","kumulierte_afa","restwert","methode","notizen","created_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","immobilie_id":"string","jahr":0,"afa_basis":0,"afa_satz_pct":0,"afa_betrag":0,"kumulierte_afa":0,"restwert":0,"methode":"string","notizen":"string","created_at":"string"}]}}}},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1ImmoAfaByImmobilie_id","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"immobilie_id","required":true}],"summary":"Gespeicherte AfA-Jahre einer Immobilie","description":"Liest den zuletzt ueber POST /generate erzeugten Plan aus der Datenbank — es wird NICHTS neu gerechnet. Eine Zeile je Jahr, aufsteigend sortiert, ohne Blaetterung und ohne weitere Filter. Gibt es keinen gespeicherten Plan, kommt eine leere Liste; das heiszt „noch nicht erzeugt\", nicht „keine AfA\". Die Kennung wird nicht gegen die Immobilientabelle geprueft."}},"/api/v1/immo/afa/generate":{"post":{"responses":{"200":{"description":"Plan erzeugt und gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"jahre_generiert":{"type":"integer"},"afa_satz_pct":{"type":"number"},"summe_afa":{"type":"number"}},"required":["ok","jahre_generiert","afa_satz_pct","summe_afa"]},"example":{"ok":true,"jahre_generiert":0,"afa_satz_pct":0,"summe_afa":0}}}},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1ImmoAfaGenerate","tags":["immo"],"parameters":[],"summary":"AfA-Plan berechnen + persistieren (§7 EStG)","description":"Rechnet den Plan aus Bemessungsgrundlage, Baujahr, Startjahr, Laufzeit und Methode und schreibt ihn fest. ERSETZEND: alle bereits gespeicherten AfA-Jahre dieser Immobilie werden zuvor GELOESCHT — ein zweiter Aufruf mit anderen Werten wirft den alten Plan weg, und es gibt kein Rueckgaengig. Loeschen und Schreiben laufen NICHT in einer gemeinsamen Transaktion: bricht der Vorgang mittendrin ab, kann ein unvollstaendiger Plan zurueckbleiben. Bei „linear\" ergibt sich der Satz aus dem Baujahr, bei „degressiv\" sind es fest 5,0 %. Es entsteht keine Journalbuchung; die Immobilie selbst wird nicht angefasst und ihre Kennung nicht geprueft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"immobilie_id":{"type":"string","format":"uuid"},"afa_basis":{"type":"number","exclusiveMinimum":0},"baujahr":{"type":"integer","minimum":1800,"maximum":2100},"start_jahr":{"type":"integer","minimum":1800,"maximum":2200},"jahre":{"type":"integer","minimum":1,"maximum":60},"methode":{"type":"string","enum":["linear","degressiv"],"default":"linear"}},"required":["immobilie_id","afa_basis","baujahr","start_jahr","jahre"]},"example":{"immobilie_id":"00000000-0000-4000-8000-000000000000","afa_basis":1,"baujahr":1800,"start_jahr":1800,"jahre":1,"methode":"linear"}}}}}},"/api/v1/immo/postfach":{"get":{"responses":{"200":{"description":"Die gefilterten Eintraege plus die Status-Zaehler fuer die Bahnen.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"immobilie_id":{"type":["string","null"]},"von_email":{"type":"string"},"betreff":{"type":["string","null"]},"eingang":{"type":"string","description":"Eingangszeitpunkt als ISO-Zeichenkette"},"channel":{"type":"string","description":"email | whatsapp | upload | manual"},"status":{"type":["string","null"],"description":"neu | gelesen | zugeordnet | verarbeitet | archiviert"},"ai_classification":{"description":"Ergebnis der KI-Klassifizierung; null, solange keine lief"},"ai_confidence":{"type":["number","null"],"description":"0..1, hier bereits in eine Zahl gewandelt"}},"required":["id","immobilie_id","von_email","betreff","eingang","channel","status","ai_confidence"]}},"counts":{"type":"object","additionalProperties":{"type":"number"},"description":"Anzahl je Status — ueber ALLE Eintraege, nicht nur die gelieferte Seite"}},"required":["data","counts"]},"example":{"data":[{"id":"string","immobilie_id":"string","von_email":"string","betreff":"string","eingang":"string","channel":"string","status":"string","ai_confidence":0}],"counts":{"beispiel":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoPostfach","tags":["immo"],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["neu","gelesen","zugeordnet","verarbeitet","archiviert"]}},{"in":"query","name":"immobilie_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"description":"Listet die Postfach-Eintraege des Mandanten, neueste zuerst (nach Eingang). `status` und `immobilie_id` grenzen ein, `limit` begrenzt die Zeilen (1..200, Vorgabe 50) — eine Blaetterung gibt es nicht, aeltere Eintraege sind ueber die Filter zu erreichen. Neben den Zeilen kommt `counts`: die Anzahl je Status ueber ALLE Eintraege, unabhaengig von Filter und Limit. Ohne Datenbankverbindung 503.","summary":"Listet die Postfach-Eintraege des Mandanten, neueste zuerst (nach Eingang)","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Der angelegte Eintrag, vollstaendig wie er in der Tabelle steht.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"immobilie_id":{"type":["string","null"]},"von_email":{"type":"string"},"betreff":{"type":["string","null"]},"eingang":{"type":"string"},"body_text":{"type":["string","null"]},"attachments":{},"channel":{"type":"string"},"status":{"type":["string","null"]},"ai_classification":{},"ai_confidence":{"type":["string","null"],"description":"NUMERIC — hier ROH, also als Zeichenkette"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"uid":{"type":["number","null"],"description":"IMAP-Nummer, nur bei abgeholten Mails"},"suggested_action_jsonb":{},"auto_action_status":{"type":["string","null"],"description":"applied | dismissed"},"calendar_event_id":{"type":["string","null"]}},"required":["id","immobilie_id","von_email","betreff","eingang","body_text","channel","status","ai_confidence","created_at","updated_at"]},"example":{"id":"string","immobilie_id":"string","von_email":"string","betreff":"string","eingang":"string","body_text":"string","channel":"string","status":"string","ai_confidence":"string","created_at":"string","updated_at":"string","uid":0,"auto_action_status":"string","calendar_event_id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ImmoPostfach","tags":["immo"],"parameters":[],"description":"Legt einen Postfach-Eintrag von Hand an — fuer alles, was nicht ueber den IMAP-Abruf hereinkommt. Pflicht ist nur `von_email`; `channel` ist ohne Angabe `manual`. Der Eintrag startet im Status `neu` und ist damit noch NICHT klassifiziert: das uebernimmt erst `/:id/classify` oder `/classify-pending`. Ohne Datenbankverbindung 503.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"immobilie_id":{"type":"string","format":"uuid"},"von_email":{"type":"string","format":"email"},"betreff":{"type":"string","maxLength":500},"body_text":{"type":"string"},"channel":{"type":"string","enum":["email","whatsapp","upload","manual"],"default":"manual"}},"required":["von_email"]},"example":{"immobilie_id":"00000000-0000-4000-8000-000000000000","von_email":"beispiel@example.com","betreff":"string","body_text":"string","channel":"email"}}}},"summary":"Legt einen Postfach-Eintrag von Hand an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/postfach/{id}":{"patch":{"responses":{"200":{"description":"Der geaenderte Eintrag. Bei leerem Rumpf stattdessen `{ ok, noop }` — es wurde nichts geschrieben.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"immobilie_id":{"type":["string","null"]},"von_email":{"type":"string"},"betreff":{"type":["string","null"]},"eingang":{"type":"string"},"body_text":{"type":["string","null"]},"attachments":{},"channel":{"type":"string"},"status":{"type":["string","null"]},"ai_classification":{},"ai_confidence":{"type":["string","null"],"description":"NUMERIC — hier ROH, also als Zeichenkette"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"uid":{"type":["number","null"],"description":"IMAP-Nummer, nur bei abgeholten Mails"},"suggested_action_jsonb":{},"auto_action_status":{"type":["string","null"],"description":"applied | dismissed"},"calendar_event_id":{"type":["string","null"]}},"required":["id","immobilie_id","von_email","betreff","eingang","body_text","channel","status","ai_confidence","created_at","updated_at"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok","noop"]}]},"example":{"id":"string","immobilie_id":"string","von_email":"string","betreff":"string","eingang":"string","body_text":"string","channel":"string","status":"string","ai_confidence":"string","created_at":"string","updated_at":"string","uid":0,"auto_action_status":"string","calendar_event_id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Eintrag nicht gefunden"}},"operationId":"patchApiV1ImmoPostfachById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt Status und/oder Objekt-Zuordnung eines Eintrags. Beide Felder sind einzeln optional; was nicht im Rumpf steht, bleibt unveraendert. Ein Rumpf OHNE beide Felder aendert nichts und antwortet 200 mit `noop: true` — nicht 400. Ein unbekannter Eintrag ergibt 404. Ohne Datenbankverbindung 503.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["neu","gelesen","zugeordnet","verarbeitet","archiviert"]},"immobilie_id":{"type":"string","format":"uuid"}}},"example":{"status":"neu","immobilie_id":"00000000-0000-4000-8000-000000000000"}}}},"summary":"Setzt Status und/oder Objekt-Zuordnung eines Eintrags","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/postfach/classify-pending":{"post":{"responses":{"200":{"description":"Zahl der bearbeiteten Eintraege und je Eintrag das Ergebnis.","content":{"application/json":{"schema":{"type":"object","properties":{"classified":{"type":"integer","description":"Zahl der bearbeiteten Eintraege"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"kategorie":{"type":"string"},"confidence":{"type":"number","description":"0..1"},"immobilie_id":{"type":["string","null"],"description":"Zugeordnetes Objekt, sonst null"}},"required":["id","kategorie","confidence","immobilie_id"]}}},"required":["classified","results"]},"example":{"classified":0,"results":[{"id":"string","kategorie":"string","confidence":0,"immobilie_id":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ImmoPostfachClassify-pending","tags":["immo"],"parameters":[],"summary":"Klassifiziert alle offenen Postfach-Eintraege per KI (bis zu 25)","description":"Nimmt bis zu 25 Eintraege im Status `neu` ohne Klassifizierung — aelteste zuerst — und laesst sie einzeln vom Modell einordnen. Je Eintrag werden Kategorie, Konfidenz und Objekt-Zuordnung geschrieben; eine bereits gesetzte Zuordnung bleibt bestehen. Wo eine Zuordnung entsteht, springt der Status auf `zugeordnet`. Gibt es nichts zu tun, kommt 200 mit `classified: 0`. Der Aufruf laeuft nacheinander und ist bei 25 Eintraegen entsprechend lang."}},"/api/v1/immo/postfach/{id}/classify":{"post":{"responses":{"200":{"description":"Der Eintrag nach der Klassifizierung, vollstaendig.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"immobilie_id":{"type":["string","null"]},"von_email":{"type":"string"},"betreff":{"type":["string","null"]},"eingang":{"type":"string"},"body_text":{"type":["string","null"]},"attachments":{},"channel":{"type":"string"},"status":{"type":["string","null"]},"ai_classification":{},"ai_confidence":{"type":["number","null"],"description":"0..1, hier bereits in eine Zahl gewandelt"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"uid":{"type":["number","null"],"description":"IMAP-Nummer, nur bei abgeholten Mails"},"suggested_action_jsonb":{},"auto_action_status":{"type":["string","null"],"description":"applied | dismissed"},"calendar_event_id":{"type":["string","null"]}},"required":["id","immobilie_id","von_email","betreff","eingang","body_text","channel","status","ai_confidence","created_at","updated_at"]},"example":{"id":"string","immobilie_id":"string","von_email":"string","betreff":"string","eingang":"string","body_text":"string","channel":"string","status":"string","ai_confidence":0,"created_at":"string","updated_at":"string","uid":0,"auto_action_status":"string","calendar_event_id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Eintrag nicht gefunden"}},"operationId":"postApiV1ImmoPostfachByIdClassify","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Klassifiziert einen Postfach-Eintrag per KI und ordnet ihn zu","description":"Laesst Absender, Betreff und Text eines Eintrags vom Modell einordnen und schreibt Kategorie, Konfidenz und Objekt-Zuordnung zurueck. Eine bereits gesetzte (manuelle) Zuordnung GEWINNT gegen den Vorschlag des Modells. Automatisch zugeordnet wird nur ab einer Konfidenz von 0,5; entsteht dabei eine Zuordnung und stand der Eintrag auf `neu`, wechselt der Status auf `zugeordnet` — ein anderer Status bleibt unangetastet. Der Aufruf ist wiederholbar und ueberschreibt die vorige Klassifizierung. Unbekannter Eintrag: 404."}},"/api/v1/immo/postfach/{id}/apply-action":{"post":{"responses":{"200":{"description":"Ausgefuehrt: `applied` nennt Art und — wo es einen gibt — den angelegten bzw. getroffenen Datensatz. War die Aktion schon angewendet, kommt stattdessen `idempotent: true` und es passiert nichts.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"idempotent":{"type":"boolean","const":true},"applied":{"type":"object","properties":{"type":{"type":"string","description":"create_hausgeld_kosten | create_handwerker_kosten | match_mietzahlung | set_versicherung_reminder"},"entityId":{"type":"string","description":"Id des angelegten bzw. getroffenen Datensatzes"}},"required":["type"]}},"required":["ok"]},"example":{"ok":true,"idempotent":true,"applied":{"type":"string","entityId":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Eintrag nicht gefunden"},"422":{"description":"Kein Vorschlag hinterlegt, oder dem Vorschlag fehlt etwas zum Ausfuehren (Objekt, Monat, Betrag, offene Sollposition)."}},"operationId":"postApiV1ImmoPostfachByIdApply-action","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Vorgeschlagene Auto-Aktion eines Postfach-Eintrags ausfuehren","description":"Wendet die vorgeschlagene Auto-Aktion eines Postfach-Eintrags an (z.B. Kostenposition anlegen)."}},"/api/v1/immo/postfach/{id}/dismiss-action":{"post":{"responses":{"200":{"description":"Verworfen — die Antwort traegt nur die Bestaetigung.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Eintrag nicht gefunden"}},"operationId":"postApiV1ImmoPostfachByIdDismiss-action","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Verwirft den hinterlegten Auto-Aktions-Vorschlag: der Vorschlag wird GELOESCHT (nicht nur ausgeblendet) und der Eintrag auf `dismissed` gesetzt. Erneut erzeugen laesst er sich nur ueber eine neue Klassifizierung. Status und Zuordnung des Eintrags bleiben unberuehrt. Unbekannter Eintrag: 404.","summary":"Verwirft den hinterlegten Auto-Aktions-Vorschlag","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/postfach/{id}/detected-appointment":{"get":{"responses":{"200":{"description":"Das Erkennungsergebnis. `datetime` und `location` stehen nur da, wenn wirklich etwas erkannt wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"detected":{"type":"boolean"},"datetime":{"type":"string","description":"ISO ohne Zeitzone (YYYY-MM-DDTHH:mm), Ortszeit — nur wenn erkannt"},"location":{"type":"string"},"confidence":{"type":"number","description":"0..1"},"reasoning":{"type":"string","description":"Warum der Text so gelesen wurde"}},"required":["detected","confidence","reasoning"]},"example":{"detected":true,"datetime":"string","location":"string","confidence":0,"reasoning":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Eintrag nicht gefunden"}},"operationId":"getApiV1ImmoPostfachByIdDetected-appointment","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest Betreff und Text des Eintrags und sagt, ob darin ein Terminwunsch steckt — rein lesend, ohne Kalender-Eintrag und ohne Aenderung am Eintrag. Die Erkennung laeuft ueber Schluesselwoerter und Datumsmuster, nicht ueber ein Modell, und wirft nie: findet sie nichts, kommt `detected: false` mit einer Begruendung. Gedacht als Vorschau vor `POST /:id/create-appointment`. Unbekannter Eintrag: 404.","summary":"Liest Betreff und Text des Eintrags und sagt, ob darin ein Terminwunsch steckt","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/postfach/{id}/create-appointment":{"post":{"responses":{"200":{"description":"Kalender-Eintrag angelegt. `calendar_event_id` ist null, wenn das Einfuegen keine Id zurueckgab — dann wurde auch nichts verknuepft.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"calendar_event_id":{"type":["string","null"],"description":"Id des angelegten Kalender-Eintrags"},"start":{"type":"string","description":"Beginn als ISO-Zeichenkette (UTC)"},"end":{"type":"string","description":"Ende = Beginn plus `duration_minutes`"}},"required":["ok","calendar_event_id","start","end"]},"example":{"ok":true,"calendar_event_id":"string","start":"string","end":"string"}}}},"400":{"description":"Zeitpunkt nicht lesbar"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Eintrag nicht gefunden"},"422":{"description":"Kein Zeitpunkt erkannt und keiner vorgegeben"}},"operationId":"postApiV1ImmoPostfachByIdCreate-appointment","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Kalender-Eintrag aus dem erkannten Terminwunsch anlegen","description":"Legt aus dem erkannten Terminwunsch einen Kalender-Eintrag an und verknuepft ihn mit dem Postfach-Eintrag. Zeitpunkt und Ort kommen aus der Erkennung, `override_datetime` und `override_location` stechen sie aus. Erkennt der Text keinen Zeitpunkt und liegt auch keine Vorgabe bei, kommt 422; ein unlesbarer Zeitpunkt ergibt 400. Das Ende ergibt sich aus `duration_minutes` (15..480, Vorgabe 60). Der Titel ist der Betreff, ersatzweise „Termin mit <Absender>\". Der Aufruf ist NICHT wiederholungssicher: ein zweiter Aufruf legt einen zweiten Kalender-Eintrag an und ueberschreibt die Verknuepfung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"override_datetime":{"type":"string","minLength":1},"override_location":{"type":"string","maxLength":200},"duration_minutes":{"type":"integer","minimum":15,"maximum":480,"default":60}}},"example":{"override_datetime":"string","override_location":"string","duration_minutes":15}}}}}},"/api/v1/immo/postfach-config":{"get":{"responses":{"200":{"description":"Die hinterlegte Verbindung, oder der Vorgabestand bei `configured: false`.","content":{"application/json":{"schema":{"type":"object","properties":{"configured":{"type":"boolean","description":"false = nichts hinterlegt; die uebrigen Felder tragen dann Vorgabewerte"},"host":{"type":"string","description":"Leer, solange nichts hinterlegt ist"},"port":{"type":"integer","description":"993, solange nichts hinterlegt ist"},"userEmail":{"type":"string"},"useTls":{"type":"boolean"},"status":{"type":"string","description":"inactive | active | error — `active` erst nach einem gelungenen Test"},"lastUid":{"type":["integer","null"],"description":"Zuletzt abgeholte IMAP-Nummer"},"lastPolledAt":{"type":["string","null"]},"lastError":{"type":["string","null"],"description":"Der Grund des letzten fehlgeschlagenen Tests"},"imapModuleAvailable":{"type":["boolean","null"],"description":"null, solange nie geprueft wurde, ob die IMAP-Bibliothek im Abbild liegt"}},"required":["configured","host","port","userEmail","useTls","status","lastUid","lastPolledAt","lastError","imapModuleAvailable"]},"example":{"configured":true,"host":"string","port":0,"userEmail":"string","useTls":true,"status":"string","lastUid":0,"lastPolledAt":"string","lastError":"string","imapModuleAvailable":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1ImmoPostfach-config","tags":["immo"],"parameters":[],"description":"Liest die eine IMAP-Verbindung des Mandanten samt Abrufstand: `lastUid` und `lastPolledAt` sagen, wie weit der Abruf ist, `status` und `lastError`, wie der letzte Verbindungstest ausging. Das Passwort kommt NIE zurueck — die Abfrage waehlt die Spalte nicht aus. Ist nichts hinterlegt, antwortet der Aufruf trotzdem 200: dann steht `configured: false` und die Felder tragen Vorgabewerte (Port 993, TLS an, Status `inactive`).","summary":"Liest die eine IMAP-Verbindung des Mandanten samt Abrufstand","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"200":{"description":"Gespeichert. Die Antwort traegt NUR die Bestaetigung — weder die gespeicherten Werte noch eine Aussage darueber, ob die Verbindung funktioniert. Der Status bleibt `inactive`, bis `POST /test` ihn setzt.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar, oder die Feldverschluesselung ist nicht eingerichtet (`FIELD_ENCRYPTION_MASTER_KEY` fehlt) — dann wird NICHTS gespeichert."}},"operationId":"postApiV1ImmoPostfach-config","tags":["immo"],"parameters":[],"description":"Speichert IMAP-Konfiguration. Passwort wird verschlüsselt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"host":{"type":"string","minLength":1,"maxLength":255},"port":{"type":"integer","minimum":1,"maximum":65535,"default":993},"userEmail":{"type":"string","format":"email","maxLength":255},"password":{"type":"string","minLength":1,"maxLength":512},"useTls":{"type":"boolean","default":true}},"required":["host","userEmail","password"]},"example":{"host":"string","port":1,"userEmail":"beispiel@example.com","password":"string","useTls":true}}}},"summary":"Speichert IMAP-Konfiguration","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Geloescht — die Antwort traegt nur die Bestaetigung.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1ImmoPostfach-config","tags":["immo"],"parameters":[],"description":"Loescht die IMAP-Verbindung des Mandanten ENDGUELTIG — samt verschluesseltem Passwort und samt Abrufstand (`lastUid`). Wer sie danach neu einrichtet, holt unter Umstaenden bereits verarbeitete Mails erneut. Bereits eingegangene Postfach-Eintraege bleiben bestehen. Der Aufruf ist wiederholbar und antwortet 200, auch wenn es gar nichts zu loeschen gab.","summary":"Loescht die IMAP-Verbindung des Mandanten ENDGUELTIG","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/postfach-config/test":{"post":{"responses":{"200":{"description":"Anmeldung gelungen — `success: true`, `error: null`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":["string","null"],"description":"null bei Erfolg. Sonst `not_configured`, `field_encryption_unavailable`, `decrypt_failed: …`, `imap_library_unavailable`, `imap_library_invalid`, `imap_timeout` oder die Meldung des Mailservers."}},"required":["success","error"]},"example":{"success":true,"error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Nichts hinterlegt, Passwort nicht entschluesselbar, oder die Anmeldung schlug fehl. `error` nennt den Grund.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":["string","null"],"description":"null bei Erfolg. Sonst `not_configured`, `field_encryption_unavailable`, `decrypt_failed: …`, `imap_library_unavailable`, `imap_library_invalid`, `imap_timeout` oder die Meldung des Mailservers."}},"required":["success","error"]}}}},"503":{"description":"Datenbank nicht erreichbar oder Feldverschluesselung nicht eingerichtet."}},"operationId":"postApiV1ImmoPostfach-configTest","tags":["immo"],"parameters":[],"summary":"Gespeicherte IMAP-Verbindung anmelden und Ergebnis festhalten","description":"Versucht sich mit der GESPEICHERTEN Verbindung am Mailserver anzumelden und trennt sofort wieder — es werden keine Mails abgeholt und `lastUid` bleibt unberuehrt. Der Aufruf nimmt keinen Rumpf entgegen: geprueft wird, was hinterlegt ist. Nach spaetestens 10 Sekunden bricht er mit `imap_timeout` ab. Nebenwirkung: das Ergebnis wird an der Verbindung fortgeschrieben — `status` auf `active` oder `error`, `lastError` auf den Grund. Ein Misserfolg kommt als 422 bzw. 503, nicht als 200 mit `success: false`."}},"/api/v1/immo/dashboard/overview":{"get":{"responses":{"200":{"description":"Übersichts-KPIs","content":{"application/json":{"schema":{"type":"object","properties":{"objekte":{"type":"number"},"wohneinheiten":{"type":"number"},"gewerbe":{"type":"number"},"stellplaetze":{"type":"number"}},"required":["objekte","wohneinheiten","gewerbe","stellplaetze"],"additionalProperties":false},"example":{"objekte":0,"wohneinheiten":0,"gewerbe":0,"stellplaetze":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoDashboardOverview","tags":["immo"],"parameters":[],"description":"Portfolio-Übersicht: Anzahl Objekte + Einheitenarten. Gezählt werden die nicht gelöschten Immobilien des Mandanten sowie ihre Einheiten, aufgeteilt nach dem Typ der ZUGEHÖRIGEN Immobilie: `wohneinheiten` steht für wohnung/haus/wohnung_anteil, `gewerbe` für gewerbe, `stellplaetze` für grundstueck. Beide Zählungen laufen einzeln abgesichert — schlägt eine fehl, kommt für sie 0 und der Rest trotzdem. Eine 0 kann hier also auch „nicht lesbar\" heißen.","summary":"Portfolio-Übersicht: Anzahl Objekte + Einheitenarten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/dashboard/cashflow":{"get":{"responses":{"200":{"description":"Eine Zeile je Monat, aufsteigend — die Reihe ist lückenlos, Monate ohne Bewegung kommen mit 0. Scheitert die Abfrage, kommt ebenfalls 200 mit einer LEEREN Liste.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"month":{"type":"string"},"income":{"type":"number"},"expenses":{"type":"number"}},"required":["month","income","expenses"],"additionalProperties":false}},"example":[{"month":"string","income":0,"expenses":0}]}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoDashboardCashflow","tags":["immo"],"parameters":[],"summary":"Monatlicher Cashflow (Mieteinnahmen vs. Bewirtschaftungskosten)","description":"Stellt je Monat die Mieteinnahmen den Bewirtschaftungskosten gegenüber, über alle Immobilien des Mandanten hinweg. `income` ist die Summe von `mietzahlungen.ist`, eingeordnet nach `eingang_datum` und ersatzweise nach `monat` — also nach dem tatsächlichen Zahlungseingang, nicht nach der Sollstellung. `expenses` ist die Summe von `immobilien_kosten.betrag` nach `datum`; dort zählen gelöschte Zeilen (`deleted_at`) nicht mit. Beide Reihen werden auf eine lückenlose Monatsreihe gelegt, sodass Monate ohne Bewegung mit 0 erscheinen; `month` ist der Monatsanfang als ISO-Zeitpunkt, sortiert aufsteigend.\n\nWie weit zurück gerechnet wird, steuert `months` einschließlich des laufenden Monats: ohne Angabe 12, erlaubt sind 1 bis 60, ein Wert darüber oder darunter wird auf die Grenze gezogen und ein unlesbarer Wert auf 12. Es wird nur gelesen; der Aufruf schreibt und ändert nichts.\n\nACHTUNG bei der leeren Liste: scheitert die Abfrage — etwa weil `mietzahlungen` oder `immobilien_kosten` im Mandanten-Schema fehlen —, antwortet die Route trotzdem mit 200 und einer leeren Liste statt mit einem Fehler. Eine leere Antwort heißt also entweder „keine Bewegungen\" oder „nicht lesbar\", und beides ist von außen nicht zu unterscheiden. Nur eine fehlende Datenbankverbindung kommt als 503."}},"/api/v1/immo/dashboard/mietzahlungen-status":{"get":{"responses":{"200":{"description":"Zahlungsstatus. Der dritte Schlüssel heißt `überfällig` — mit Umlauten, so wie er geschrieben wird.","content":{"application/json":{"schema":{"type":"object","properties":{"bezahlt":{"type":"number"},"offen":{"type":"number"},"überfällig":{"type":"number"}},"required":["bezahlt","offen","überfällig"],"additionalProperties":false},"example":{"bezahlt":0,"offen":0,"überfällig":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoDashboardMietzahlungen-status","tags":["immo"],"parameters":[],"description":"Counts der Mietzahlungen nach Status. Eine Status-Spalte gibt es in `mietzahlungen` nicht — er wird beim Zählen abgeleitet: `bezahlt` heißt ist >= soll, `offen` heißt ist < soll und der Monat liegt im laufenden Monat oder später, `überfällig` heißt ist < soll und der Monat liegt davor. Gezählt wird über ALLE Zahlungen des Mandanten, ohne Zeitfenster. Scheitert die Abfrage, kommt 200 mit drei Nullen.","summary":"Counts der Mietzahlungen nach Status","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/dashboard/freie-flaechen":{"get":{"responses":{"200":{"description":"Liste freier Einheiten. `property_address` ist ein JSON-Objekt aus der Spalte `adresse`, keine Zeichenkette. Scheitert die Abfrage, kommt 200 mit einer LEEREN Liste.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"bezeichnung":{"type":"string"},"area_m2":{"type":"number"},"etage":{"type":["string","null"]},"property_name":{"type":["string","null"]},"property_address":{},"created_at":{}},"required":["id","bezeichnung","area_m2","etage","property_name"],"additionalProperties":false}},"example":[{"id":"string","bezeichnung":"string","area_m2":0,"etage":"string","property_name":"string"}]}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoDashboardFreie-flaechen","tags":["immo"],"parameters":[],"description":"Verfügbare Einheiten (status=frei). Gelesen wird `immobilien_einheiten` mit `status = frei` und ohne `deleted_at`, verbunden mit der Immobilie für Name und Adresse; neueste zuerst. `limit` liegt zwischen 1 und 100 (Vorgabe 10), eine Blätterung oder Gesamtzahl gibt es nicht. Eine Einheit ohne zugeordnete Immobilie bleibt in der Liste — `property_name` und `property_address` sind dann null.","summary":"Verfügbare Einheiten (status=frei)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/dashboard/reparaturen-offen":{"get":{"responses":{"200":{"description":"Offene Tickets — die Datenbankzeilen unverändert, mit genau den Spalten des SELECT. Da `service_tickets` nirgends im Repo angelegt wird, sind die Werttypen nicht zugesichert.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{},"title":{},"priority":{},"status":{},"created_at":{},"property_name":{},"property_address":{}}}},"example":[{}]}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoDashboardReparaturen-offen","tags":["immo"],"parameters":[],"description":"Offene Reparatur-Tickets (Service-Tickets mit entity_type=property). Gelesen wird `service_tickets` mit Status `open` oder `in_progress`, sortiert nach Dringlichkeit (urgent vor high vor normal vor low) und dann nach Anlagedatum, neueste zuerst. `limit` liegt zwischen 1 und 100 (Vorgabe 10). Die Tabelle wird nirgends automatisch angelegt: fehlt sie im Mandanten-Schema, antwortet die Route 200 mit einer leeren Liste — leer heißt hier also nicht zwingend „keine offenen Reparaturen\".","summary":"Offene Reparatur-Tickets (Service-Tickets mit entity_type=property)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/dashboard/rendite-kpis":{"get":{"responses":{"200":{"description":"Rendite-Liste, ungekappt. `adresse` ist ein JSON-Objekt, keine Zeichenkette. Scheitert die Abfrage, kommt 200 mit einer LEEREN Liste.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"adresse":{},"kaufpreis":{"type":"number"},"yearly_income":{"type":"number"},"yearly_expenses":{"type":"number"},"brutto_rendite_pct":{"type":["number","null"]},"netto_rendite_pct":{"type":["number","null"]}},"required":["id","name","kaufpreis","yearly_income","yearly_expenses","brutto_rendite_pct","netto_rendite_pct"],"additionalProperties":false}},"example":[{"id":"string","name":"string","kaufpreis":0,"yearly_income":0,"yearly_expenses":0,"brutto_rendite_pct":0,"netto_rendite_pct":0}]}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoDashboardRendite-kpis","tags":["immo"],"parameters":[],"description":"Brutto/Netto-Rendite pro Immobilie (12-Monats-Fenster). Einnahmen sind die Ist-Beträge der Mietzahlungen der letzten zwölf Monate, zugeordnet über Mietvertrag und Einheit; Kosten sind die nicht gelöschten `immobilien_kosten` desselben Zeitraums. Brutto = Einnahmen / Kaufpreis, Netto = (Einnahmen − Kosten) / Kaufpreis, jeweils in Prozent. Ohne hinterlegten Kaufpreis stehen beide Renditen auf `null` — nicht auf 0. Aufgeführt sind ALLE nicht gelöschten Immobilien, auch die ohne Zahlungen, sortiert nach Netto-Ertrag je Kaufpreis absteigend.","summary":"Brutto/Netto-Rendite pro Immobilie (12-Monats-Fenster)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/dashboard/mahnungen":{"get":{"responses":{"200":{"description":"Mahnungs-Liste. `property_address` ist ein JSON-Objekt aus der Spalte `adresse`. Scheitert die Abfrage, kommt 200 mit einer LEEREN Liste.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"mieter_name":{"type":["string","null"]},"mieter_email":{"type":["string","null"]},"offener_betrag":{"type":"number"},"due_date":{},"days_overdue":{"type":"number"},"mahnstufe":{"type":"number"},"property_name":{"type":["string","null"]},"property_address":{}},"required":["id","mieter_name","mieter_email","offener_betrag","days_overdue","mahnstufe","property_name"],"additionalProperties":false}},"example":[{"id":"string","mieter_name":"string","mieter_email":"string","offener_betrag":0,"days_overdue":0,"mahnstufe":0,"property_name":"string"}]}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoDashboardMahnungen","tags":["immo"],"parameters":[],"description":"Offene überfällige Mietzahlungen mit Tagen Verzug. Erfasst sind Zahlungen, deren Ist unter dem Soll liegt UND deren Monat vor dem laufenden Monat liegt; `offener_betrag` ist die Differenz Soll minus Ist, `days_overdue` der Abstand zum Monatsdatum in Tagen. Sortiert nach Monat aufsteigend, die ältesten Rückstände zuerst; `limit` liegt zwischen 1 und 100 (Vorgabe 10). Der Aufruf liest nur — er verschickt keine Mahnung und erhöht `mahnstufe` nicht.","summary":"Offene überfällige Mietzahlungen mit Tagen Verzug","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/dashboard/upcoming-appointments":{"get":{"responses":{"200":{"description":"Termin-Liste — die Datenbankzeilen unverändert, mit genau den Spalten des SELECT. Da `immo_appointments` nirgends im Repo angelegt wird, sind die Werttypen nicht zugesichert.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{},"title":{},"scheduled_at":{},"type":{},"customer_name":{},"property_address":{}}}},"example":[{}]}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoDashboardUpcoming-appointments","tags":["immo"],"parameters":[],"description":"Anstehende Immo-Termine (nächste N Tage). Gelesen wird `immo_appointments` im Fenster von jetzt bis `days` Tage voraus, aufsteigend nach Termin. `days` liegt zwischen 1 und 365 (Vorgabe 30); die Zahl der Zeilen ist zusätzlich fest auf 50 begrenzt — bei einem weiten Fenster kann die Liste also abgeschnitten sein, ohne dass es die Antwort sagt. Die Tabelle wird nirgends automatisch angelegt: fehlt sie, kommt 200 mit einer leeren Liste.","summary":"Anstehende Immo-Termine (nächste N Tage)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/eigentuemer":{"get":{"responses":{"200":{"description":"Eigentuemer der aktuellen Seite samt Blaetter-Angaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":"object","additionalProperties":{}},"vat_id":{"type":["string","null"]},"bank_account_iban":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","type","email","phone","address","vat_id","bank_account_iban","notes","created_at","updated_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"page":{"type":"number"},"limit":{"type":"number"},"total":{"type":"number"},"pages":{"type":"number"}},"required":["page","limit","total","pages"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","type":"string","email":"string","phone":"string","address":{},"vat_id":"string","bank_account_iban":"string","notes":"string","created_at":"string","updated_at":"string"}],"meta":{"page":0,"limit":0,"total":0,"pages":0}}}}},"400":{"description":"Ungueltige Query-Parameter"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1ImmoEigentuemer","tags":["immo"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"type","schema":{"type":"string","enum":["person","firma","gesellschaft"]}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"Liste der Immobilien-Eigentuemer mit Pagination + Suche","description":"Liest `immo_eigentuemer` ohne gesetztes `deleted_at`, nach Name sortiert. Filtert wahlweise nach `type` und sucht mit `search` in Name, E-Mail und USt-IdNr. (Teiltreffer, Groß-/Kleinschreibung egal). Geblaettert wird ueber `page` (ab 1) und `limit` (1-200, Standard 50); `meta` nennt zusaetzlich die Gesamtzahl und die Anzahl Seiten. Die IBAN bekommt nur ein Manager entschluesselt zu sehen — fuer alle anderen steht dort null, ohne dass die Entschluesselung ueberhaupt versucht wird."},"post":{"responses":{"201":{"description":"Der angelegte Eigentuemer, ohne Umschlag","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":"object","additionalProperties":{}},"vat_id":{"type":["string","null"]},"bank_account_iban":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","type","email","phone","address","vat_id","bank_account_iban","notes","created_at","updated_at"],"additionalProperties":false},"example":{"id":"string","name":"string","type":"string","email":"string","phone":"string","address":{},"vat_id":"string","bank_account_iban":"string","notes":"string","created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Validierung"},"503":{"description":"Datenbank oder Feldverschluesselung nicht verfuegbar"}},"operationId":"postApiV1ImmoEigentuemer","tags":["immo"],"parameters":[],"summary":"Neuen Eigentuemer anlegen (IBAN wird verschluesselt gespeichert)","description":"Legt einen Eigentuemer an (201). Eine mitgeschickte IBAN wird vor dem Speichern mandantengebunden verschluesselt; steht kein Schluesseldienst bereit, endet der Aufruf mit 503 und es wird NICHTS gespeichert. `type` steht ohne Angabe auf `person`, `address` wird als JSONB abgelegt. Die Antwort ist der neue Datensatz ohne Umschlag und traegt die IBAN entschluesselt — hier unabhaengig von der Rolle des Anlegers.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"type":{"type":"string","enum":["person","firma","gesellschaft"],"default":"person"},"email":{"type":["string","null"],"format":"email","maxLength":254},"phone":{"type":["string","null"],"maxLength":64},"address":{"type":"object","properties":{"strasse":{"type":"string","maxLength":255},"hausnummer":{"type":"string","maxLength":20},"plz":{"type":"string","maxLength":10},"ort":{"type":"string","maxLength":120},"land":{"type":"string","maxLength":60}}},"vat_id":{"type":["string","null"],"maxLength":64},"bank_account_iban":{"type":["string","null"],"maxLength":64},"notes":{"type":["string","null"],"maxLength":4000}},"required":["name"]},"example":{"name":"string","type":"person","email":"beispiel@example.com","phone":"string","address":{"strasse":"string","hausnummer":"string","plz":"string","ort":"string","land":"string"},"vat_id":"string","bank_account_iban":"string","notes":"string"}}}}}},"/api/v1/immo/eigentuemer/{id}":{"get":{"responses":{"200":{"description":"Der Eigentuemer, ohne Umschlag","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":"object","additionalProperties":{}},"vat_id":{"type":["string","null"]},"bank_account_iban":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","type","email","phone","address","vat_id","bank_account_iban","notes","created_at","updated_at"],"additionalProperties":false},"example":{"id":"string","name":"string","type":"string","email":"string","phone":"string","address":{},"vat_id":"string","bank_account_iban":"string","notes":"string","created_at":"string","updated_at":"string"}}}},"400":{"description":"`invalid_id` — die id ist keine UUID"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1ImmoEigentuemerById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigentuemer-Detail (IBAN entschluesselt)","description":"Liest genau einen Eigentuemer; die Antwort ist der Datensatz SELBST, ohne Umschlag. Eine id, die keine UUID ist, ergibt 400 `invalid_id`; eine unbekannte oder geloeschte 404. Die IBAN wird nur fuer einen Manager entschluesselt. Auch dort kann null stehen, naemlich wenn kein Schluesseldienst bereitsteht oder die Entschluesselung scheitert."},"put":{"responses":{"200":{"description":"Der aktualisierte Eigentuemer — ODER `{ ok: true, noop: true }`, wenn der Rumpf kein bekanntes Feld enthielt.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":"object","additionalProperties":{}},"vat_id":{"type":["string","null"]},"bank_account_iban":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","type","email","phone","address","vat_id","bank_account_iban","notes","created_at","updated_at"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok","noop"],"additionalProperties":false}]},"example":{"id":"string","name":"string","type":"string","email":"string","phone":"string","address":{},"vat_id":"string","bank_account_iban":"string","notes":"string","created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"nicht gefunden"},"503":{"description":"Datenbank oder Feldverschluesselung nicht verfuegbar"}},"operationId":"putApiV1ImmoEigentuemerById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigentuemer aktualisieren","description":"Teil-Update: geschrieben wird nur, was im Rumpf steht. Enthaelt der Rumpf KEIN bekanntes Feld, wird gar nichts geschrieben und die Antwort ist `{ ok: true, noop: true }` statt des Datensatzes — der 200-Fall hat also zwei verschiedene Formen. Eine mitgeschickte IBAN wird neu verschluesselt, ein ausdrueckliches `null` loescht sie. Ein unbekannter oder geloeschter Eigentuemer ergibt 404. Die Antwort traegt die IBAN entschluesselt, unabhaengig von der Rolle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"type":{"type":"string","enum":["person","firma","gesellschaft"],"default":"person"},"email":{"type":["string","null"],"format":"email","maxLength":254},"phone":{"type":["string","null"],"maxLength":64},"address":{"type":"object","properties":{"strasse":{"type":"string","maxLength":255},"hausnummer":{"type":"string","maxLength":20},"plz":{"type":"string","maxLength":10},"ort":{"type":"string","maxLength":120},"land":{"type":"string","maxLength":60}}},"vat_id":{"type":["string","null"],"maxLength":64},"bank_account_iban":{"type":["string","null"],"maxLength":64},"notes":{"type":["string","null"],"maxLength":4000}}},"example":{"name":"string","type":"person","email":"beispiel@example.com","phone":"string","address":{"strasse":"string","hausnummer":"string","plz":"string","ort":"string","land":"string"},"vat_id":"string","bank_account_iban":"string","notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung — sagt NICHT, ob es den Eigentuemer gab","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1ImmoEigentuemerById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigentuemer loeschen (soft, deleted_at=NOW())","description":"Soft-Delete: setzt `deleted_at`, die Zeile bleibt in `immo_eigentuemer` stehen. Der Handler prueft NICHT, ob eine Zeile getroffen wurde — eine unbekannte oder laengst geloeschte id wird ebenso mit `{ ok: true }` quittiert. Anders als beim Lesen wird die id hier nicht auf UUID-Form geprueft. Die Eigentumsverhaeltnisse zu Immobilien bleiben unveraendert stehen."}},"/api/v1/immo/eigentuemer/{id}/properties":{"get":{"responses":{"200":{"description":"Die aktuell zugeordneten Immobilien samt Anteil und Zeitraum","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{},"typ":{},"adresse":{},"kaufpreis":{},"aktueller_wert":{},"anteil_pct":{},"von_datum":{},"bis_datum":{}},"required":["id"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string"}]}}}},"400":{"description":"`invalid_id` — die id ist keine UUID"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1ImmoEigentuemerByIdProperties","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Liste der Immobilien dieses Eigentuemers (aktuelle Verhaeltnisse)","description":"Verbindet `immobilien` mit `immobilien_eigentumsverhaeltnisse` und liefert nur die AKTUELLEN Verhaeltnisse: solche ohne `bis_datum` oder mit einem `bis_datum` ab heute. Beendete Beteiligungen fehlen also. Geloeschte Immobilien bleiben aussen vor, sortiert wird nach Name. Es gibt weder Blaetterung noch Filter. Eine id, die keine UUID ist, ergibt 400 `invalid_id`."}},"/api/v1/immo/maklerprovisionen":{"get":{"responses":{"200":{"description":"Seite der Maklerprovisionen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"makler_id":{"type":["string","null"]},"property_id":{"type":["string","null"]},"type":{"type":"string","enum":["kauf","miete","verwaltung"]},"basis_betrag":{"type":"number"},"prozent":{"type":"number"},"betrag":{"type":"number"},"status":{"type":"string","enum":["offen","faellig","bezahlt","storniert"]},"faellig_am":{"type":["string","null"]},"bezahlt_am":{"type":["string","null"]},"bemerkung":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","makler_id","property_id","type","basis_betrag","prozent","betrag","status","faellig_am","bezahlt_am","bemerkung","created_at","updated_at"]}},"meta":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"pages":{"type":"integer"}},"required":["page","limit","total","pages"]}},"required":["data","meta"]},"example":{"data":[{"id":"string","makler_id":"string","property_id":"string","type":"kauf","basis_betrag":0,"prozent":0,"betrag":0,"status":"offen","faellig_am":"string","bezahlt_am":"string","bemerkung":"string","created_at":"string","updated_at":"string"}],"meta":{"page":0,"limit":0,"total":0,"pages":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1ImmoMaklerprovisionen","tags":["immo"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"makler_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"property_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"status","schema":{"type":"string","enum":["offen","faellig","bezahlt","storniert"]}},{"in":"query","name":"type","schema":{"type":"string","enum":["kauf","miete","verwaltung"]}}],"summary":"Liste der Maklerprovisionen mit Filtern + Pagination","description":"Blaettert ueber `page` (ab 1) und `limit` (1…200, Vorgabe 50) — nicht ueber offset. Sortiert nach Faelligkeitsdatum absteigend; fehlt eines, tritt das Anlagedatum an seine Stelle. Filter fuer `makler_id`, `property_id`, `status` und `type` wirken zusammen (UND). Soft-geloeschte Provisionen erscheinen nie. `meta.total` zaehlt die Treffer NACH den Filtern, `meta.pages` die daraus folgende Seitenzahl. Fehlt die Tabelle im Mandanten-Schema, wird sie vor der Abfrage angelegt."},"post":{"responses":{"201":{"description":"angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"makler_id":{"type":["string","null"]},"property_id":{"type":["string","null"]},"type":{"type":"string","enum":["kauf","miete","verwaltung"]},"basis_betrag":{"type":"number"},"prozent":{"type":"number"},"betrag":{"type":"number"},"status":{"type":"string","enum":["offen","faellig","bezahlt","storniert"]},"faellig_am":{"type":["string","null"]},"bezahlt_am":{"type":["string","null"]},"bemerkung":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","makler_id","property_id","type","basis_betrag","prozent","betrag","status","faellig_am","bezahlt_am","bemerkung","created_at","updated_at"]},"example":{"id":"string","makler_id":"string","property_id":"string","type":"kauf","basis_betrag":0,"prozent":0,"betrag":0,"status":"offen","faellig_am":"string","bezahlt_am":"string","bemerkung":"string","created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Validierung"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1ImmoMaklerprovisionen","tags":["immo"],"parameters":[],"summary":"Neue Maklerprovision anlegen","description":"Ohne `betrag` rechnet der Server ihn aus `basis_betrag` × `prozent` / 100 und rundet auf Cent; ein mitgesendeter `betrag` wird dagegen unveraendert uebernommen, auch wenn er nicht zur Grundlage passt. `makler_id` und `property_id` sind Pflicht und muessen UUIDs sein, werden aber NICHT gegen Kontakte oder Immobilien geprueft — eine Provision auf eine unbekannte Kennung wird angelegt. Vorgaben: type=miete, status=offen. Es entsteht keine Buchung und keine Zahlung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"makler_id":{"type":"string","format":"uuid"},"property_id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["kauf","miete","verwaltung"],"default":"miete"},"basis_betrag":{"type":"number","minimum":0,"default":0},"prozent":{"type":"number","minimum":0,"maximum":100,"default":0},"betrag":{"type":"number","minimum":0},"status":{"type":"string","enum":["offen","faellig","bezahlt","storniert"],"default":"offen"},"faellig_am":{"type":["string","null"]},"bezahlt_am":{"type":["string","null"]},"bemerkung":{"type":["string","null"],"maxLength":2000}},"required":["makler_id","property_id"]},"example":{"makler_id":"00000000-0000-4000-8000-000000000000","property_id":"00000000-0000-4000-8000-000000000000","type":"kauf","basis_betrag":0,"prozent":0,"betrag":0,"status":"offen","faellig_am":"string","bezahlt_am":"string","bemerkung":"string"}}}}}},"/api/v1/immo/maklerprovisionen/summary":{"get":{"responses":{"200":{"description":"Summen und Anzahlen je Status","content":{"application/json":{"schema":{"type":"object","properties":{"filter":{"type":"object","properties":{"makler_id":{"type":["string","null"]},"property_id":{"type":["string","null"]}},"required":["makler_id","property_id"]},"total":{"type":"object","properties":{"count":{"type":"integer"},"summe":{"type":"number"}},"required":["count","summe"]},"by_status":{"type":"object","properties":{"offen":{"type":"object","properties":{"count":{"type":"integer"},"summe":{"type":"number"}},"required":["count","summe"]},"faellig":{"type":"object","properties":{"count":{"type":"integer"},"summe":{"type":"number"}},"required":["count","summe"]},"bezahlt":{"type":"object","properties":{"count":{"type":"integer"},"summe":{"type":"number"}},"required":["count","summe"]},"storniert":{"type":"object","properties":{"count":{"type":"integer"},"summe":{"type":"number"}},"required":["count","summe"]}},"required":["offen","faellig","bezahlt","storniert"],"additionalProperties":{"type":"object","properties":{"count":{"type":"integer"},"summe":{"type":"number"}},"required":["count","summe"]}}},"required":["filter","total","by_status"]},"example":{"filter":{"makler_id":"string","property_id":"string"},"total":{"count":0,"summe":0},"by_status":{"offen":{"count":0,"summe":0},"faellig":{"count":0,"summe":0},"bezahlt":{"count":0,"summe":0},"storniert":{"count":0,"summe":0}}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1ImmoMaklerprovisionenSummary","tags":["immo"],"parameters":[{"in":"query","name":"makler_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"property_id","schema":{"type":"string","format":"uuid"}}],"summary":"Aggregierte Provisionen (Summe + Count je Status), optional gefiltert","description":"Zaehlt und summiert `betrag` ueber alle nicht geloeschten Provisionen, einmal insgesamt und einmal je Status. Alle vier Stati sind immer enthalten, ohne Treffer mit Null — eine 0 heiszt also „nichts in diesem Status\", nicht „nicht erhoben\". Stornierte Provisionen werden NICHT abgezogen, sie stehen als eigener Posten daneben. Filtern laesst sich nur nach `makler_id` und `property_id`; die angewandten Filter stehen in der Antwort. Ohne Filter umfasst die Auswertung den ganzen Mandanten."}},"/api/v1/immo/maklerprovisionen/{id}":{"get":{"responses":{"200":{"description":"Die Maklerprovision","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"makler_id":{"type":["string","null"]},"property_id":{"type":["string","null"]},"type":{"type":"string","enum":["kauf","miete","verwaltung"]},"basis_betrag":{"type":"number"},"prozent":{"type":"number"},"betrag":{"type":"number"},"status":{"type":"string","enum":["offen","faellig","bezahlt","storniert"]},"faellig_am":{"type":["string","null"]},"bezahlt_am":{"type":["string","null"]},"bemerkung":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","makler_id","property_id","type","basis_betrag","prozent","betrag","status","faellig_am","bezahlt_am","bemerkung","created_at","updated_at"]},"example":{"id":"string","makler_id":"string","property_id":"string","type":"kauf","basis_betrag":0,"prozent":0,"betrag":0,"status":"offen","faellig_am":"string","bezahlt_am":"string","bemerkung":"string","created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Provision nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1ImmoMaklerprovisionenById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Maklerprovision Detail","description":"Liest EINE Provision anhand ihrer UUID. Soft-geloeschte gelten als nicht vorhanden und ergeben 404. Die Antwort ist der Datensatz selbst, ohne Umschlag; Makler und Immobilie stehen nur als Kennung darin und werden nicht mitgeladen."},"put":{"responses":{"200":{"description":"OK — Datensatz nach der Aenderung, oder noop bei leerem Rumpf","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"makler_id":{"type":["string","null"]},"property_id":{"type":["string","null"]},"type":{"type":"string","enum":["kauf","miete","verwaltung"]},"basis_betrag":{"type":"number"},"prozent":{"type":"number"},"betrag":{"type":"number"},"status":{"type":"string","enum":["offen","faellig","bezahlt","storniert"]},"faellig_am":{"type":["string","null"]},"bezahlt_am":{"type":["string","null"]},"bemerkung":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","makler_id","property_id","type","basis_betrag","prozent","betrag","status","faellig_am","bezahlt_am","bemerkung","created_at","updated_at"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok","noop"]}]},"example":{"id":"string","makler_id":"string","property_id":"string","type":"kauf","basis_betrag":0,"prozent":0,"betrag":0,"status":"offen","faellig_am":"string","bezahlt_am":"string","bemerkung":"string","created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1ImmoMaklerprovisionenById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Maklerprovision aktualisieren","description":"Teil-Update: geschrieben werden nur die gesendeten Felder, die uebrigen bleiben stehen. Anders als beim Anlegen wird `betrag` hier NICHT nachgerechnet — wer `basis_betrag` oder `prozent` aendert, muss den Betrag selbst mitschicken, sonst passen die drei Werte nicht mehr zusammen. Der Status laesst sich frei setzen, auch rueckwaerts; ein Zahlungsvorgang entsteht dabei nicht. Ein leerer Rumpf aendert nichts und antwortet 200 mit noop=true — ohne zu pruefen, ob es die Provision ueberhaupt gibt. Sonst ergibt eine unbekannte oder soft-geloeschte Kennung 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"makler_id":{"type":"string","format":"uuid"},"property_id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["kauf","miete","verwaltung"],"default":"miete"},"basis_betrag":{"type":"number","minimum":0,"default":0},"prozent":{"type":"number","minimum":0,"maximum":100,"default":0},"betrag":{"type":"number","minimum":0},"status":{"type":"string","enum":["offen","faellig","bezahlt","storniert"],"default":"offen"},"faellig_am":{"type":["string","null"]},"bezahlt_am":{"type":["string","null"]},"bemerkung":{"type":["string","null"],"maxLength":2000}}},"example":{"makler_id":"00000000-0000-4000-8000-000000000000","property_id":"00000000-0000-4000-8000-000000000000","type":"kauf","basis_betrag":0,"prozent":0,"betrag":0,"status":"offen","faellig_am":"string","bezahlt_am":"string","bemerkung":"string"}}}}},"delete":{"responses":{"200":{"description":"Aufruf angenommen — es kann auch nichts getroffen worden sein","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1ImmoMaklerprovisionenById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Maklerprovision loeschen (soft)","description":"Setzt `deleted_at` — die Zeile bleibt bestehen und verschwindet nur aus Liste, Detail und Auswertung. Ueber diese Schnittstelle gibt es kein Zurueckholen. Eine unbekannte oder bereits geloeschte Kennung ist KEIN Fehler: die Antwort ist immer 200 mit ok=true, sie beweist also nicht, dass etwas geloescht wurde. Wer eine Provision fachlich zuruecknehmen will, setzt besser den Status auf „storniert\" — nur so bleibt sie in der Auswertung sichtbar."}},"/api/v1/immo/energieausweise/expiring":{"get":{"responses":{"200":{"description":"Ablauf-Liste. `data` traegt WENIGER Felder als die uebrigen Routen — `notes` und die Zeitstempel fehlen, dafuer kommen `tage_bis_ablauf` und `property_name` dazu.","content":{"application/json":{"schema":{"type":"object","properties":{"horizon_days":{"type":"number"},"count":{"type":"number"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"property_id":{"type":"string"},"typ":{"type":"string"},"ausgestellt_am":{},"gueltig_bis":{},"energieeffizienzklasse":{"type":["string","null"]},"endenergiebedarf_kwh_qm_a":{"type":["number","null"]},"primaerenergiebedarf_kwh_qm_a":{"type":["number","null"]},"co2_emission_kg_qm_a":{"type":["number","null"]},"heizungsart":{"type":["string","null"]},"baujahr":{"type":["number","null"]},"aussteller_name":{"type":["string","null"]},"pdf_s3_key":{"type":["string","null"]},"tage_bis_ablauf":{"type":"number"},"property_name":{"type":["string","null"]}},"required":["id","property_id","typ","energieeffizienzklasse","endenergiebedarf_kwh_qm_a","primaerenergiebedarf_kwh_qm_a","co2_emission_kg_qm_a","heizungsart","baujahr","aussteller_name","pdf_s3_key","tage_bis_ablauf","property_name"],"additionalProperties":false}}},"required":["horizon_days","count","data"],"additionalProperties":false},"example":{"horizon_days":0,"count":0,"data":[{"id":"string","property_id":"string","typ":"string","energieeffizienzklasse":"string","endenergiebedarf_kwh_qm_a":0,"primaerenergiebedarf_kwh_qm_a":0,"co2_emission_kg_qm_a":0,"heizungsart":"string","baujahr":0,"aussteller_name":"string","pdf_s3_key":"string","tage_bis_ablauf":0,"property_name":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoEnergieausweiseExpiring","tags":["immo"],"parameters":[{"in":"query","name":"days","schema":{"type":"integer","minimum":1,"maximum":3650,"default":90}}],"description":"Bald ablaufende Energieausweise (default: 90 Tage Vorlauf). `days` spannt das Fenster auf (1–3650): geliefert wird alles, dessen `gueltig_bis` innerhalb dieser Frist liegt — einschliesslich der BEREITS ABGELAUFENEN, deren `tage_bis_ablauf` dann negativ ist. Sortiert nach Ablaufdatum aufsteigend, ohne Blaetterung und ohne Obergrenze. Je Zeile kommt der Name der Immobilie dazu; die Route schickt keine Erinnerung, sie liest nur.","summary":"Bald ablaufende Energieausweise (default: 90 Tage Vorlauf)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/energieausweise":{"get":{"responses":{"200":{"description":"Eine Seite Energieausweise samt Seitenangaben.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"property_id":{"type":"string"},"typ":{"type":"string"},"ausgestellt_am":{},"gueltig_bis":{},"energieeffizienzklasse":{"type":["string","null"]},"endenergiebedarf_kwh_qm_a":{"type":["number","null"]},"primaerenergiebedarf_kwh_qm_a":{"type":["number","null"]},"co2_emission_kg_qm_a":{"type":["number","null"]},"heizungsart":{"type":["string","null"]},"baujahr":{"type":["number","null"]},"aussteller_name":{"type":["string","null"]},"pdf_s3_key":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_at":{},"updated_at":{}},"required":["id","property_id","typ","energieeffizienzklasse","endenergiebedarf_kwh_qm_a","primaerenergiebedarf_kwh_qm_a","co2_emission_kg_qm_a","heizungsart","baujahr","aussteller_name","pdf_s3_key","notes"],"additionalProperties":false}},"meta":{"type":"object","properties":{"page":{"type":"number"},"limit":{"type":"number"},"total":{"type":"number"},"pages":{"type":"number"}},"required":["page","limit","total","pages"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","property_id":"string","typ":"string","energieeffizienzklasse":"string","endenergiebedarf_kwh_qm_a":0,"primaerenergiebedarf_kwh_qm_a":0,"co2_emission_kg_qm_a":0,"heizungsart":"string","baujahr":0,"aussteller_name":"string","pdf_s3_key":"string","notes":"string"}],"meta":{"page":0,"limit":0,"total":0,"pages":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ImmoEnergieausweise","tags":["immo"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"property_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"typ","schema":{"type":"string","enum":["verbrauchsausweis","bedarfsausweis"]}}],"description":"Liste der Energieausweise mit Pagination. Gelesen wird `immo_energieausweise` des Mandanten ohne die weich geloeschten Zeilen, sortiert nach `gueltig_bis` absteigend — der am laengsten gueltige zuerst. `page` beginnt bei 1, `limit` liegt zwischen 1 und 200 (Vorgabe 50); optional laesst sich auf `property_id` und `typ` einschraenken. `meta.total` und `meta.pages` zaehlen mit denselben Bedingungen wie die Liste.","summary":"Liste der Energieausweise mit Pagination","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Der angelegte Energieausweis, vollstaendig.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"property_id":{"type":"string"},"typ":{"type":"string"},"ausgestellt_am":{},"gueltig_bis":{},"energieeffizienzklasse":{"type":["string","null"]},"endenergiebedarf_kwh_qm_a":{"type":["number","null"]},"primaerenergiebedarf_kwh_qm_a":{"type":["number","null"]},"co2_emission_kg_qm_a":{"type":["number","null"]},"heizungsart":{"type":["string","null"]},"baujahr":{"type":["number","null"]},"aussteller_name":{"type":["string","null"]},"pdf_s3_key":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_at":{},"updated_at":{}},"required":["id","property_id","typ","energieeffizienzklasse","endenergiebedarf_kwh_qm_a","primaerenergiebedarf_kwh_qm_a","co2_emission_kg_qm_a","heizungsart","baujahr","aussteller_name","pdf_s3_key","notes"],"additionalProperties":false},"example":{"id":"string","property_id":"string","typ":"string","energieeffizienzklasse":"string","endenergiebedarf_kwh_qm_a":0,"primaerenergiebedarf_kwh_qm_a":0,"co2_emission_kg_qm_a":0,"heizungsart":"string","baujahr":0,"aussteller_name":"string","pdf_s3_key":"string","notes":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Validierung"}},"operationId":"postApiV1ImmoEnergieausweise","tags":["immo"],"parameters":[],"description":"Neuen Energieausweis anlegen. Pflicht sind `property_id`, `ausgestellt_am` und `gueltig_bis`; ohne `typ` wird `verbrauchsausweis` gesetzt, alle uebrigen Felder duerfen leer bleiben. Die GEG-Frist von hoechstens zehn Jahren zwischen Ausstellung und Ablauf wird NICHT geprueft, und ob die Immobilie existiert, ebenso wenig — beides liegt beim Aufrufer. Mehrere Ausweise je Immobilie sind moeglich.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"property_id":{"type":"string","format":"uuid"},"typ":{"type":"string","enum":["verbrauchsausweis","bedarfsausweis"],"default":"verbrauchsausweis"},"ausgestellt_am":{"type":"string"},"gueltig_bis":{"type":"string"},"energieeffizienzklasse":{"type":["string","null"],"enum":["A+","A","B","C","D","E","F","G","H",null]},"endenergiebedarf_kwh_qm_a":{"type":["number","null"],"minimum":0},"primaerenergiebedarf_kwh_qm_a":{"type":["number","null"],"minimum":0},"co2_emission_kg_qm_a":{"type":["number","null"],"minimum":0},"heizungsart":{"type":["string","null"],"maxLength":120},"baujahr":{"type":["integer","null"],"minimum":1700,"maximum":2100},"aussteller_name":{"type":["string","null"],"maxLength":255},"pdf_s3_key":{"type":["string","null"],"maxLength":500},"notes":{"type":["string","null"],"maxLength":4000}},"required":["property_id","ausgestellt_am","gueltig_bis"]},"example":{"property_id":"00000000-0000-4000-8000-000000000000","typ":"verbrauchsausweis","ausgestellt_am":"string","gueltig_bis":"string","energieeffizienzklasse":"A+","endenergiebedarf_kwh_qm_a":0,"primaerenergiebedarf_kwh_qm_a":0,"co2_emission_kg_qm_a":0,"heizungsart":"string","baujahr":1700,"aussteller_name":"string","pdf_s3_key":"string","notes":"string"}}}},"summary":"Neuen Energieausweis anlegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/energieausweise/{id}":{"get":{"responses":{"200":{"description":"Ein Energieausweis.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"property_id":{"type":"string"},"typ":{"type":"string"},"ausgestellt_am":{},"gueltig_bis":{},"energieeffizienzklasse":{"type":["string","null"]},"endenergiebedarf_kwh_qm_a":{"type":["number","null"]},"primaerenergiebedarf_kwh_qm_a":{"type":["number","null"]},"co2_emission_kg_qm_a":{"type":["number","null"]},"heizungsart":{"type":["string","null"]},"baujahr":{"type":["number","null"]},"aussteller_name":{"type":["string","null"]},"pdf_s3_key":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_at":{},"updated_at":{}},"required":["id","property_id","typ","energieeffizienzklasse","endenergiebedarf_kwh_qm_a","primaerenergiebedarf_kwh_qm_a","co2_emission_kg_qm_a","heizungsart","baujahr","aussteller_name","pdf_s3_key","notes"],"additionalProperties":false},"example":{"id":"string","property_id":"string","typ":"string","energieeffizienzklasse":"string","endenergiebedarf_kwh_qm_a":0,"primaerenergiebedarf_kwh_qm_a":0,"co2_emission_kg_qm_a":0,"heizungsart":"string","baujahr":0,"aussteller_name":"string","pdf_s3_key":"string","notes":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"nicht gefunden (`energieausweis_not_found`)"}},"operationId":"getApiV1ImmoEnergieausweiseById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Energieausweis-Detail. Der Datensatz steht flach in der Antwort, ohne `data`-Umschlag, und traegt als einzige Leseroute auch `notes` und die Zeitstempel. Weich geloeschte Ausweise sind nicht auffindbar: ein geloeschter und ein nie vorhandener ergeben gleichermassen 404 `energieausweis_not_found`.","summary":"Energieausweis-Detail","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Der geaenderte Energieausweis — oder die Leerlauf-Quittung `{ ok: true, noop: true }`, wenn der Rumpf kein aenderbares Feld enthielt.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"property_id":{"type":"string"},"typ":{"type":"string"},"ausgestellt_am":{},"gueltig_bis":{},"energieeffizienzklasse":{"type":["string","null"]},"endenergiebedarf_kwh_qm_a":{"type":["number","null"]},"primaerenergiebedarf_kwh_qm_a":{"type":["number","null"]},"co2_emission_kg_qm_a":{"type":["number","null"]},"heizungsart":{"type":["string","null"]},"baujahr":{"type":["number","null"]},"aussteller_name":{"type":["string","null"]},"pdf_s3_key":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_at":{},"updated_at":{}},"required":["id","property_id","typ","energieeffizienzklasse","endenergiebedarf_kwh_qm_a","primaerenergiebedarf_kwh_qm_a","co2_emission_kg_qm_a","heizungsart","baujahr","aussteller_name","pdf_s3_key","notes"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok","noop"],"additionalProperties":false}]},"example":{"id":"string","property_id":"string","typ":"string","energieeffizienzklasse":"string","endenergiebedarf_kwh_qm_a":0,"primaerenergiebedarf_kwh_qm_a":0,"co2_emission_kg_qm_a":0,"heizungsart":"string","baujahr":0,"aussteller_name":"string","pdf_s3_key":"string","notes":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"nicht gefunden"}},"operationId":"putApiV1ImmoEnergieausweiseById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Energieausweis aktualisieren. Geschrieben werden nur die mitgeschickten Felder; nicht genannte bleiben unberuehrt, ein ausdrueckliches `null` leert das Feld. Enthaelt der Rumpf kein einziges Feld, antwortet der Aufruf `{ ok: true, noop: true }` und fasst nichts an — dann kommt KEIN Datensatz zurueck. Ein weich geloeschter oder unbekannter Ausweis ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"property_id":{"type":"string","format":"uuid"},"typ":{"type":"string","enum":["verbrauchsausweis","bedarfsausweis"],"default":"verbrauchsausweis"},"ausgestellt_am":{"type":"string"},"gueltig_bis":{"type":"string"},"energieeffizienzklasse":{"type":["string","null"],"enum":["A+","A","B","C","D","E","F","G","H",null]},"endenergiebedarf_kwh_qm_a":{"type":["number","null"],"minimum":0},"primaerenergiebedarf_kwh_qm_a":{"type":["number","null"],"minimum":0},"co2_emission_kg_qm_a":{"type":["number","null"],"minimum":0},"heizungsart":{"type":["string","null"],"maxLength":120},"baujahr":{"type":["integer","null"],"minimum":1700,"maximum":2100},"aussteller_name":{"type":["string","null"],"maxLength":255},"pdf_s3_key":{"type":["string","null"],"maxLength":500},"notes":{"type":["string","null"],"maxLength":4000}}},"example":{"property_id":"00000000-0000-4000-8000-000000000000","typ":"verbrauchsausweis","ausgestellt_am":"string","gueltig_bis":"string","energieeffizienzklasse":"A+","endenergiebedarf_kwh_qm_a":0,"primaerenergiebedarf_kwh_qm_a":0,"co2_emission_kg_qm_a":0,"heizungsart":"string","baujahr":1700,"aussteller_name":"string","pdf_s3_key":"string","notes":"string"}}}},"summary":"Energieausweis aktualisieren","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Quittung — sagt NICHT, ob eine Zeile getroffen wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1ImmoEnergieausweiseById","tags":["immo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Energieausweis loeschen (soft). Gesetzt wird nur `deleted_at`; die Zeile bleibt in der Datenbank und verschwindet aus Liste, Detail und Ablauf-Watcher. Einen Endpunkt zum Zurueckholen gibt es nicht. Der Aufruf quittiert IMMER mit `{ ok: true }` — auch dann, wenn es den Ausweis nie gab oder er schon geloescht war; ein 404 kommt hier also nie.","summary":"Energieausweis loeschen (soft)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/immo/cashflow":{"post":{"responses":{"200":{"description":"Projektion + Sensitivität","content":{"application/json":{"schema":{"type":"object","properties":{"projektion":{"type":"object","properties":{"jahre":{"type":"array","items":{"type":"object","properties":{"jahr":{"type":"integer"},"mieteinnahmen":{"type":"number"},"bewirtschaftungskosten":{"type":"number"},"kapitaldienst":{"type":"number"},"zinsanteil":{"type":"number"},"afa":{"type":"number"},"steuerpflichtiges_einkommen":{"type":"number"},"steuer":{"type":"number"},"cashflow_vor_steuer":{"type":"number"},"cashflow_nach_steuer":{"type":"number"},"kumuliert_nach_steuer":{"type":"number"}},"required":["jahr","mieteinnahmen","bewirtschaftungskosten","kapitaldienst","zinsanteil","afa","steuerpflichtiges_einkommen","steuer","cashflow_vor_steuer","cashflow_nach_steuer","kumuliert_nach_steuer"]}},"summe_cashflow_vor_steuer":{"type":"number"},"summe_cashflow_nach_steuer":{"type":"number"},"summe_steuer":{"type":"number"},"durchschnitt_cashflow_nach_steuer":{"type":"number"}},"required":["jahre","summe_cashflow_vor_steuer","summe_cashflow_nach_steuer","summe_steuer","durchschnitt_cashflow_nach_steuer"]},"sensitivitaet":{"type":["object","null"],"properties":{"basis":{"type":"object","properties":{"szenario":{"type":"string"},"jahr1_cashflow_nach_steuer":{"type":"number"},"gesamt_cashflow_nach_steuer":{"type":"number"}},"required":["szenario","jahr1_cashflow_nach_steuer","gesamt_cashflow_nach_steuer"]},"szenarien":{"type":"array","items":{"type":"object","properties":{"szenario":{"type":"string"},"jahr1_cashflow_nach_steuer":{"type":"number"},"gesamt_cashflow_nach_steuer":{"type":"number"}},"required":["szenario","jahr1_cashflow_nach_steuer","gesamt_cashflow_nach_steuer"]}}},"required":["basis","szenarien"]},"abgeleitet":{"type":"object","properties":{"kapitaldienst_aus_darlehen":{"type":"boolean"},"afa_berechnet":{"type":"boolean"}},"required":["kapitaldienst_aus_darlehen","afa_berechnet"]}},"required":["projektion","sensitivitaet","abgeleitet"]},"example":{"projektion":{"jahre":[{"jahr":0,"mieteinnahmen":0,"bewirtschaftungskosten":0,"kapitaldienst":0,"zinsanteil":0,"afa":0,"steuerpflichtiges_einkommen":0,"steuer":0,"cashflow_vor_steuer":0,"cashflow_nach_steuer":0,"kumuliert_nach_steuer":0}],"summe_cashflow_vor_steuer":0,"summe_cashflow_nach_steuer":0,"summe_steuer":0,"durchschnitt_cashflow_nach_steuer":0},"sensitivitaet":{"basis":{"szenario":"string","jahr1_cashflow_nach_steuer":0,"gesamt_cashflow_nach_steuer":0},"szenarien":[{"szenario":"string","jahr1_cashflow_nach_steuer":0,"gesamt_cashflow_nach_steuer":0}]},"abgeleitet":{"kapitaldienst_aus_darlehen":true,"afa_berechnet":true}}}}},"401":{"description":"Kein Mandantenkontext"}},"operationId":"postApiV1ImmoCashflow","tags":["immo"],"parameters":[],"summary":"Mehrjahres-Cashflow-Projektion + Sensitivität","description":"Optional Darlehen/AfA → automatische Ableitung von Kapitaldienst, Zins und AfA pro Jahr. REINE BERECHNUNG: der Endpunkt liest KEINE Immobilie und keinen Datenbestand — alle Eingaben stehen im Rumpf, und es wird nichts gespeichert. Ausdrueckliche Pro-Jahr-Werte (`kapitaldienst_jahre`, `zinsanteil_jahre`, `afa_jahre`) haben Vorrang; nur was fehlt, wird aus `darlehen` bzw. `afa` hergeleitet — `abgeleitet` sagt, was davon wirklich gerechnet wurde. Fehlen sowohl Werte als auch Herleitung, rechnet die Projektion mit 0, nicht mit einem Fehler. Die Sensitivitaet variiert Miete und Kosten um je 10 % und den Leerstand um 5 Prozentpunkte; mit `sensitivitaet: false` entfaellt sie und das Feld ist null.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jahre":{"type":"integer","minimum":1,"maximum":50},"jahres_kaltmiete":{"type":"number","minimum":0},"mietsteigerung_pct_pa":{"type":"number"},"leerstand_pct":{"type":"number","minimum":0,"maximum":100},"bewirtschaftungskosten_jahr":{"type":"number","minimum":0},"kosten_steigerung_pct_pa":{"type":"number"},"steuersatz_pct":{"type":"number","minimum":0,"maximum":100},"kapitaldienst_jahre":{"type":"array","items":{"type":"number"}},"zinsanteil_jahre":{"type":"array","items":{"type":"number"}},"afa_jahre":{"type":"array","items":{"type":"number"}},"darlehen":{"type":"object","properties":{"betrag":{"type":"number","exclusiveMinimum":0},"sollzins_pct":{"type":"number","minimum":0},"art":{"type":"string","enum":["annuitaet","tilgung","endfaellig"]},"max_monate":{"type":"integer","exclusiveMinimum":0,"maximum":720},"start_datum":{"type":"string"},"anfangs_tilgung_pct":{"type":"number","minimum":0},"monatliche_tilgung_eur":{"type":"number","exclusiveMinimum":0}},"required":["betrag","sollzins_pct","art","max_monate"]},"afa":{"type":"object","properties":{"afa_basis":{"type":"number","exclusiveMinimum":0},"baujahr":{"type":"integer","minimum":1800,"maximum":2100},"start_jahr":{"type":"integer","minimum":1800,"maximum":2200},"methode":{"type":"string","enum":["linear","degressiv"],"default":"linear"}},"required":["afa_basis","baujahr","start_jahr"]},"sensitivitaet":{"type":"boolean","default":true}},"required":["jahre","jahres_kaltmiete","bewirtschaftungskosten_jahr"]},"example":{"jahre":1,"jahres_kaltmiete":0,"mietsteigerung_pct_pa":0,"leerstand_pct":0,"bewirtschaftungskosten_jahr":0,"kosten_steigerung_pct_pa":0,"steuersatz_pct":0,"kapitaldienst_jahre":[0],"zinsanteil_jahre":[0],"afa_jahre":[0],"darlehen":{"betrag":1,"sollzins_pct":0,"art":"annuitaet","max_monate":1,"start_datum":"string","anfangs_tilgung_pct":0,"monatliche_tilgung_eur":1},"afa":{"afa_basis":1,"baujahr":1800,"start_jahr":1800,"methode":"linear"},"sensitivitaet":true}}}}}},"/api/v1/warehouse/standorte":{"get":{"responses":{"200":{"description":"Die nicht geloeschten Standorte, nach Name sortiert.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{},"description":"Die Tabellenzeile, wie sie ist — Feldnamen in snake_case. Die Abfrage liest `*`, deshalb sagt dieser Vertrag die einzelnen Felder bewusst nicht zu."},"description":"Die nicht geloeschten Lagerstandorte des Mandanten, nach Name sortiert. Angelegt werden sie mit `name`, `code`, `adresse`, `typ`, `aktiv` und `notizen`."}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"400":{"description":"Mandantenkennung unbrauchbar (`invalid tenant slug`), als Text."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"getApiV1WarehouseStandorte","tags":["warehouse"],"parameters":[],"summary":"Lagerstandorte auflisten","description":"Liste der Lagerstandorte"},"post":{"responses":{"201":{"description":"Die angelegte Zeile, unverpackt — kein `data`-Umschlag. Sie kommt aus `RETURNING *`, traegt also die Spalten der Mandantentabelle in snake_case.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Die Tabellenzeile, wie sie ist — Feldnamen in snake_case. Die Abfrage liest `*`, deshalb sagt dieser Vertrag die einzelnen Felder bewusst nicht zu."}}}},"400":{"description":"Eingabe ungueltig, oder Mandantenkennung unbrauchbar."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"postApiV1WarehouseStandorte","tags":["warehouse"],"parameters":[],"summary":"Lagerstandort anlegen","description":"Neuer Lagerstandort","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"code":{"type":"string","maxLength":40},"adresse":{"type":"object","additionalProperties":{}},"typ":{"type":"string","enum":["hauptlager","aussenlager","konsignation","produktionspuffer","quarantaene"],"default":"hauptlager"},"aktiv":{"type":"boolean","default":true},"notizen":{"type":"string"}},"required":["name"]},"example":{"name":"string","code":"string","adresse":{},"typ":"hauptlager","aktiv":true,"notizen":"string"}}}}}},"/api/v1/warehouse/bewegungen":{"post":{"responses":{"201":{"description":"Die Bewegung ist gebucht; der Koerper sagt, was sie bewirkt hat.","content":{"application/json":{"schema":{"type":"object","properties":{"bewegung_id":{"type":"string","description":"Kennung der geschriebenen Bewegung (UUID). Leere Zeichenkette, wenn das INSERT nichts zurueckgab."},"bestand_nach":{"type":"number","description":"Bestand des Artikels NACH der Buchung, ueber alle Lagerorte summiert."},"betroffen_lagerorte":{"type":"integer","minimum":0,"description":"Wie viele Lagerort-Zeilen die Buchung angefasst hat."}},"required":["bewegung_id","bestand_nach","betroffen_lagerorte"],"additionalProperties":false},"example":{"bewegung_id":"string","bestand_nach":0,"betroffen_lagerorte":0}}}},"400":{"description":"Eingabe ungueltig, oder Mandantenkennung unbrauchbar."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"409":{"description":"Der Bestand reicht fuer den Abgang nicht. Der EINZIGE Fehler dieser Datei mit JSON-Rumpf — alle anderen kommen als Text.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"insufficient_stock","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext des Lagerdienstes, er nennt den fehlenden Rest."}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"Jeder andere Fehler des Lagerdienstes, z. B. eine fehlende Pflichtangabe wie `nach_lagerplatz_id` beim Eingang. Als Text: `Bewegung fehlgeschlagen: …`."},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"postApiV1WarehouseBewegungen","tags":["warehouse"],"parameters":[],"summary":"Lagerbewegung buchen","description":"Lagerbewegung buchen (Wareneingang/Ausgang/Umlagerung/Inventur)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"art":{"type":"string","enum":["eingang","ausgang","umlagerung","inventur","korrektur","verbrauch_produktion","ruecksendung_kunde","reklamation"]},"artikel_id":{"type":"string","format":"uuid"},"menge":{"type":"number","exclusiveMinimum":0},"von_lagerplatz_id":{"type":"string","format":"uuid"},"nach_lagerplatz_id":{"type":"string","format":"uuid"},"charge_id":{"type":"string","format":"uuid"},"serien_nr":{"type":"string","maxLength":120},"beleg_dokument_id":{"type":"string","format":"uuid"},"kommentar":{"type":"string"},"verbrauchs_strategie":{"type":"string","enum":["fifo","lifo","fefo"]}},"required":["art","artikel_id","menge"]},"example":{"art":"eingang","artikel_id":"00000000-0000-4000-8000-000000000000","menge":1,"von_lagerplatz_id":"00000000-0000-4000-8000-000000000000","nach_lagerplatz_id":"00000000-0000-4000-8000-000000000000","charge_id":"00000000-0000-4000-8000-000000000000","serien_nr":"string","beleg_dokument_id":"00000000-0000-4000-8000-000000000000","kommentar":"string","verbrauchs_strategie":"fifo"}}}}}},"/api/v1/warehouse/bestand/{artikel_id}":{"get":{"responses":{"200":{"description":"Gesamtbestand und Aufteilung auf die Lagerplaetze. ACHTUNG: `standort_id` grenzt nur `bestand_gesamt` ein, NICHT die Liste `lagerorte` — bei gesetztem Standort passen die beiden Angaben deshalb nicht zusammen.","content":{"application/json":{"schema":{"type":"object","properties":{"artikel_id":{"type":"string","description":"Der abgefragte Artikel, unveraendert aus dem Pfad uebernommen."},"bestand_gesamt":{"type":"number","description":"Gesamtbestand. Ist `standort_id` mitgegeben, zaehlt nur dieser Standort — die Summe passt dann nicht zwingend zu allen Zeilen unter `lagerorte`."},"lagerorte":{"type":"array","items":{"type":"object","properties":{"lagerplatz_id":{"type":"string","format":"uuid","description":"Der Lagerplatz, auf dem die Teilmenge liegt."},"menge":{"type":"number","exclusiveMinimum":0,"description":"Menge auf diesem Platz. Leere Plaetze kommen gar nicht vor (`menge > 0`)."},"charge_id":{"type":["string","null"],"format":"uuid","description":"Charge der Teilmenge. `null`, wenn ohne Charge gefuehrt."},"serien_nr":{"type":["string","null"],"description":"Seriennummer der Teilmenge. `null`, wenn ohne Seriennummer gefuehrt."},"platz_code":{"type":["string","null"],"description":"Kurzzeichen des Platzes. `null`, wenn der Platz nicht mehr existiert."},"zone_name":{"type":["string","null"],"description":"Name der Lagerzone. `null`, wenn Platz oder Zone nicht mehr existieren."},"standort_name":{"type":["string","null"],"description":"Name des Standorts. `null`, wenn die Kette darueber nicht mehr aufloesbar ist."}},"required":["lagerplatz_id","menge","charge_id","serien_nr","platz_code","zone_name","standort_name"],"additionalProperties":false},"description":"Aufteilung auf die einzelnen Lagerplaetze, nach Standort, Zone und Platz sortiert."}},"required":["artikel_id","bestand_gesamt","lagerorte"],"additionalProperties":false},"example":{"artikel_id":"string","bestand_gesamt":0,"lagerorte":[{"lagerplatz_id":"00000000-0000-4000-8000-000000000000","menge":1,"charge_id":"00000000-0000-4000-8000-000000000000","serien_nr":"string","platz_code":"string","zone_name":"string","standort_name":"string"}]}}}},"400":{"description":"Mandantenkennung unbrauchbar (`invalid tenant slug`), als Text."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"getApiV1WarehouseBestandByArtikel_id","tags":["warehouse"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"artikel_id","required":true}],"summary":"Bestand eines Artikels lesen","description":"Aktueller Bestand eines Artikels (optional pro Standort)"}},"/api/v1/warehouse/analyse/abc-xyz":{"post":{"responses":{"200":{"description":"Die Analyse ist durchgelaufen; der Koerper nennt die Zahl der Artikel.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`. Ein Fehlschlag kommt als Ausnahme, nicht als `ok: false`."},"analysiert":{"type":"integer","minimum":0,"description":"Anzahl der Artikel, die eine ABC-Klasse bekommen haben."},"hinweis":{"type":"string","description":"Fester Erklaertext zu den Zaehlrhythmen der drei Klassen."}},"required":["ok","analysiert","hinweis"],"additionalProperties":false},"example":{"ok":true,"analysiert":0,"hinweis":"string"}}}},"400":{"description":"Mandantenkennung unbrauchbar (`invalid tenant slug`), als Text."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"postApiV1WarehouseAnalyseAbc-xyz","tags":["warehouse"],"parameters":[],"summary":"ABC/XYZ-Analyse neu berechnen","description":"ABC/XYZ-Analyse neu berechnen (manueller Trigger). Vereinfachte Fassung: die ABC-Klasse entsteht aus den Ausgangsmengen der letzten zwoelf Monate, nicht aus Umsatzwerten. Ein XYZ-Teil wird trotz des Pfadnamens NICHT gerechnet."}},"/api/v1/production/auftraege":{"get":{"responses":{"200":{"description":"Die Auftraege, hoechste Prioritaet zuerst. Hoechstens 200, ohne Blaetterung.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{},"description":"Die Tabellenzeile, wie sie ist — Feldnamen in snake_case. Die Abfrage liest `*`, deshalb sagt dieser Vertrag die einzelnen Felder bewusst nicht zu. `menge` und `prioritaet` sind Zahlen, die uebrigen Dezimalspalten Zeichenketten."},"maxItems":200,"description":"Die Fertigungsauftraege, hoechste Prioritaet zuerst, dann fruehester Planstart. Hoechstens 200 Eintraege — es gibt KEINE Blaetterung, aeltere Auftraege fallen stillschweigend heraus."}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"400":{"description":"Mandantenkennung unbrauchbar (`invalid tenant slug`), als Text."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"getApiV1ProductionAuftraege","tags":["production"],"parameters":[],"summary":"Fertigungsauftraege auflisten","description":"Liste der Fertigungsaufträge"},"post":{"responses":{"201":{"description":"Die angelegte Zeile, unverpackt. Sie kommt aus `RETURNING *`, traegt also die Spalten der Mandantentabelle in snake_case.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Die Tabellenzeile, wie sie ist — Feldnamen in snake_case. Die Abfrage liest `*`, deshalb sagt dieser Vertrag die einzelnen Felder bewusst nicht zu. `menge` und `prioritaet` sind Zahlen, die uebrigen Dezimalspalten Zeichenketten."}}}},"400":{"description":"Eingabe ungueltig, oder Mandantenkennung unbrauchbar."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"postApiV1ProductionAuftraege","tags":["production"],"parameters":[],"summary":"Fertigungsauftrag anlegen","description":"Neuer Fertigungsauftrag. Die Auftragsnummer vergibt der Server im Muster `FA-JAHR-NNNNN`, der Zustand startet auf `planung`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"artikel_id":{"type":"string","format":"uuid"},"menge":{"type":"number","exclusiveMinimum":0},"stueckliste_id":{"type":"string","format":"uuid"},"arbeitsplan_id":{"type":"string","format":"uuid"},"plan_start":{"type":"string"},"plan_ende":{"type":"string"},"prioritaet":{"type":"integer","minimum":0,"maximum":100,"default":50},"kunden_auftrag_id":{"type":"string","format":"uuid"},"notizen":{"type":"string"}},"required":["artikel_id","menge"]},"example":{"artikel_id":"00000000-0000-4000-8000-000000000000","menge":1,"stueckliste_id":"00000000-0000-4000-8000-000000000000","arbeitsplan_id":"00000000-0000-4000-8000-000000000000","plan_start":"string","plan_ende":"string","prioritaet":0,"kunden_auftrag_id":"00000000-0000-4000-8000-000000000000","notizen":"string"}}}}}},"/api/v1/production/auftraege/{id}/status":{"patch":{"responses":{"200":{"description":"Die Zeile im Zustand nach der Aenderung. Sie kommt aus `RETURNING *`, traegt also die Spalten der Mandantentabelle in snake_case.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Die Tabellenzeile, wie sie ist — Feldnamen in snake_case. Die Abfrage liest `*`, deshalb sagt dieser Vertrag die einzelnen Felder bewusst nicht zu. `menge` und `prioritaet` sind Zahlen, die uebrigen Dezimalspalten Zeichenketten."}}}},"400":{"description":"Die Kennung im Pfad ist keine UUID. Ein ungueltiger Zielzustand kommt dagegen vom Eingabe-Validator und hat eine andere Form.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_id","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"404":{"description":"Kein Fertigungsauftrag mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Der Sprung ist im Lebenszyklus nicht vorgesehen. `allowed` nennt die Alternativen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_transition","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext mit Ist- und Wunschzustand."},"allowed":{"type":"array","items":{"type":"string","enum":["planung","freigegeben","in_arbeit","abgeschlossen","storniert"]},"description":"Die vom aktuellen Zustand aus erlaubten Zielzustaende. LEER heisst: der Auftrag ist endgueltig (`abgeschlossen` oder `storniert`) und laesst sich nicht mehr bewegen."}},"required":["error","message","allowed"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"patchApiV1ProductionAuftraegeByIdStatus","tags":["production"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Zustand eines Fertigungsauftrags aendern","description":"Status eines Fertigungsauftrags ändern (Lebenszyklus). Der Weg ist einbahnig: `planung` → `freigegeben` → `in_arbeit` → `abgeschlossen`; `storniert` geht aus jedem nicht endgueltigen Zustand. Derselbe Zustand noch einmal zu setzen ist erlaubt und bewirkt nichts. Der Server stempelt dabei `ist_start` und `ist_ende`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["planung","freigegeben","in_arbeit","abgeschlossen","storniert"]}},"required":["status"]},"example":{"status":"planung"}}}}}},"/api/v1/production/auftraege/{id}/rueckmeldung":{"post":{"responses":{"201":{"description":"Die angelegte Rueckmeldung, unverpackt. Sie kommt aus `RETURNING *`, traegt also die Spalten der Mandantentabelle in snake_case.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Die Tabellenzeile, wie sie ist — Feldnamen in snake_case. Die Abfrage liest `*`, deshalb sagt dieser Vertrag die einzelnen Felder bewusst nicht zu. `menge` und `prioritaet` sind Zahlen, die uebrigen Dezimalspalten Zeichenketten."}}}},"400":{"description":"Eingabe ungueltig, oder Mandantenkennung unbrauchbar."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"postApiV1ProductionAuftraegeByIdRueckmeldung","tags":["production"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rueckmeldung zu einem Fertigungsauftrag erfassen","description":"BDE-Rückmeldung: Zeit + Menge je Vorgang. Der Auftrag wird dabei NICHT geprueft — eine unbekannte Kennung im Pfad fuehrt zu einem Datenbankfehler, nicht zu 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"vorgang_id":{"type":"string","format":"uuid"},"mitarbeiter_id":{"type":"string","format":"uuid"},"start":{"type":"string"},"ende":{"type":"string"},"menge_gut":{"type":"number","minimum":0,"default":0},"menge_ausschuss":{"type":"number","minimum":0,"default":0},"menge_nacharbeit":{"type":"number","minimum":0,"default":0},"kommentar":{"type":"string"}}},"example":{"vorgang_id":"00000000-0000-4000-8000-000000000000","mitarbeiter_id":"00000000-0000-4000-8000-000000000000","start":"string","ende":"string","menge_gut":0,"menge_ausschuss":0,"menge_nacharbeit":0,"kommentar":"string"}}}}}},"/api/v1/production/oee/{arbeitsplatz_id}":{"get":{"responses":{"200":{"description":"Die drei Faktoren, ihr Produkt und die Einordnung — alle als ANTEIL von 0 bis 1, nicht in Prozent. `hat_rueckmeldungen: false` heisst: es lag nichts vor, die Nullen sind kein gemessener Wirkungsgrad.","content":{"application/json":{"schema":{"type":"object","properties":{"arbeitsplatz_id":{"type":"string","format":"uuid","description":"Der abgefragte Arbeitsplatz, aus dem Pfad uebernommen."},"planzeit_min":{"type":"number","description":"Geplante Betriebszeit in Minuten. Stammt aus der Tageskapazitaet des Arbeitsplatzes; ohne gepflegte Kapazitaet werden 480 Minuten angenommen."},"hat_rueckmeldungen":{"type":"boolean","description":"Gab es in den letzten 24 Stunden ueberhaupt Rueckmeldungen? Bei `false` sind alle Kennzahlen `0` — das heisst „nichts gemeldet\", NICHT „0 % Wirkungsgrad\"."},"verfuegbarkeit":{"type":"number","minimum":0,"maximum":1,"description":"Laufzeit geteilt durch Planzeit, als ANTEIL von 0 bis 1 (nicht in Prozent)."},"leistung":{"type":"number","minimum":0,"maximum":1,"description":"Mengenleistung gegen die Soll-Taktzeit, 0 bis 1. Vereinfacht: die Taktzeit wird fest mit einer Minute je Stueck angesetzt, nicht aus dem Arbeitsplan gelesen."},"qualitaet":{"type":"number","minimum":0,"maximum":1,"description":"Gutstueck geteilt durch Gesamtstueck, 0 bis 1."},"oee":{"type":"number","minimum":0,"maximum":1,"description":"Produkt der drei Faktoren, 0 bis 1. Die Gesamtanlageneffektivitaet."},"klassifikation":{"type":"string","enum":["world_class","best","gut","standard","verbesserung_noetig"],"description":"Einordnung des `oee`-Werts: ab 0,90 `world_class`, ab 0,85 `best`, ab 0,70 `gut`, ab 0,60 `standard`, darunter `verbesserung_noetig`."}},"required":["arbeitsplatz_id","planzeit_min","hat_rueckmeldungen","verfuegbarkeit","leistung","qualitaet","oee","klassifikation"],"additionalProperties":false},"example":{"arbeitsplatz_id":"00000000-0000-4000-8000-000000000000","planzeit_min":0,"hat_rueckmeldungen":true,"verfuegbarkeit":0,"leistung":0,"qualitaet":0,"oee":0,"klassifikation":"world_class"}}}},"400":{"description":"Die Kennung im Pfad ist keine UUID, oder die Mandantenkennung ist unbrauchbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_arbeitsplatz_id","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`), als Text."}},"operationId":"getApiV1ProductionOeeByArbeitsplatz_id","tags":["production"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"arbeitsplatz_id","required":true}],"summary":"Gesamtanlageneffektivitaet eines Arbeitsplatzes","description":"OEE eines Arbeitsplatzes (heute). Vereinfachte Rechnung: ausgewertet werden die Rueckmeldungen der letzten 24 Stunden, und die Soll-Taktzeit wird fest mit einer Minute je Stueck angesetzt statt aus dem Arbeitsplan gelesen."}},"/api/v1/inventory/articles":{"get":{"responses":{"200":{"description":"Artikelliste. `degraded` bzw. `warning` sagen, ob die Zeilen aus dem Normalfall, dem Drift-Notfall oder gar nicht kamen.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"}},"required":["limit","offset"]},"degraded":{"type":"boolean"},"warning":{"type":"string"}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0},"degraded":true,"warning":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1InventoryArticles","tags":["Inventory"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"category","schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"List inventory articles","description":"LH-063 — paginated catalogue of inventory articles"},"post":{"responses":{"201":{"description":"Artikel angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"allowed":{"type":"array","items":{"type":"string"}},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1InventoryArticles","tags":["Inventory"],"parameters":[],"summary":"Create article","description":"Legt eine Zeile in `inventory_articles` an (201). Wird `categories` mitgeschickt, landet der erste Eintrag zugleich als Primaerkategorie in `category` und die vollstaendige Liste in der Zuordnungstabelle `inventory_article_categories`. Eine bereits vergebene SKU beantwortet die Route mit 409 und `error: \"duplicate_sku\"`. Das Anlegen wird im Aktivitaetsverlauf des Artikels vermerkt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string","maxLength":5000},"category":{"type":"string"},"categories":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0,"default":0},"purchasePrice":{"type":"number","minimum":0,"default":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"trackingType":{"type":"string","enum":["none","lot","serial"],"default":"none"},"minStock":{"type":"number","minimum":0,"default":0},"customFields":{"type":"object","additionalProperties":{}},"ean":{"type":"string","pattern":"^[0-9]*$","maxLength":13},"supplierId":{"type":"string","format":"uuid"},"supplierArticleNo":{"type":"string","maxLength":64},"minOrderQty":{"type":"number","minimum":0}},"required":["sku","name"]},"example":{"sku":"string","name":"string","description":"string","category":"string","categories":["string"],"unit":"string","unitPrice":0,"purchasePrice":0,"taxRate":0,"trackingType":"none","minStock":0,"customFields":{},"supplierId":"00000000-0000-4000-8000-000000000000","supplierArticleNo":"string","minOrderQty":0}}}}}},"/api/v1/inventory/articles/next-number":{"get":{"responses":{"200":{"description":"Vorschlag. `degraded` heisst: ohne Datenbank geraten.","content":{"application/json":{"schema":{"type":"object","properties":{"suggestion":{"type":"string"},"degraded":{"type":"boolean"}},"required":["suggestion"],"additionalProperties":false},"example":{"suggestion":"string","degraded":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1InventoryArticlesNext-number","tags":["Inventory"],"parameters":[],"summary":"Suggest next article number","description":"Welle 4 — schlägt die nächste Artikelnummer aus dem Bestand vor"}},"/api/v1/inventory/articles/{id}":{"get":{"responses":{"200":{"description":"Der Artikel","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden (auch bei ungueltiger Id-Form)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"allowed":{"type":"array","items":{"type":"string"}},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1InventoryArticlesById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get article by id","description":"Liest genau einen Artikel und ergaenzt `categories` aus der Zuordnungstabelle `inventory_article_categories`; ist dort nichts hinterlegt, steht die Primaerkategorie als einziger Eintrag darin. Der Einbettungsvektor wird vor dem Senden entfernt. Die Abfrage filtert `deleted_at` NICHT — ein zuvor geloeschter Artikel kommt hier weiterhin. Eine id, die keine UUID ist, ergibt 404 und keinen Serverfehler."},"put":{"responses":{"200":{"description":"Der aktualisierte Artikel","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"422":{"description":"Eigene Regel verletzt — nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"entity_rule_violation"},"violations":{"type":"array","items":{"type":"object","additionalProperties":{}}},"message_de":{"type":"string"}},"required":["error","violations","message_de"]}}}}},"operationId":"putApiV1InventoryArticlesById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Artikel aktualisieren","description":"Nimmt denselben Teilkoerper wie PATCH /:id und schreibt nur die mitgeschickten Felder — trotz PUT also kein vollstaendiges Ersetzen. `customFields` wird in das JSONB gemergt: ein nicht mitgeschickter Schluessel bleibt stehen, `null` loescht ihn. Wird `categories` mitgeschickt, ersetzt die Liste die bisherige Zuordnung vollstaendig und ihr erster Eintrag wird zur Primaerkategorie. Vorab pruefen die Eigenen Regeln den Vorgang; bei einer Verletzung kommt 422 und es wird NICHTS geschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string","maxLength":5000},"category":{"type":"string"},"categories":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0,"default":0},"purchasePrice":{"type":"number","minimum":0,"default":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"trackingType":{"type":"string","enum":["none","lot","serial"],"default":"none"},"minStock":{"type":"number","minimum":0,"default":0},"customFields":{"type":"object","additionalProperties":{}},"ean":{"type":"string","pattern":"^[0-9]*$","maxLength":13},"supplierId":{"type":"string","format":"uuid"},"supplierArticleNo":{"type":"string","maxLength":64},"minOrderQty":{"type":"number","minimum":0}}},"example":{"sku":"string","name":"string","description":"string","category":"string","categories":["string"],"unit":"string","unitPrice":0,"purchasePrice":0,"taxRate":0,"trackingType":"none","minStock":0,"customFields":{},"supplierId":"00000000-0000-4000-8000-000000000000","supplierArticleNo":"string","minOrderQty":0}}}}},"patch":{"responses":{"200":{"description":"Der aktualisierte Artikel","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"422":{"description":"Eigene Regel verletzt — nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"entity_rule_violation"},"violations":{"type":"array","items":{"type":"object","additionalProperties":{}}},"message_de":{"type":"string"}},"required":["error","violations","message_de"]}}}}},"operationId":"patchApiV1InventoryArticlesById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelne Felder eines Artikels aktualisieren","description":"Gleicher Vertrag wie PUT /:id — derselbe Teilkoerper, dieselbe Logik. Die Route besteht, weil die Maske fuer Eigene Felder ueber `PATCH /api/v1/products/:id` speichert und der `/products`-Alias auf diesen Router zeigt. `customFields` wird in das JSONB gemergt, `null` loescht einen Schluessel. Eigene Regeln koennen den Vorgang mit 422 ablehnen, dann wird nichts geschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string","maxLength":5000},"category":{"type":"string"},"categories":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0,"default":0},"purchasePrice":{"type":"number","minimum":0,"default":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"trackingType":{"type":"string","enum":["none","lot","serial"],"default":"none"},"minStock":{"type":"number","minimum":0,"default":0},"customFields":{"type":"object","additionalProperties":{}},"ean":{"type":"string","pattern":"^[0-9]*$","maxLength":13},"supplierId":{"type":"string","format":"uuid"},"supplierArticleNo":{"type":"string","maxLength":64},"minOrderQty":{"type":"number","minimum":0}}},"example":{"sku":"string","name":"string","description":"string","category":"string","categories":["string"],"unit":"string","unitPrice":0,"purchasePrice":0,"taxRate":0,"trackingType":"none","minStock":0,"customFields":{},"supplierId":"00000000-0000-4000-8000-000000000000","supplierArticleNo":"string","minOrderQty":0}}}}},"delete":{"responses":{"200":{"description":"Geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"deleteApiV1InventoryArticlesById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Artikel löschen","description":"Soft-Delete: setzt allein `deleted_at`. Die Zeile bleibt in `inventory_articles` stehen und wird von GET /:id weiterhin geliefert; Bestand, Varianten und Bilder bleiben unberuehrt. Der Vorgang wird im Aktivitaetsverlauf vermerkt. Der Handler prueft NICHT, ob eine Zeile getroffen wurde — eine unbekannte id wird ebenso mit `{ ok: true }` quittiert."}},"/api/v1/inventory/articles/{id}/variants":{"get":{"responses":{"200":{"description":"Varianten des Artikels","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1InventoryArticlesByIdVariants","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List product variants","description":"W17-A1 — all variants for a product/article"},"post":{"responses":{"201":{"description":"Variante angelegt","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1InventoryArticlesByIdVariants","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create product variant","description":"W17-A1 — create a variant for a product/article","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string"},"attributes":{"type":"object","additionalProperties":{},"default":{}},"price":{"type":"number","minimum":0},"stockQty":{"type":"integer","default":0},"barcode":{"type":"string"}}},"example":{"sku":"string","attributes":{},"price":0,"stockQty":0,"barcode":"string"}}}}}},"/api/v1/inventory/articles/{id}/variants/{variantId}":{"put":{"responses":{"200":{"description":"Die aktualisierte Variante","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"putApiV1InventoryArticlesByIdVariantsByVariantId","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"variantId","required":true}],"summary":"Update product variant","description":"W17-A1 — update sku, attributes, price, stock_qty or barcode of a variant","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string"},"attributes":{"type":"object","additionalProperties":{},"default":{}},"price":{"type":"number","minimum":0},"stockQty":{"type":"integer","default":0},"barcode":{"type":"string"}}},"example":{"sku":"string","attributes":{},"price":0,"stockQty":0,"barcode":"string"}}}}},"delete":{"responses":{"200":{"description":"Variante geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"deleteApiV1InventoryArticlesByIdVariantsByVariantId","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"variantId","required":true}],"summary":"Delete product variant","description":"W17-A1 — permanently delete a variant"}},"/api/v1/inventory/articles/{id}/attributes":{"get":{"responses":{"200":{"description":"Merkmale des Artikels","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1InventoryArticlesByIdAttributes","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List product attributes","description":"W17-A1 — attribute definitions (e.g. Farbe: [Rot, Blau]) for a product"},"post":{"responses":{"201":{"description":"Merkmal angelegt","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1InventoryArticlesByIdAttributes","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create product attribute","description":"W17-A1 — define an attribute (name + allowed values) for a product","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"values":{"type":"array","items":{"type":"string"},"minItems":1}},"required":["name","values"]},"example":{"name":"string","values":["string"]}}}}}},"/api/v1/inventory/articles/{id}/image":{"put":{"responses":{"200":{"description":"Der Artikel mit dem neuen Bild","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Article not found"},"413":{"description":"File too large"}},"operationId":"putApiV1InventoryArticlesByIdImage","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Upload article image (main or additional)","description":"W2-D — multipart upload. Form field `file` (binary, max 5 MB, jpeg/png/webp). Optional `slot=main` (default) or `slot=N` where N is the additional-image index (0-4). On success returns the updated article row."}},"/api/v1/inventory/articles/{id}/image/{idx}":{"get":{"responses":{"200":{"description":"Bilddatei (Bytes oder 302 auf eine externe Adresse)","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}}}},"302":{"description":"Das Bild liegt extern — Weiterleitung auf seine Adresse"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Artikel oder Bild nicht vorhanden"},"503":{"description":"Speicher nicht erreichbar"}},"operationId":"getApiV1InventoryArticlesByIdImageByIdx","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"idx","required":true}],"summary":"Artikelbild ausliefern","description":"Liefert die Bilddatei als Bytes. `:idx` ist `main` oder 0-4 (Position der Zusatzbilder). Der Mandantenbezug wird beim Lesen aus dem Speicher geprueft. Der Inhaltstyp ergibt sich aus der Dateiendung des Schluessels — es kommen nur JPEG, PNG und WebP in Frage, weil der Upload nichts anderes durchlaesst. Steht in der Spalte bereits eine vollstaendige http(s)-Adresse, liegt das Bild NICHT bei uns: dann kommt statt Bytes eine Weiterleitung (302) dorthin. Die Antwort ist als privat gekennzeichnet und gehoert in keinen geteilten Zwischenspeicher. Fremder Mandant und fehlendes Objekt sind fuer den Aufrufer dasselbe: 404."},"delete":{"responses":{"200":{"description":"Der Artikel ohne das entfernte Bild","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"deleteApiV1InventoryArticlesByIdImageByIdx","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"idx","required":true}],"summary":"Remove article image","description":"W2-D — :idx is `main` or 0-4 (additional-image position)."}},"/api/v1/inventory/articles/{id}/datasheet":{"put":{"responses":{"200":{"description":"Der Artikel mit dem neuen Datenblatt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"413":{"description":"File too large"}},"operationId":"putApiV1InventoryArticlesByIdDatasheet","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Upload technical datasheet (PDF, max 25 MB)","description":"W2-D — sets datasheet_url to the storage key returned by @nemix/storage."}},"/api/v1/inventory/articles/{id}/usage":{"get":{"responses":{"200":{"description":"Verwendungs-Liste","content":{"application/json":{"schema":{"type":"object","properties":{"articleId":{"type":"string"},"quotes":{"type":"array","items":{"type":"object","properties":{"id":{},"number":{},"date":{}}}},"orders":{"type":"array","items":{"type":"object","properties":{"id":{},"number":{},"date":{}}}},"invoices":{"type":"array","items":{"type":"object","properties":{"id":{},"number":{},"date":{}}}}},"required":["articleId","quotes","orders","invoices"],"additionalProperties":false},"example":{"articleId":"string","quotes":[{}],"orders":[{}],"invoices":[{}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Artikel nicht gefunden"}},"operationId":"getApiV1InventoryArticlesByIdUsage","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Artikel-Verwendung: Belege mit diesem Artikel","description":"#408/A2 — alle Angebote/Aufträge/Rechnungen deren Positionen articleId=:id enthalten. Tenant-gescopt, Read-only."}},"/api/v1/inventory/articles/{id}/supplier":{"put":{"responses":{"200":{"description":"Der Artikel mit der neuen Lieferanten-Zuordnung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"putApiV1InventoryArticlesByIdSupplier","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update supplier metadata for an article","description":"W2-D — body: { supplierId?, supplierArticleNo?, minOrderQty?, ean? }. Pass null on a field to clear it. Returns the updated article row.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"supplierId":{"type":["string","null"],"format":"uuid"},"supplierArticleNo":{"type":["string","null"],"maxLength":64},"minOrderQty":{"type":["number","null"],"minimum":0},"ean":{"type":["string","null"],"maxLength":13}}},"example":{"supplierId":"00000000-0000-4000-8000-000000000000","supplierArticleNo":"string","minOrderQty":0,"ean":"string"}}}}}},"/api/v1/inventory/wareneingang":{"post":{"responses":{"200":{"description":"Bereits gebucht (gleicher Idempotenz-Schluessel) — es wurde nichts erneut gebucht","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"belegnummer":{"type":"string"},"status":{"type":"string"},"bestellungId":{"type":["string","null"]},"bestellStatus":{"type":["string","null"]},"gesamtwert":{"type":"number"},"positionen":{"type":"array","items":{}},"bewegungIds":{"type":"array","items":{"type":"string"}},"restmengen":{"type":"array","items":{}},"journalId":{"type":["string","null"]},"auditIntentId":{"type":["string","null"]},"warnungen":{"type":"array","items":{"type":"string"}},"bereitsGebucht":{"type":"boolean"}},"required":["id","belegnummer","status","bestellungId","bestellStatus","gesamtwert","positionen","bewegungIds","restmengen","journalId","auditIntentId","warnungen","bereitsGebucht"]},"example":{"id":"string","belegnummer":"string","status":"string","bestellungId":"string","bestellStatus":"string","gesamtwert":0,"positionen":[],"bewegungIds":["string"],"restmengen":[],"journalId":"string","auditIntentId":"string","warnungen":["string"],"bereitsGebucht":true}}}},"201":{"description":"Wareneingang gebucht","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"belegnummer":{"type":"string"},"status":{"type":"string"},"bestellungId":{"type":["string","null"]},"bestellStatus":{"type":["string","null"]},"gesamtwert":{"type":"number"},"positionen":{"type":"array","items":{}},"bewegungIds":{"type":"array","items":{"type":"string"}},"restmengen":{"type":"array","items":{}},"journalId":{"type":["string","null"]},"auditIntentId":{"type":["string","null"]},"warnungen":{"type":"array","items":{"type":"string"}},"bereitsGebucht":{"type":"boolean"}},"required":["id","belegnummer","status","bestellungId","bestellStatus","gesamtwert","positionen","bewegungIds","restmengen","journalId","auditIntentId","warnungen","bereitsGebucht"]},"example":{"id":"string","belegnummer":"string","status":"string","bestellungId":"string","bestellStatus":"string","gesamtwert":0,"positionen":[],"bewegungIds":["string"],"restmengen":[],"journalId":"string","auditIntentId":"string","warnungen":["string"],"bereitsGebucht":true}}}},"400":{"description":"Eingabe unvollstaendig"},"401":{"description":"Nicht angemeldet"},"403":{"description":"Rolle darf nicht buchen"},"404":{"description":"Bestellung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Fehlerschluessel zum Auswerten im Programm, z. B. `wareneingang_not_found`."},"message":{"type":"string","description":"Deutscher Klartext. Fehlt, wenn der Schluessel fuer sich spricht."}},"required":["error"]}}}},"409":{"description":"Fachlicher Konflikt (storniert, Seriennummer doppelt)"},"422":{"description":"Fachlich unzulaessig (Ueberlieferung, unbekannter Artikel, fremde Charge)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Fehlerschluessel zum Auswerten im Programm, z. B. `wareneingang_not_found`."},"message":{"type":"string","description":"Deutscher Klartext. Fehlt, wenn der Schluessel fuer sich spricht."}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1InventoryWareneingang","tags":["Inventory"],"parameters":[],"summary":"Bucht einen Wareneingang vollstaendig in einer Transaktion","description":"Bucht einen Wareneingang vollstaendig: Beleg, Positionen, Bestandsbewegungen, Fortschreibung der Bestellung, Journal und Audit-Vorsatz in einer Transaktion.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lieferantName":{"type":"string","minLength":1,"maxLength":255},"lieferantId":{"type":"string"},"bestellungId":{"type":"string","format":"uuid"},"idempotencyKey":{"type":"string","minLength":1,"maxLength":200},"positionen":{"type":"array","items":{"type":"object","properties":{"artikelId":{"type":"string","format":"uuid"},"bestellPosition":{"type":"integer","minimum":1},"bezeichnung":{"type":"string","maxLength":255},"menge":{"type":"number","exclusiveMinimum":0},"einheit":{"type":"string","maxLength":20},"einzelpreis":{"type":"number","minimum":0},"chargeId":{"type":"string","format":"uuid"},"serieId":{"type":"string","format":"uuid"},"ortId":{"type":"string","format":"uuid"},"qmStatus":{"type":"string","enum":["frei","gesperrt","pruefung"]}},"required":["menge"]},"minItems":1},"notizen":{"type":"string","maxLength":2000},"ueberlieferungErlaubt":{"type":"boolean"}},"required":["lieferantName","positionen"]},"example":{"lieferantName":"string","lieferantId":"string","bestellungId":"00000000-0000-4000-8000-000000000000","idempotencyKey":"string","positionen":[{"artikelId":"00000000-0000-4000-8000-000000000000","bestellPosition":1,"bezeichnung":"string","menge":1,"einheit":"string","einzelpreis":0,"chargeId":"00000000-0000-4000-8000-000000000000","serieId":"00000000-0000-4000-8000-000000000000","ortId":"00000000-0000-4000-8000-000000000000","qmStatus":"frei"}],"notizen":"string","ueberlieferungErlaubt":true}}}}},"get":{"responses":{"200":{"description":"Die Belegkoepfe des Mandanten samt Seitenangaben.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Wareneingangs (UUID, vom Server vergeben)"},"belegnummer":{"type":"string","minLength":1,"description":"Belegnummer des Wareneingangs, mandantenweit eindeutig."},"lieferantName":{"type":"string","minLength":1,"description":"Name des Lieferanten, wie er beim Buchen angegeben wurde."},"lieferantId":{"type":["string","null"],"description":"Kennung des Lieferanten im Stamm. `null`, wenn nur ein Name erfasst wurde."},"bestellnr":{"type":["string","null"],"description":"Bestellnummer als Klartext. `null`, wenn ohne Bestellbezug gebucht."},"bestellungId":{"type":["string","null"],"format":"uuid","description":"Kennung der zugehoerigen Bestellung. `null`, wenn ohne Bestellbezug gebucht."},"eingangsdatum":{"type":"string","minLength":10,"description":"Tag des Wareneingangs. In der Datenbank ein reiner Kalendertag (Spaltentyp DATE); in der Antwort steht die Zeichenkette, die der Treiber daraus macht."},"status":{"type":"string","minLength":1,"description":"Zustand des Belegs. Beobachtet: `angekommen` (Voreinstellung) und `eingelagert`."},"positionsCount":{"type":"integer","minimum":0,"description":"Anzahl der Positionen dieses Belegs."},"gesamtwert":{"type":"number","description":"Warenwert des Belegs in Euro. Unlesbare Werte werden zu `0`, nie zu `null`."},"journalId":{"type":["string","null"],"format":"uuid","description":"Kennung der erzeugten Journalbuchung. `null`, wenn keine geschrieben wurde."},"notizen":{"type":["string","null"],"description":"Freitext zum Beleg. `null`, wenn keiner erfasst ist."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Buchung als ISO-8601-Zeitstempel in UTC."}},"required":["id","belegnummer","lieferantName","lieferantId","bestellnr","bestellungId","eingangsdatum","status","positionsCount","gesamtwert","journalId","notizen","createdAt"],"additionalProperties":false},"description":"Die Belegkoepfe, neueste zuerst. OHNE Positionen — die liefert erst GET /{id}."},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angeforderte Seitengroesse."},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege."},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer, unabhaengig von der Seite."}},"required":["limit","offset","total"],"additionalProperties":false,"description":"Seitenangaben zur Abfrage."},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, dessen Belege gelesen wurden."},"source":{"type":"string","const":"db","description":"Immer `db`. Die Liste stammt nie aus einem Zwischenspeicher."}},"required":["tenantId","source"],"additionalProperties":false,"description":"Angaben zur Herkunft der Daten."}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","belegnummer":"string","lieferantName":"string","lieferantId":"string","bestellnr":"string","bestellungId":"00000000-0000-4000-8000-000000000000","eingangsdatum":"stringxxxx","status":"string","positionsCount":0,"gesamtwert":0,"journalId":"00000000-0000-4000-8000-000000000000","notizen":"string","createdAt":"2026-01-01T12:00:00.000Z"}],"pagination":{"limit":1,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"Nicht angemeldet"},"422":{"description":"Fachlich unzulaessig — der Lagerdienst hat die Abfrage abgelehnt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Fehlerschluessel zum Auswerten im Programm, z. B. `wareneingang_not_found`."},"message":{"type":"string","description":"Deutscher Klartext. Fehlt, wenn der Schluessel fuer sich spricht."}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar oder das Anlegen der Tabellen schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryWareneingang","tags":["Inventory"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"bestellungId","schema":{"type":"string","format":"uuid"}}],"summary":"Wareneingaenge auflisten","description":"Liste der gebuchten Wareneingaenge, neueste zuerst. Nur die Belegkoepfe — die Positionen liefert erst der Aufruf eines einzelnen Belegs. Ueber `bestellungId` auf eine Bestellung eingrenzbar."}},"/api/v1/inventory/wareneingang/{id}":{"get":{"responses":{"200":{"description":"Der Belegkopf, um seine Positionen erweitert. Die Positionen stehen unter `positionen` DIREKT im Kopf-Objekt, nicht in einem eigenen Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Wareneingangs (UUID, vom Server vergeben)"},"belegnummer":{"type":"string","minLength":1,"description":"Belegnummer des Wareneingangs, mandantenweit eindeutig."},"lieferantName":{"type":"string","minLength":1,"description":"Name des Lieferanten, wie er beim Buchen angegeben wurde."},"lieferantId":{"type":["string","null"],"description":"Kennung des Lieferanten im Stamm. `null`, wenn nur ein Name erfasst wurde."},"bestellnr":{"type":["string","null"],"description":"Bestellnummer als Klartext. `null`, wenn ohne Bestellbezug gebucht."},"bestellungId":{"type":["string","null"],"format":"uuid","description":"Kennung der zugehoerigen Bestellung. `null`, wenn ohne Bestellbezug gebucht."},"eingangsdatum":{"type":"string","minLength":10,"description":"Tag des Wareneingangs. In der Datenbank ein reiner Kalendertag (Spaltentyp DATE); in der Antwort steht die Zeichenkette, die der Treiber daraus macht."},"status":{"type":"string","minLength":1,"description":"Zustand des Belegs. Beobachtet: `angekommen` (Voreinstellung) und `eingelagert`."},"positionsCount":{"type":"integer","minimum":0,"description":"Anzahl der Positionen dieses Belegs."},"gesamtwert":{"type":"number","description":"Warenwert des Belegs in Euro. Unlesbare Werte werden zu `0`, nie zu `null`."},"journalId":{"type":["string","null"],"format":"uuid","description":"Kennung der erzeugten Journalbuchung. `null`, wenn keine geschrieben wurde."},"notizen":{"type":["string","null"],"description":"Freitext zum Beleg. `null`, wenn keiner erfasst ist."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Buchung als ISO-8601-Zeitstempel in UTC."},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Position (UUID, vom Server vergeben)"},"position":{"type":"integer","minimum":0,"description":"Laufende Nummer innerhalb des Belegs, 1-basiert."},"artikelId":{"type":"string","format":"uuid","description":"Vereinnahmter Artikel."},"bezeichnung":{"type":"string","description":"Bezeichnung der Position zum Zeitpunkt der Buchung."},"menge":{"type":"number","description":"Vereinnahmte Menge. Unlesbare Werte werden zu `0`, nie zu `null`."},"einheit":{"type":"string","minLength":1,"description":"Mengeneinheit, z. B. `Stk`. Voreinstellung ist `Stk`."},"einzelpreis":{"type":"number","description":"Preis je Einheit in Euro, vier Nachkommastellen in der Datenbank."},"gesamtpreis":{"type":"number","description":"Menge mal Einzelpreis in Euro."},"chargeId":{"type":["string","null"],"format":"uuid","description":"Zugeordnete Charge. `null`, wenn nicht chargengefuehrt."},"serieId":{"type":["string","null"],"format":"uuid","description":"Zugeordnete Seriennummer. `null`, wenn nicht seriengefuehrt."},"ortId":{"type":"string","format":"uuid","description":"Lagerort, auf den eingelagert wurde. Bei gesperrter Ware der Sperrort."},"qmStatus":{"type":"string","enum":["frei","gesperrt","pruefung"],"description":"Pruefzustand der Ware: `frei` verwendbar, `gesperrt` nicht entnehmbar, `pruefung` in der Eingangspruefung. Die Datenbank erzwingt diese drei Werte."},"bestellPosition":{"type":["integer","null"],"description":"Nummer der bedienten Bestellposition. `null`, wenn ohne Bestellbezug gebucht."},"bewegungId":{"type":["string","null"],"format":"uuid","description":"Die erzeugte Lagerbewegung. `null`, wenn keine geschrieben wurde."}},"required":["id","position","artikelId","bezeichnung","menge","einheit","einzelpreis","gesamtpreis","chargeId","serieId","ortId","qmStatus","bestellPosition","bewegungId"],"additionalProperties":false},"description":"Die Positionen des Belegs, aufsteigend nach `position`."}},"required":["id","belegnummer","lieferantName","lieferantId","bestellnr","bestellungId","eingangsdatum","status","positionsCount","gesamtwert","journalId","notizen","createdAt","positionen"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","belegnummer":"string","lieferantName":"string","lieferantId":"string","bestellnr":"string","bestellungId":"00000000-0000-4000-8000-000000000000","eingangsdatum":"stringxxxx","status":"string","positionsCount":0,"gesamtwert":0,"journalId":"00000000-0000-4000-8000-000000000000","notizen":"string","createdAt":"2026-01-01T12:00:00.000Z","positionen":[{"id":"00000000-0000-4000-8000-000000000000","position":0,"artikelId":"00000000-0000-4000-8000-000000000000","bezeichnung":"string","menge":0,"einheit":"string","einzelpreis":0,"gesamtpreis":0,"chargeId":"00000000-0000-4000-8000-000000000000","serieId":"00000000-0000-4000-8000-000000000000","ortId":"00000000-0000-4000-8000-000000000000","qmStatus":"frei","bestellPosition":0,"bewegungId":"00000000-0000-4000-8000-000000000000"}]}}}},"401":{"description":"Nicht angemeldet"},"404":{"description":"Nicht gefunden. Der Koerper traegt nur `error: \"wareneingang_not_found\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Fehlerschluessel zum Auswerten im Programm, z. B. `wareneingang_not_found`."},"message":{"type":"string","description":"Deutscher Klartext. Fehlt, wenn der Schluessel fuer sich spricht."}},"required":["error"]}}}},"422":{"description":"Fachlich unzulaessig — der Lagerdienst hat die Abfrage abgelehnt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Fehlerschluessel zum Auswerten im Programm, z. B. `wareneingang_not_found`."},"message":{"type":"string","description":"Deutscher Klartext. Fehlt, wenn der Schluessel fuer sich spricht."}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar oder das Anlegen der Tabellen schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryWareneingangById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen Wareneingang lesen","description":"Ein Wareneingang mit seinen Positionen."}},"/api/v1/inventory/warehouses":{"get":{"responses":{"200":{"description":"Lagerliste — in einer der drei Formen (siehe Beschreibung)","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"address":{"type":["string","null"]},"isActive":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","code","name","address","isActive","createdAt","updatedAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}},"degraded":{"type":"boolean","const":true}},"required":["data","degraded"],"additionalProperties":false},{"type":"object","properties":{"data":{"type":"array","items":{},"maxItems":0},"warning":{"type":"string"}},"required":["data","warning"],"additionalProperties":false}]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","address":"string","isActive":true,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbankfehler — `message` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryWarehouses","tags":["Inventory"],"parameters":[],"summary":"List warehouses","description":"Alle Lager des Mandanten. Ungeblättert und ohne Filter. ACHTUNG: Der Aufruf antwortet in DREI Formen, alle mit Status 200 — (1) normal, (2) mit `degraded: true`, wenn die Tabelle von der erwarteten Struktur abweicht und ein roher Notlese-Zweig einspringt (die Feldnamen stammen dann direkt aus der Tabelle), (3) `{ data: [], warning: \"<Fehlertext>\" }`, wenn Tabelle oder Datenbank ganz fehlen. Eine leere Liste ist hier also KEIN Beweis dafür, dass es keine Lager gibt — dafür muss `warning` geprüft werden."},"post":{"responses":{"201":{"description":"Lager angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"address":{"type":["string","null"]},"isActive":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","code","name","address","isActive","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","address":"string","isActive":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbankfehler — `error` trägt den rohen Fehlertext, darunter auch die Verletzung der Eindeutigkeit des `code`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1InventoryWarehouses","tags":["Inventory"],"parameters":[],"summary":"Create warehouse","description":"Legt ein Lager an. Ohne `code` wird ein Kürzel `WH-<8 Zeichen>` aus einer Zufalls-UUID erzeugt. Der `code` ist eindeutig — ein zweites Lager mit demselben Kürzel scheitert am Datenbank-Index und kommt als 503 zurück, nicht als 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":40},"name":{"type":"string","minLength":1,"maxLength":255},"address":{"type":"string"},"isActive":{"type":"boolean","default":true}},"required":["name"]},"example":{"code":"string","name":"string","address":"string","isActive":true}}}}}},"/api/v1/inventory/warehouses/locations":{"get":{"responses":{"200":{"description":"Lagerplätze, neueste zuerst — Felder nicht zugesagt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbankfehler — `message` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryWarehousesLocations","tags":["Inventory"],"parameters":[],"summary":"List inventory locations","description":"Lagerplätze (Bestand je Lager/Produkt/Variante), optional gefiltert über `?warehouse=` und `?product=`. Ungeblättert. Die Antwort reicht die Zeilen unverändert durch (`SELECT *`), deshalb sind hier KEINE Feldnamen zugesagt: die Tabelle wird von dieser Datei bei Bedarf selbst angelegt und nachträglich um Spalten ergänzt, ihre Form hängt also vom Alter des Mandanten ab."}},"/api/v1/inventory/warehouses/transfer":{"post":{"responses":{"200":{"description":"Beide Zeilen geschrieben. `transferred` spiegelt nur die angefragte Menge zurück — es ist keine Messung dessen, was verfügbar war.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"transferred":{"type":"integer"}},"required":["success","transferred"],"additionalProperties":false},"example":{"success":true,"transferred":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbankfehler — `error` trägt den rohen Fehlertext. Kann auch nach der ERSTEN erfolgreichen Einfügung auftreten (siehe Beschreibung).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1InventoryWarehousesTransfer","tags":["Inventory"],"parameters":[],"summary":"Transfer stock between warehouses","description":"Verbucht eine Umlagerung von einem Lager ins andere. WIE DAS WIRKLICH PASSIERT: Es wird nichts umgebucht, sondern es werden ZWEI NEUE Zeilen angelegt — eine mit `+qty` beim Zielort, eine mit `-qty` beim Quellort. Der Bestand eines Lagerplatzes ist damit die SUMME seiner Zeilen, nicht der Wert einer Zeile. Es findet KEINE Bestandsprüfung statt: eine Umlagerung aus einem leeren oder gar nicht vorhandenen Lager gelingt und hinterlässt einen negativen Eintrag. Beide Einfügungen laufen ohne gemeinsame Transaktion — scheitert die zweite, bleibt die erste stehen und es entsteht Bestand aus dem Nichts.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"variantId":{"type":"string","format":"uuid"},"fromWarehouseId":{"type":"string","format":"uuid"},"toWarehouseId":{"type":"string","format":"uuid"},"qty":{"type":"integer","exclusiveMinimum":0}},"required":["productId","fromWarehouseId","toWarehouseId","qty"]},"example":{"productId":"00000000-0000-4000-8000-000000000000","variantId":"00000000-0000-4000-8000-000000000000","fromWarehouseId":"00000000-0000-4000-8000-000000000000","toWarehouseId":"00000000-0000-4000-8000-000000000000","qty":1}}}}}},"/api/v1/inventory/warehouses/reorder-suggestions":{"get":{"responses":{"200":{"description":"Vorschläge, knappster Bestand zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"suggestions":{"type":"array","items":{"type":"object","properties":{"warehouse_id":{"type":"string","format":"uuid"},"product_id":{"type":"string","format":"uuid"},"variant_id":{"type":["string","null"],"format":"uuid"},"current_qty":{"type":"integer"},"reorder_point":{"type":"integer"},"suggested_qty":{"type":["integer","null"]},"warehouse_name":{"type":"string"}},"required":["warehouse_id","product_id","variant_id","current_qty","reorder_point","suggested_qty","warehouse_name"],"additionalProperties":false}}},"required":["suggestions"],"additionalProperties":false},"example":{"suggestions":[{"warehouse_id":"00000000-0000-4000-8000-000000000000","product_id":"00000000-0000-4000-8000-000000000000","variant_id":"00000000-0000-4000-8000-000000000000","current_qty":0,"reorder_point":0,"suggested_qty":0,"warehouse_name":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbankfehler — `message` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryWarehousesReorder-suggestions","tags":["Inventory"],"parameters":[],"summary":"Reorder suggestions","description":"Lagerplätze, die ihren Meldebestand erreicht oder unterschritten haben, zusammen mit dem Lagernamen. Nur Plätze mit einem gesetzten Meldebestand (> 0) werden betrachtet — wo keiner hinterlegt ist, entsteht nie ein Vorschlag, egal wie leer das Lager ist. Die Schlüssel dieser Antwort sind snake_case, anders als in den übrigen Antworten dieser Datei."}},"/api/v1/inventory/warehouses/locations/{id}":{"patch":{"responses":{"200":{"description":"Die geänderte Zeile — Felder nicht zugesagt","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Lagerplatz nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbankfehler — `message` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"patchApiV1InventoryWarehousesLocationsById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update inventory location","description":"Ändert Bestand, Meldebestand oder Nachbestellmenge EINES Lagerplatzes. Weggelassene Felder bleiben stehen. Setzt den Bestand ABSOLUT — anders als /transfer, das Zeilen addiert; beide schreiben auf dieselbe Tabelle. Die Antwort ist die rohe Zeile (`RETURNING *`), deshalb sind hier keine Feldnamen zugesagt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"qty_on_hand":{"type":"integer"},"reorder_point":{"type":"integer"},"reorder_qty":{"type":"integer"}}},"example":{"qty_on_hand":0,"reorder_point":0,"reorder_qty":0}}}}}},"/api/v1/inventory/warehouses/{id}":{"get":{"responses":{"200":{"description":"Das Lager","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"address":{"type":["string","null"]},"isActive":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","code","name","address","isActive","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","address":"string","isActive":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Nicht angemeldet"},"404":{"description":"Lager nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbankfehler — `error` trägt den rohen Fehlertext. Auch eine unlesbare `:id` landet hier: sie wird nicht als UUID geprüft, sondern läuft in den SQL-Fehler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryWarehousesById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelnes Lager","description":"Ein Lager anhand seiner id. Abgeschaltete Lager (`isActive: false`) werden ebenfalls geliefert — die Abfrage filtert nicht danach."},"put":{"responses":{"200":{"description":"Das geänderte Lager","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"address":{"type":["string","null"]},"isActive":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","code","name","address","isActive","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","address":"string","isActive":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Nicht angemeldet"},"404":{"description":"Lager nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbankfehler — `error` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1InventoryWarehousesById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lager ändern","description":"Ändert ein Lager. Trotz PUT ist der Aufruf TEILWEISE ersetzend: alle Felder sind optional, weggelassene bleiben stehen. Über `isActive` lässt sich ein abgeschaltetes Lager hier auch wieder einschalten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":40},"name":{"type":"string","minLength":1,"maxLength":255},"address":{"type":"string"},"isActive":{"type":"boolean","default":true}}},"example":{"code":"string","name":"string","address":"string","isActive":true}}}}},"delete":{"responses":{"200":{"description":"Quittung — sagt nichts darüber aus, ob die id existierte","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Nicht angemeldet"},"503":{"description":"Datenbankfehler — `error` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1InventoryWarehousesById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lager abschalten","description":"Schaltet ein Lager ab. LÖSCHT NICHT: der Datensatz bleibt vollständig stehen und wird nur auf `isActive: false` gesetzt — ein PUT auf dieselbe id schaltet ihn wieder ein. Vorhandene Lagerplätze und Bestände bleiben unberührt und erscheinen weiterhin unter /locations. `{ ok: true }` ist eine Konstante: der Handler prüft nicht, ob die id überhaupt existiert — ein unbekanntes Lager liefert dieselbe Antwort. Der bisher dokumentierte 404 kann hier deshalb gar nicht entstehen und ist entfernt."}},"/api/v1/inventory/stock":{"get":{"responses":{"200":{"description":"Bestandszeilen. `degraded: true` heisst: die Zeilen kamen ueber den driftfesten Notweg, nicht ueber das ORM. Ist `warning` gesetzt, ist `data` leer, WEIL Tabelle oder Datenbank fehlten — nicht, weil es keinen Bestand gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}},"degraded":{"type":"boolean"},"warning":{"type":"string"}},"required":["data"]},"example":{"data":[{}],"degraded":true,"warning":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1InventoryStock","tags":["Inventory"],"parameters":[{"in":"query","name":"articleId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"warehouseId","schema":{"type":"string","format":"uuid"}}],"summary":"Current stock levels","description":"LH-063 — current per-warehouse quantities"}},"/api/v1/inventory/stock/warehouses":{"get":{"responses":{"200":{"description":"Alle Lagerorte des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"address":{"type":["string","null"]},"isActive":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","code","name","address","isActive","createdAt","updatedAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","code":"string","name":"string","address":"string","isActive":true,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1InventoryStockWarehouses","tags":["Inventory"],"parameters":[],"summary":"Lagerorte auflisten","description":"Liest alle Zeilen aus `inventory_warehouses` des Mandanten — ohne Filter, ohne Sortierung und ohne Blaetterung. Stillgelegte Lagerorte werden nicht ausgeblendet; ob einer noch benutzt wird, steht in `isActive`. Faellt die Datenbank aus, antwortet die Route mit 503 und der Meldung des Treibers."},"post":{"responses":{"201":{"description":"Der angelegte Lagerort","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"address":{"type":["string","null"]},"isActive":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","code","name","address","isActive","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","code":"string","name":"string","address":"string","isActive":true,"createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1InventoryStockWarehouses","tags":["Inventory"],"parameters":[],"summary":"Lagerort anlegen","description":"Fuegt eine Zeile in `inventory_warehouses` ein und gibt sie zurueck (201). `code` und `name` sind Pflicht, `isActive` steht ohne Angabe auf true. Auf `code` liegt in der Datenbank eine Eindeutigkeits-Bedingung; ein bereits vergebener Wert laeuft in den Fehlerzweig und ergibt 503, nicht 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"address":{"type":"string"},"isActive":{"type":"boolean","default":true}},"required":["code","name"]},"example":{"code":"string","name":"string","address":"string","isActive":true}}}}}},"/api/v1/inventory/movements":{"get":{"responses":{"200":{"description":"DREI FORMEN. Im Normalfall die Bewegungen mit allen Feldern. Fehlen dem Mandanten Spalten (Schema-Drift), antwortet der Notweg mit `degraded: true` und den Feldern, die seine Tabelle wirklich hat. Fehlt die Tabelle ganz, kommt eine leere Liste mit `warning` — bewusst 200 statt 503, damit die Oberflaeche eine leere Tabelle zeigt statt „API nicht erreichbar\".","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Bewegung (UUID, vom Server vergeben)"},"articleId":{"type":"string","format":"uuid","description":"Artikel, den die Bewegung betrifft. Pflichtfeld."},"type":{"type":"string","enum":["IN","OUT","TRANSFER","ADJUST"],"description":"Art der Bewegung: `IN` Zugang, `OUT` Abgang, `TRANSFER` Umlagerung, `ADJUST` Korrektur aus der Inventur."},"quantity":{"type":"string","description":"Menge als Dezimalzeichenkette mit drei Nachkommastellen (z. B. `\"5.000\"`). Sie steht so da, wie sie gebucht wurde — das Vorzeichen der Bestandswirkung ergibt sich aus `type`, nicht aus diesem Feld."},"warehouseId":{"type":["string","null"],"format":"uuid","description":"Betroffenes Lager; bei `TRANSFER` das QUELL-Lager. `null`, wenn ohne Lagerbezug gebucht."},"toWarehouseId":{"type":["string","null"],"format":"uuid","description":"Ziel-Lager. Nur bei `TRANSFER` gesetzt, sonst `null`."},"lotId":{"type":["string","null"],"format":"uuid","description":"Betroffene Charge. `null`, wenn nicht chargengefuehrt."},"serialId":{"type":["string","null"],"format":"uuid","description":"Betroffene Seriennummer. `null`, wenn nicht seriengefuehrt."},"referenceType":{"type":["string","null"],"maxLength":40,"description":"Art des ausloesenden Belegs, z. B. `delivery`. `null` bei Handbuchung."},"referenceId":{"type":["string","null"],"format":"uuid","description":"Kennung des ausloesenden Belegs. `null` bei Handbuchung."},"reason":{"type":["string","null"],"description":"Freitext-Begruendung. `null`, wenn keine erfasst ist."},"userId":{"type":["string","null"],"format":"uuid","description":"Wer gebucht hat. `null` bei maschinellen Buchungen und Altdaten."},"createdAt":{"type":"string","format":"date-time","description":"Buchungszeitpunkt als ISO-8601-Zeitstempel in UTC."}},"required":["id","articleId","type","quantity","warehouseId","toWarehouseId","lotId","serialId","referenceType","referenceId","reason","userId","createdAt"],"additionalProperties":false},"description":"Die gefundenen Bewegungen, neueste zuerst, geblaettert ueber `limit`/`offset`."}},"required":["data"],"additionalProperties":false},{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Zeilen, wie die Tabelle des Mandanten sie hergibt — Spaltennamen in camelCase umgeschrieben. Welche Felder dabei sind, haengt vom Stand seiner Tabelle ab."},"degraded":{"type":"boolean","const":true,"description":"Immer `true`. Kennzeichnet, dass der Notweg geantwortet hat und Felder fehlen koennen."}},"required":["data","degraded"],"additionalProperties":false},{"type":"object","properties":{"data":{"type":"array","items":{},"maxItems":0,"description":"Immer leer. Es konnte nichts gelesen werden."},"warning":{"type":"string","description":"Die Fehlermeldung, die zum Leerlauf gefuehrt hat — meist „relation … does not exist\"."}},"required":["data","warning"],"additionalProperties":false}]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","articleId":"00000000-0000-4000-8000-000000000000","type":"IN","quantity":"string","warehouseId":"00000000-0000-4000-8000-000000000000","toWarehouseId":"00000000-0000-4000-8000-000000000000","lotId":"00000000-0000-4000-8000-000000000000","serialId":"00000000-0000-4000-8000-000000000000","referenceType":"string","referenceId":"00000000-0000-4000-8000-000000000000","reason":"string","userId":"00000000-0000-4000-8000-000000000000","createdAt":"2026-01-01T12:00:00.000Z"}]}}}},"400":{"description":"Ungueltige Abfrageparameter"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Ein echter Abfragefehler. Ausfall und Schema-Drift landen NICHT hier, sondern in den Rueckfallformen von 200.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Die durchgereichte Fehlermeldung der Datenbankschicht (englisch)."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryMovements","tags":["Inventory"],"parameters":[{"in":"query","name":"articleId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"type","schema":{"type":"string","enum":["IN","OUT","TRANSFER","ADJUST"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":500,"default":100}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List inventory movements","description":"AK-405 — IN/OUT/TRANSFER/ADJUST audit trail"},"post":{"responses":{"201":{"description":"Die gebuchte Bewegung, unverpackt — kein `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Bewegung (UUID, vom Server vergeben)"},"articleId":{"type":"string","format":"uuid","description":"Artikel, den die Bewegung betrifft. Pflichtfeld."},"type":{"type":"string","enum":["IN","OUT","TRANSFER","ADJUST"],"description":"Art der Bewegung: `IN` Zugang, `OUT` Abgang, `TRANSFER` Umlagerung, `ADJUST` Korrektur aus der Inventur."},"quantity":{"type":"string","description":"Menge als Dezimalzeichenkette mit drei Nachkommastellen (z. B. `\"5.000\"`). Sie steht so da, wie sie gebucht wurde — das Vorzeichen der Bestandswirkung ergibt sich aus `type`, nicht aus diesem Feld."},"warehouseId":{"type":["string","null"],"format":"uuid","description":"Betroffenes Lager; bei `TRANSFER` das QUELL-Lager. `null`, wenn ohne Lagerbezug gebucht."},"toWarehouseId":{"type":["string","null"],"format":"uuid","description":"Ziel-Lager. Nur bei `TRANSFER` gesetzt, sonst `null`."},"lotId":{"type":["string","null"],"format":"uuid","description":"Betroffene Charge. `null`, wenn nicht chargengefuehrt."},"serialId":{"type":["string","null"],"format":"uuid","description":"Betroffene Seriennummer. `null`, wenn nicht seriengefuehrt."},"referenceType":{"type":["string","null"],"maxLength":40,"description":"Art des ausloesenden Belegs, z. B. `delivery`. `null` bei Handbuchung."},"referenceId":{"type":["string","null"],"format":"uuid","description":"Kennung des ausloesenden Belegs. `null` bei Handbuchung."},"reason":{"type":["string","null"],"description":"Freitext-Begruendung. `null`, wenn keine erfasst ist."},"userId":{"type":["string","null"],"format":"uuid","description":"Wer gebucht hat. `null` bei maschinellen Buchungen und Altdaten."},"createdAt":{"type":"string","format":"date-time","description":"Buchungszeitpunkt als ISO-8601-Zeitstempel in UTC."}},"required":["id","articleId","type","quantity","warehouseId","toWarehouseId","lotId","serialId","referenceType","referenceId","reason","userId","createdAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","articleId":"00000000-0000-4000-8000-000000000000","type":"IN","quantity":"string","warehouseId":"00000000-0000-4000-8000-000000000000","toWarehouseId":"00000000-0000-4000-8000-000000000000","lotId":"00000000-0000-4000-8000-000000000000","serialId":"00000000-0000-4000-8000-000000000000","referenceType":"string","referenceId":"00000000-0000-4000-8000-000000000000","reason":"string","userId":"00000000-0000-4000-8000-000000000000","createdAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"ZWEI FORMEN, weil zwei Stellen ablehnen: der Handler bei `TRANSFER` ohne `toWarehouseId` (fester Text im Feld `error`), der Eingabe-Validator bei allem anderen (`success: false` samt Zod-Befunden).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"toWarehouseId required for TRANSFER","description":"Fester Text. Eine Umlagerung ohne Ziel-Lager wird nicht gebucht."}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false`. Daran ist die Antwort des Validators erkennbar."},"error":{"type":"object","additionalProperties":{},"description":"Der Zod-Fehler als Objekt; die Einzelbefunde stehen in seiner Liste `issues`."}},"required":["success","error"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar oder das Buchen schlug fehl. Anders als bei GET / gibt es hier KEINEN Notweg — eine Buchung wird nie stillschweigend verworfen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Die durchgereichte Fehlermeldung (englisch), NICHT eine feste Kennung. „DB unavailable\" bedeutet: Datenbank nicht erreichbar."}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1InventoryMovements","tags":["Inventory"],"parameters":[],"summary":"Record an inventory movement","description":"AK-405 — atomically records a movement and updates the running stock total. TRANSFER decrements the source warehouse and increments toWarehouseId.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"articleId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["IN","OUT","TRANSFER","ADJUST"]},"quantity":{"type":"number"},"warehouseId":{"type":"string","format":"uuid"},"toWarehouseId":{"type":"string","format":"uuid"},"lotId":{"type":"string","format":"uuid"},"serialId":{"type":"string","format":"uuid"},"referenceType":{"type":"string"},"referenceId":{"type":"string","format":"uuid"},"reason":{"type":"string"}},"required":["articleId","type","quantity"]},"example":{"articleId":"00000000-0000-4000-8000-000000000000","type":"IN","quantity":0,"warehouseId":"00000000-0000-4000-8000-000000000000","toWarehouseId":"00000000-0000-4000-8000-000000000000","lotId":"00000000-0000-4000-8000-000000000000","serialId":"00000000-0000-4000-8000-000000000000","referenceType":"string","referenceId":"00000000-0000-4000-8000-000000000000","reason":"string"}}}}}},"/api/v1/inventory/serials":{"get":{"responses":{"200":{"description":"Die gefundenen Seriennummern.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Datensatzes (UUID, vom Server vergeben)"},"articleId":{"type":"string","format":"uuid","description":"Artikel, zu dem diese Seriennummer gehoert. Pflichtfeld."},"serialNumber":{"type":"string","minLength":1,"maxLength":150,"description":"Die Seriennummer selbst, wie sie am Geraet steht."},"warehouseId":{"type":["string","null"],"format":"uuid","description":"Lager, in dem das Stueck liegt. `null`, solange es keinem zugeordnet ist."},"status":{"type":"string","enum":["in_stock","sold","scrapped","reserved"],"description":"Zustand des Stuecks: `in_stock` am Lager, `reserved` vorgemerkt, `sold` ausgeliefert, `scrapped` verschrottet. Voreinstellung beim Anlegen ist `in_stock`."},"lotId":{"type":["string","null"],"format":"uuid","description":"Charge, aus der das Stueck stammt. `null`, wenn nicht chargengefuehrt."},"deliveredAt":{"type":["string","null"],"format":"date-time","description":"Auslieferdatum als ISO-8601-Zeitstempel in UTC. `null`, solange nicht geliefert."},"notes":{"type":["string","null"],"description":"Freitext-Notiz. `null`, wenn keine erfasst ist."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."}},"required":["id","articleId","serialNumber","warehouseId","status","lotId","deliveredAt","notes","createdAt","updatedAt"],"additionalProperties":false},"description":"Die gefundenen Seriennummern, ungeblaettert und ohne feste Sortierung. Die Abfrageparameter `articleId` und `status` grenzen sie ein."}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","articleId":"00000000-0000-4000-8000-000000000000","serialNumber":"string","warehouseId":"00000000-0000-4000-8000-000000000000","status":"in_stock","lotId":"00000000-0000-4000-8000-000000000000","deliveredAt":"2026-01-01T12:00:00.000Z","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar oder die Abfrage schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Die durchgereichte Fehlermeldung der Datenbankschicht (englisch)."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"getApiV1InventorySerials","tags":["Inventory"],"parameters":[],"summary":"List serial numbers","description":"Listet die Seriennummern des Mandanten. Optional eingrenzbar ueber die Abfrageparameter `articleId` und `status`; ohne beide kommt der ganze Bestand."},"post":{"responses":{"201":{"description":"Die angelegte Zeile, unverpackt — kein `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Datensatzes (UUID, vom Server vergeben)"},"articleId":{"type":"string","format":"uuid","description":"Artikel, zu dem diese Seriennummer gehoert. Pflichtfeld."},"serialNumber":{"type":"string","minLength":1,"maxLength":150,"description":"Die Seriennummer selbst, wie sie am Geraet steht."},"warehouseId":{"type":["string","null"],"format":"uuid","description":"Lager, in dem das Stueck liegt. `null`, solange es keinem zugeordnet ist."},"status":{"type":"string","enum":["in_stock","sold","scrapped","reserved"],"description":"Zustand des Stuecks: `in_stock` am Lager, `reserved` vorgemerkt, `sold` ausgeliefert, `scrapped` verschrottet. Voreinstellung beim Anlegen ist `in_stock`."},"lotId":{"type":["string","null"],"format":"uuid","description":"Charge, aus der das Stueck stammt. `null`, wenn nicht chargengefuehrt."},"deliveredAt":{"type":["string","null"],"format":"date-time","description":"Auslieferdatum als ISO-8601-Zeitstempel in UTC. `null`, solange nicht geliefert."},"notes":{"type":["string","null"],"description":"Freitext-Notiz. `null`, wenn keine erfasst ist."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."}},"required":["id","articleId","serialNumber","warehouseId","status","lotId","deliveredAt","notes","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","articleId":"00000000-0000-4000-8000-000000000000","serialNumber":"string","warehouseId":"00000000-0000-4000-8000-000000000000","status":"in_stock","lotId":"00000000-0000-4000-8000-000000000000","deliveredAt":"2026-01-01T12:00:00.000Z","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar oder das INSERT schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Die durchgereichte Fehlermeldung (englisch), NICHT eine feste Kennung. „DB unavailable\" bedeutet: Datenbank nicht erreichbar."}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1InventorySerials","tags":["Inventory"],"parameters":[],"summary":"Seriennummer anlegen","description":"Legt eine einzelne Seriennummer an. Ohne Angabe steht sie auf `in_stock`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"articleId":{"type":"string","format":"uuid"},"serialNumber":{"type":"string","minLength":1},"warehouseId":{"type":"string","format":"uuid"},"lotId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["in_stock","sold","scrapped","reserved"],"default":"in_stock"},"deliveredAt":{"type":"string","format":"date-time"},"notes":{"type":"string"}},"required":["articleId","serialNumber"]},"example":{"articleId":"00000000-0000-4000-8000-000000000000","serialNumber":"string","warehouseId":"00000000-0000-4000-8000-000000000000","lotId":"00000000-0000-4000-8000-000000000000","status":"in_stock","deliveredAt":"2026-01-01T12:00:00.000Z","notes":"string"}}}}}},"/api/v1/inventory/serials/{id}":{"put":{"responses":{"200":{"description":"Die geaenderte Zeile im Zustand nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Datensatzes (UUID, vom Server vergeben)"},"articleId":{"type":"string","format":"uuid","description":"Artikel, zu dem diese Seriennummer gehoert. Pflichtfeld."},"serialNumber":{"type":"string","minLength":1,"maxLength":150,"description":"Die Seriennummer selbst, wie sie am Geraet steht."},"warehouseId":{"type":["string","null"],"format":"uuid","description":"Lager, in dem das Stueck liegt. `null`, solange es keinem zugeordnet ist."},"status":{"type":"string","enum":["in_stock","sold","scrapped","reserved"],"description":"Zustand des Stuecks: `in_stock` am Lager, `reserved` vorgemerkt, `sold` ausgeliefert, `scrapped` verschrottet. Voreinstellung beim Anlegen ist `in_stock`."},"lotId":{"type":["string","null"],"format":"uuid","description":"Charge, aus der das Stueck stammt. `null`, wenn nicht chargengefuehrt."},"deliveredAt":{"type":["string","null"],"format":"date-time","description":"Auslieferdatum als ISO-8601-Zeitstempel in UTC. `null`, solange nicht geliefert."},"notes":{"type":["string","null"],"description":"Freitext-Notiz. `null`, wenn keine erfasst ist."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."}},"required":["id","articleId","serialNumber","warehouseId","status","lotId","deliveredAt","notes","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","articleId":"00000000-0000-4000-8000-000000000000","serialNumber":"string","warehouseId":"00000000-0000-4000-8000-000000000000","status":"in_stock","lotId":"00000000-0000-4000-8000-000000000000","deliveredAt":"2026-01-01T12:00:00.000Z","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Keine Seriennummer mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das UPDATE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Die durchgereichte Fehlermeldung (englisch), NICHT eine feste Kennung. „DB unavailable\" bedeutet: Datenbank nicht erreichbar."}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1InventorySerialsById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Seriennummer aendern","description":"Aendert Status, Lager oder Notiz einer Seriennummer. Nicht mitgeschickte Felder bleiben unveraendert; `updatedAt` setzt der Server selbst.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["in_stock","sold","scrapped","reserved"]},"warehouseId":{"type":"string","format":"uuid"},"notes":{"type":"string"}}},"example":{"status":"in_stock","warehouseId":"00000000-0000-4000-8000-000000000000","notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzdaten.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` — auch dann, wenn die Kennung zu keiner Zeile passte. Der Handler prueft die Trefferzahl nicht."}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar oder das DELETE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Die durchgereichte Fehlermeldung (englisch), NICHT eine feste Kennung. „DB unavailable\" bedeutet: Datenbank nicht erreichbar."}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1InventorySerialsById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Seriennummer loeschen","description":"Loescht eine Seriennummer endgueltig (kein Soft-Delete). Der Handler wertet die Trefferzahl nicht aus: eine unbekannte Kennung ergibt dieselbe Quittung wie ein echter Treffer."}},"/api/v1/inventory/forecast":{"get":{"responses":{"200":{"description":"ZWEI FORMEN. Wurde mindestens ein Artikel betrachtet, kommen die Vorschlaege mit vollem `meta`. Gab es keinen, kommt eine leere Liste mit VERKUERZTEM `meta` (`source: \"none\"`, ohne `lookbackDays`, `critical`, `low`, `ok`).\n\nDer Endpunkt antwortet auch OHNE eingerichtetes Sprachmodell: dann rechnet die feste Regel, und `meta.source` steht auf `rule-based`. Die Zahlen sind in beiden Faellen dieselben — das Sprachmodell aendert nur `trendNote`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":"string","format":"uuid","description":"Kennung des betrachteten Artikels (UUID)"},"sku":{"type":"string","description":"Artikelnummer."},"name":{"type":"string","description":"Bezeichnung des Artikels."},"currentStock":{"type":"number","description":"Aktueller Bestand, ueber alle Lager summiert. `0`, wenn nichts gebucht ist."},"minStock":{"type":"number","description":"Gepflegter Mindestbestand. `0`, wenn keiner hinterlegt ist."},"avgDailyOut":{"type":"number","minimum":0,"description":"Durchschnittlicher Abgang je Tag im Betrachtungszeitraum (`lookbackDays`)."},"daysOfStock":{"type":["integer","null"],"description":"Reichweite in ganzen Tagen beim bisherigen Verbrauch. `null` heisst: es gab keine Abgaenge, eine Reichweite ist also nicht berechenbar — NICHT „null Tage\"."},"urgency":{"type":"string","enum":["critical","low","ok"],"description":"Dringlichkeit: `critical`, wenn der Bestand den Mindestbestand erreicht oder unterschritten hat; `low` unterhalb des Anderthalbfachen; sonst `ok`."},"recommendedOrder":{"type":"number","minimum":0,"description":"Empfohlene Bestellmenge — der Bedarf von 30 Tagen abzueglich der freien Reserve."},"trendNote":{"type":"string","description":"Ein bis zwei Saetze Handlungsempfehlung auf Deutsch. Ist ein Sprachmodell eingerichtet, stammt der Satz von ihm, sonst aus der festen Regel."}},"required":["articleId","sku","name","currentStock","minStock","avgDailyOut","daysOfStock","urgency","recommendedOrder","trendNote"],"additionalProperties":false},"description":"Die Vorschlaege, ein Eintrag je betrachtetem Artikel."},"meta":{"type":"object","properties":{"analysedArticles":{"type":"integer","minimum":0,"description":"Anzahl betrachteter Artikel."},"lookbackDays":{"type":"integer","minimum":7,"maximum":365,"description":"Laenge des ausgewerteten Zeitraums in Tagen."},"source":{"type":"string","minLength":1,"description":"Woher die Empfehlungstexte stammen: die Kennung des Sprachmodells, oder `rule-based`, wenn keines eingerichtet ist."},"critical":{"type":"integer","minimum":0,"description":"Wie viele Vorschlaege auf `critical` stehen."},"low":{"type":"integer","minimum":0,"description":"Wie viele Vorschlaege auf `low` stehen."},"ok":{"type":"integer","minimum":0,"description":"Wie viele Vorschlaege auf `ok` stehen."}},"required":["analysedArticles","lookbackDays","source","critical","low","ok"],"additionalProperties":false,"description":"Kennzahlen zur Auswertung."}},"required":["data","meta"],"additionalProperties":false},{"type":"object","properties":{"data":{"type":"array","items":{},"maxItems":0,"description":"Immer leer."},"meta":{"type":"object","properties":{"analysedArticles":{"type":"number","const":0,"description":"Immer `0`."},"source":{"type":"string","const":"none","description":"Fester Wert. Es lief weder eine Regel noch ein Sprachmodell."}},"required":["analysedArticles","source"],"additionalProperties":false,"description":"Verkuerzte Kennzahlen — die uebrigen Felder fehlen hier."}},"required":["data","meta"],"additionalProperties":false}]},"example":{"data":[{"articleId":"00000000-0000-4000-8000-000000000000","sku":"string","name":"string","currentStock":0,"minStock":0,"avgDailyOut":0,"daysOfStock":0,"urgency":"critical","recommendedOrder":0,"trendNote":"string"}],"meta":{"analysedArticles":0,"lookbackDays":7,"source":"string","critical":0,"low":0,"ok":0}}}}},"400":{"description":"Ungueltige Abfrageparameter"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar oder eine der drei Abfragen schlug fehl. Ein Ausfall des Sprachmodells fuehrt NICHT hierher — er faellt still auf die Regel zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Die durchgereichte Fehlermeldung der Datenbankschicht (englisch)."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryForecast","tags":["Inventory","ai"],"parameters":[{"in":"query","name":"articleId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":50,"default":20}},{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":7,"maximum":365,"default":90}}],"summary":"AI inventory forecast & reorder suggestions","description":"W24-E — analyses movement history and returns reorder urgency + recommendations. Uses claude-haiku-4-5 when available, falls back to rule-based analysis."}},"/api/v1/inventory":{"get":{"responses":{"200":{"description":"Der Wegweiser. Der Handler setzt keinen Statuscode, `c.json(...)` heisst also immer 200 — es gibt in diesem Handler keinen Fehlerfall.","content":{"application/json":{"schema":{"type":"object","properties":{"module":{"type":"string","const":"inventory"},"endpoints":{"type":"array","items":{"type":"string"}},"lh":{"type":"string"},"ak":{"type":"array","items":{"type":"string"}}},"required":["module","endpoints","lh","ak"],"additionalProperties":false},"example":{"module":"inventory","endpoints":["string"],"lh":"string","ak":["string"]}}}},"401":{"description":"Nicht aus diesem Handler, sondern aus `authMiddleware`: der Router hängt unter der `api`-Unter-App (index.ts, `api.route('/inventory', inventoryRoutes)`), und die trägt `authMiddleware` auf `*`. Ohne gültige Anmeldung kommt der Aufruf hier nie an."}},"operationId":"getApiV1Inventory","tags":["Inventory"],"parameters":[],"summary":"Wegweiser des Lager-Moduls","description":"Nennt die Unter-Bereiche des Lager-Moduls. KEINE Lagerdaten — die Antwort ist eine feste Liste von Pfad-Bausteinen (`/articles`, `/warehouses`, `/stock`, `/movements`, `/serials`, `/forecast`, `/wareneingang`) plus den Kennungen `lh` und `ak`. Sie hängt nicht am Mandanten und nicht an der Datenbank, ist also für „gibt es das Modul?\" brauchbar und für „was liegt im Lager?\" nicht.\n\nNICHT IN DER LISTE, aber erreichbar: `/lots` und `/lots/*` antworten für jede Methode mit 410 und verweisen auf `/api/v1/lot-tracking/lots` (siehe Kommentar oben). Sie stehen bewusst nicht im Wegweiser — abgeräumt ist kein Angebot."}},"/api/v1/products":{"get":{"responses":{"200":{"description":"Artikelliste. `degraded` bzw. `warning` sagen, ob die Zeilen aus dem Normalfall, dem Drift-Notfall oder gar nicht kamen.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"}},"required":["limit","offset"]},"degraded":{"type":"boolean"},"warning":{"type":"string"}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0},"degraded":true,"warning":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Products","tags":["Inventory"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"category","schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"List inventory articles","description":"LH-063 — paginated catalogue of inventory articles"},"post":{"responses":{"201":{"description":"Artikel angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"allowed":{"type":"array","items":{"type":"string"}},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1Products","tags":["Inventory"],"parameters":[],"summary":"Create article","description":"Legt eine Zeile in `inventory_articles` an (201). Wird `categories` mitgeschickt, landet der erste Eintrag zugleich als Primaerkategorie in `category` und die vollstaendige Liste in der Zuordnungstabelle `inventory_article_categories`. Eine bereits vergebene SKU beantwortet die Route mit 409 und `error: \"duplicate_sku\"`. Das Anlegen wird im Aktivitaetsverlauf des Artikels vermerkt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string","maxLength":5000},"category":{"type":"string"},"categories":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0,"default":0},"purchasePrice":{"type":"number","minimum":0,"default":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"trackingType":{"type":"string","enum":["none","lot","serial"],"default":"none"},"minStock":{"type":"number","minimum":0,"default":0},"customFields":{"type":"object","additionalProperties":{}},"ean":{"type":"string","pattern":"^[0-9]*$","maxLength":13},"supplierId":{"type":"string","format":"uuid"},"supplierArticleNo":{"type":"string","maxLength":64},"minOrderQty":{"type":"number","minimum":0}},"required":["sku","name"]},"example":{"sku":"string","name":"string","description":"string","category":"string","categories":["string"],"unit":"string","unitPrice":0,"purchasePrice":0,"taxRate":0,"trackingType":"none","minStock":0,"customFields":{},"supplierId":"00000000-0000-4000-8000-000000000000","supplierArticleNo":"string","minOrderQty":0}}}}}},"/api/v1/products/next-number":{"get":{"responses":{"200":{"description":"Vorschlag. `degraded` heisst: ohne Datenbank geraten.","content":{"application/json":{"schema":{"type":"object","properties":{"suggestion":{"type":"string"},"degraded":{"type":"boolean"}},"required":["suggestion"],"additionalProperties":false},"example":{"suggestion":"string","degraded":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ProductsNext-number","tags":["Inventory"],"parameters":[],"summary":"Suggest next article number","description":"Welle 4 — schlägt die nächste Artikelnummer aus dem Bestand vor"}},"/api/v1/products/{id}":{"get":{"responses":{"200":{"description":"Der Artikel","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden (auch bei ungueltiger Id-Form)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"allowed":{"type":"array","items":{"type":"string"}},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1ProductsById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get article by id","description":"Liest genau einen Artikel und ergaenzt `categories` aus der Zuordnungstabelle `inventory_article_categories`; ist dort nichts hinterlegt, steht die Primaerkategorie als einziger Eintrag darin. Der Einbettungsvektor wird vor dem Senden entfernt. Die Abfrage filtert `deleted_at` NICHT — ein zuvor geloeschter Artikel kommt hier weiterhin. Eine id, die keine UUID ist, ergibt 404 und keinen Serverfehler."},"put":{"responses":{"200":{"description":"Der aktualisierte Artikel","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"422":{"description":"Eigene Regel verletzt — nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"entity_rule_violation"},"violations":{"type":"array","items":{"type":"object","additionalProperties":{}}},"message_de":{"type":"string"}},"required":["error","violations","message_de"]}}}}},"operationId":"putApiV1ProductsById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Artikel aktualisieren","description":"Nimmt denselben Teilkoerper wie PATCH /:id und schreibt nur die mitgeschickten Felder — trotz PUT also kein vollstaendiges Ersetzen. `customFields` wird in das JSONB gemergt: ein nicht mitgeschickter Schluessel bleibt stehen, `null` loescht ihn. Wird `categories` mitgeschickt, ersetzt die Liste die bisherige Zuordnung vollstaendig und ihr erster Eintrag wird zur Primaerkategorie. Vorab pruefen die Eigenen Regeln den Vorgang; bei einer Verletzung kommt 422 und es wird NICHTS geschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string","maxLength":5000},"category":{"type":"string"},"categories":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0,"default":0},"purchasePrice":{"type":"number","minimum":0,"default":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"trackingType":{"type":"string","enum":["none","lot","serial"],"default":"none"},"minStock":{"type":"number","minimum":0,"default":0},"customFields":{"type":"object","additionalProperties":{}},"ean":{"type":"string","pattern":"^[0-9]*$","maxLength":13},"supplierId":{"type":"string","format":"uuid"},"supplierArticleNo":{"type":"string","maxLength":64},"minOrderQty":{"type":"number","minimum":0}}},"example":{"sku":"string","name":"string","description":"string","category":"string","categories":["string"],"unit":"string","unitPrice":0,"purchasePrice":0,"taxRate":0,"trackingType":"none","minStock":0,"customFields":{},"supplierId":"00000000-0000-4000-8000-000000000000","supplierArticleNo":"string","minOrderQty":0}}}}},"patch":{"responses":{"200":{"description":"Der aktualisierte Artikel","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"422":{"description":"Eigene Regel verletzt — nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"entity_rule_violation"},"violations":{"type":"array","items":{"type":"object","additionalProperties":{}}},"message_de":{"type":"string"}},"required":["error","violations","message_de"]}}}}},"operationId":"patchApiV1ProductsById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelne Felder eines Artikels aktualisieren","description":"Gleicher Vertrag wie PUT /:id — derselbe Teilkoerper, dieselbe Logik. Die Route besteht, weil die Maske fuer Eigene Felder ueber `PATCH /api/v1/products/:id` speichert und der `/products`-Alias auf diesen Router zeigt. `customFields` wird in das JSONB gemergt, `null` loescht einen Schluessel. Eigene Regeln koennen den Vorgang mit 422 ablehnen, dann wird nichts geschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string","maxLength":5000},"category":{"type":"string"},"categories":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0,"default":0},"purchasePrice":{"type":"number","minimum":0,"default":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"trackingType":{"type":"string","enum":["none","lot","serial"],"default":"none"},"minStock":{"type":"number","minimum":0,"default":0},"customFields":{"type":"object","additionalProperties":{}},"ean":{"type":"string","pattern":"^[0-9]*$","maxLength":13},"supplierId":{"type":"string","format":"uuid"},"supplierArticleNo":{"type":"string","maxLength":64},"minOrderQty":{"type":"number","minimum":0}}},"example":{"sku":"string","name":"string","description":"string","category":"string","categories":["string"],"unit":"string","unitPrice":0,"purchasePrice":0,"taxRate":0,"trackingType":"none","minStock":0,"customFields":{},"supplierId":"00000000-0000-4000-8000-000000000000","supplierArticleNo":"string","minOrderQty":0}}}}},"delete":{"responses":{"200":{"description":"Geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"deleteApiV1ProductsById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Artikel löschen","description":"Soft-Delete: setzt allein `deleted_at`. Die Zeile bleibt in `inventory_articles` stehen und wird von GET /:id weiterhin geliefert; Bestand, Varianten und Bilder bleiben unberuehrt. Der Vorgang wird im Aktivitaetsverlauf vermerkt. Der Handler prueft NICHT, ob eine Zeile getroffen wurde — eine unbekannte id wird ebenso mit `{ ok: true }` quittiert."}},"/api/v1/products/{id}/variants":{"get":{"responses":{"200":{"description":"Varianten des Artikels","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ProductsByIdVariants","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List product variants","description":"W17-A1 — all variants for a product/article"},"post":{"responses":{"201":{"description":"Variante angelegt","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ProductsByIdVariants","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create product variant","description":"W17-A1 — create a variant for a product/article","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string"},"attributes":{"type":"object","additionalProperties":{},"default":{}},"price":{"type":"number","minimum":0},"stockQty":{"type":"integer","default":0},"barcode":{"type":"string"}}},"example":{"sku":"string","attributes":{},"price":0,"stockQty":0,"barcode":"string"}}}}}},"/api/v1/products/{id}/variants/{variantId}":{"put":{"responses":{"200":{"description":"Die aktualisierte Variante","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"putApiV1ProductsByIdVariantsByVariantId","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"variantId","required":true}],"summary":"Update product variant","description":"W17-A1 — update sku, attributes, price, stock_qty or barcode of a variant","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string"},"attributes":{"type":"object","additionalProperties":{},"default":{}},"price":{"type":"number","minimum":0},"stockQty":{"type":"integer","default":0},"barcode":{"type":"string"}}},"example":{"sku":"string","attributes":{},"price":0,"stockQty":0,"barcode":"string"}}}}},"delete":{"responses":{"200":{"description":"Variante geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"deleteApiV1ProductsByIdVariantsByVariantId","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"variantId","required":true}],"summary":"Delete product variant","description":"W17-A1 — permanently delete a variant"}},"/api/v1/products/{id}/attributes":{"get":{"responses":{"200":{"description":"Merkmale des Artikels","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ProductsByIdAttributes","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List product attributes","description":"W17-A1 — attribute definitions (e.g. Farbe: [Rot, Blau]) for a product"},"post":{"responses":{"201":{"description":"Merkmal angelegt","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ProductsByIdAttributes","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create product attribute","description":"W17-A1 — define an attribute (name + allowed values) for a product","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"values":{"type":"array","items":{"type":"string"},"minItems":1}},"required":["name","values"]},"example":{"name":"string","values":["string"]}}}}}},"/api/v1/products/{id}/image":{"put":{"responses":{"200":{"description":"Der Artikel mit dem neuen Bild","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Article not found"},"413":{"description":"File too large"}},"operationId":"putApiV1ProductsByIdImage","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Upload article image (main or additional)","description":"W2-D — multipart upload. Form field `file` (binary, max 5 MB, jpeg/png/webp). Optional `slot=main` (default) or `slot=N` where N is the additional-image index (0-4). On success returns the updated article row."}},"/api/v1/products/{id}/image/{idx}":{"get":{"responses":{"200":{"description":"Bilddatei (Bytes oder 302 auf eine externe Adresse)","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}}}},"302":{"description":"Das Bild liegt extern — Weiterleitung auf seine Adresse"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Artikel oder Bild nicht vorhanden"},"503":{"description":"Speicher nicht erreichbar"}},"operationId":"getApiV1ProductsByIdImageByIdx","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"idx","required":true}],"summary":"Artikelbild ausliefern","description":"Liefert die Bilddatei als Bytes. `:idx` ist `main` oder 0-4 (Position der Zusatzbilder). Der Mandantenbezug wird beim Lesen aus dem Speicher geprueft. Der Inhaltstyp ergibt sich aus der Dateiendung des Schluessels — es kommen nur JPEG, PNG und WebP in Frage, weil der Upload nichts anderes durchlaesst. Steht in der Spalte bereits eine vollstaendige http(s)-Adresse, liegt das Bild NICHT bei uns: dann kommt statt Bytes eine Weiterleitung (302) dorthin. Die Antwort ist als privat gekennzeichnet und gehoert in keinen geteilten Zwischenspeicher. Fremder Mandant und fehlendes Objekt sind fuer den Aufrufer dasselbe: 404."},"delete":{"responses":{"200":{"description":"Der Artikel ohne das entfernte Bild","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"deleteApiV1ProductsByIdImageByIdx","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"idx","required":true}],"summary":"Remove article image","description":"W2-D — :idx is `main` or 0-4 (additional-image position)."}},"/api/v1/products/{id}/datasheet":{"put":{"responses":{"200":{"description":"Der Artikel mit dem neuen Datenblatt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"413":{"description":"File too large"}},"operationId":"putApiV1ProductsByIdDatasheet","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Upload technical datasheet (PDF, max 25 MB)","description":"W2-D — sets datasheet_url to the storage key returned by @nemix/storage."}},"/api/v1/products/{id}/usage":{"get":{"responses":{"200":{"description":"Verwendungs-Liste","content":{"application/json":{"schema":{"type":"object","properties":{"articleId":{"type":"string"},"quotes":{"type":"array","items":{"type":"object","properties":{"id":{},"number":{},"date":{}}}},"orders":{"type":"array","items":{"type":"object","properties":{"id":{},"number":{},"date":{}}}},"invoices":{"type":"array","items":{"type":"object","properties":{"id":{},"number":{},"date":{}}}}},"required":["articleId","quotes","orders","invoices"],"additionalProperties":false},"example":{"articleId":"string","quotes":[{}],"orders":[{}],"invoices":[{}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Artikel nicht gefunden"}},"operationId":"getApiV1ProductsByIdUsage","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Artikel-Verwendung: Belege mit diesem Artikel","description":"#408/A2 — alle Angebote/Aufträge/Rechnungen deren Positionen articleId=:id enthalten. Tenant-gescopt, Read-only."}},"/api/v1/products/{id}/supplier":{"put":{"responses":{"200":{"description":"Der Artikel mit der neuen Lieferanten-Zuordnung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string"},"unitPrice":{"type":"string"},"purchasePrice":{"type":"string"},"taxRate":{"type":"string"},"language":{"type":"string"},"trackingType":{"type":"string"},"minStock":{"type":"string"},"ean":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"additionalImages":{"type":"array","items":{}},"datasheetUrl":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierArticleNo":{"type":["string","null"]},"minOrderQty":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","sku","name","description","category","unit","unitPrice","purchasePrice","taxRate","language","trackingType","minStock","ean","imageUrl","additionalImages","datasheetUrl","supplierId","supplierArticleNo","minOrderQty","customFields","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","sku":"string","name":"string","description":"string","category":"string","unit":"string","unitPrice":"string","purchasePrice":"string","taxRate":"string","language":"string","trackingType":"string","minStock":"string","ean":"string","imageUrl":"string","additionalImages":[],"datasheetUrl":"string","supplierId":"string","supplierArticleNo":"string","minOrderQty":"string","customFields":{},"deletedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"putApiV1ProductsByIdSupplier","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update supplier metadata for an article","description":"W2-D — body: { supplierId?, supplierArticleNo?, minOrderQty?, ean? }. Pass null on a field to clear it. Returns the updated article row.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"supplierId":{"type":["string","null"],"format":"uuid"},"supplierArticleNo":{"type":["string","null"],"maxLength":64},"minOrderQty":{"type":["number","null"],"minimum":0},"ean":{"type":["string","null"],"maxLength":13}}},"example":{"supplierId":"00000000-0000-4000-8000-000000000000","supplierArticleNo":"string","minOrderQty":0,"ean":"string"}}}}}},"/api/v1/inventory/variants/{variantId}":{"patch":{"responses":{"200":{"description":"Die geaenderte Variante im Zustand nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Variante (UUID, vom Server vergeben)"},"tenantId":{"type":["string","null"],"format":"uuid","description":"Mandant der Zeile. `null` bei Zeilen, die vor der Einfuehrung des Feldes entstanden."},"articleId":{"type":"string","format":"uuid","description":"Artikel, zu dem die Variante gehoert. Pflichtfeld."},"sku":{"type":"string","minLength":1,"maxLength":120,"description":"Artikelnummer der Variante. Mandantenweit eindeutig."},"name":{"type":["string","null"],"maxLength":255,"description":"Bezeichnung der Variante. `null`, wenn keine erfasst ist."},"attributes":{"type":"object","additionalProperties":{},"description":"Unterscheidende Merkmale als freies Objekt, z. B. `{\"color\":\"rot\",\"size\":\"XL\"}`. Leeres Objekt, wenn keine erfasst sind."},"priceOverride":{"type":["string","null"],"description":"Abweichender Preis als Dezimalzeichenkette mit zwei Nachkommastellen (z. B. `\"19.90\"`). `null` heisst: es gilt der Preis des Artikels."},"stockQty":{"type":"string","description":"Bestand als Dezimalzeichenkette mit drei Nachkommastellen (z. B. `\"12.000\"`)."},"barcode":{"type":["string","null"],"maxLength":100,"description":"EAN/Barcode der Variante. `null`, wenn keiner erfasst ist."},"imageUrl":{"type":["string","null"],"description":"Speicherschluessel oder Adresse des Variantenbildes. `null`, wenn keines hinterlegt ist."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."}},"required":["id","tenantId","articleId","sku","name","attributes","priceOverride","stockQty","barcode","imageUrl","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"00000000-0000-4000-8000-000000000000","articleId":"00000000-0000-4000-8000-000000000000","sku":"string","name":"string","attributes":{},"priceOverride":"string","stockQty":"string","barcode":"string","imageUrl":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Variante mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das UPDATE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Die durchgereichte Fehlermeldung (englisch), NICHT eine feste Kennung. „DB unavailable\" bedeutet: Datenbank nicht erreichbar."}},"required":["error"],"additionalProperties":false}}}}},"operationId":"patchApiV1InventoryVariantsByVariantId","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"variantId","required":true}],"summary":"Update a product variant","description":"Aendert eine Variante ueber ihre eigene Kennung. Nicht mitgeschickte Felder bleiben unveraendert; `updatedAt` setzt der Server selbst.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255},"attributes":{"type":"object","additionalProperties":{},"default":{}},"priceOverride":{"type":"number","minimum":0},"stockQty":{"type":"number","minimum":0,"default":0},"barcode":{"type":"string","maxLength":100},"imageUrl":{"type":"string","maxLength":2048},"sku":{"type":"string","minLength":1,"maxLength":120}}},"example":{"name":"string","attributes":{},"priceOverride":0,"stockQty":0,"barcode":"string","imageUrl":"string","sku":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzdaten.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` — auch dann, wenn die Kennung zu keiner Zeile passte. Der Handler prueft die Trefferzahl nicht."}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar oder das DELETE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Die durchgereichte Fehlermeldung (englisch), NICHT eine feste Kennung. „DB unavailable\" bedeutet: Datenbank nicht erreichbar."}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1InventoryVariantsByVariantId","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"variantId","required":true}],"summary":"Delete a product variant","description":"Loescht eine Variante endgueltig (kein Soft-Delete). Der Handler wertet die Trefferzahl nicht aus: eine unbekannte Kennung ergibt dieselbe Quittung wie ein echter Treffer."}},"/api/v1/inventory/articles/{articleId}/variants/import":{"post":{"responses":{"200":{"description":"Das Ergebnis des Imports. ZWEI FORMEN: war mindestens eine Zeile lesbar, kommt das vollstaendige Ergebnis mit `insertedIds`. War KEINE lesbar und gab es auch keinen Parser-Fehler (also nur eine Kopfzeile), kommt die verkuerzte Form OHNE `insertedIds`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"imported":{"type":"integer","minimum":0,"description":"Anzahl wirklich angelegter Varianten."},"insertedIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Die Kennungen der angelegten Varianten, in der Reihenfolge der Datei."},"skipped":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string","minLength":1,"description":"Artikelnummer der uebersprungenen Zeile."},"reason":{"type":"string","description":"Grund. `sku exists` heisst: die Nummer gibt es schon; sonst die Fehlermeldung der Datenbank."}},"required":["sku","reason"],"additionalProperties":false},"description":"Zeilen, die die Datenbank nicht angenommen hat — meist wegen doppelter Artikelnummer."},"errors":{"type":"array","items":{"type":"object","properties":{"line":{"type":"integer","minimum":1,"description":"Zeilennummer in der hochgeladenen Datei, 1-basiert (Zeile 1 ist die Kopfzeile)."},"reason":{"type":"string","description":"Grund, z. B. `empty sku`, `sku column missing` oder `invalid price: …`."}},"required":["line","reason"],"additionalProperties":false},"description":"Zeilen, die schon der Parser abgelehnt hat. Der Import laeuft trotzdem weiter."}},"required":["imported","insertedIds","skipped","errors"],"additionalProperties":false},{"type":"object","properties":{"imported":{"type":"number","const":0,"description":"Immer `0` — es gab nichts anzulegen."},"skipped":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string","minLength":1,"description":"Artikelnummer der uebersprungenen Zeile."},"reason":{"type":"string","description":"Grund. `sku exists` heisst: die Nummer gibt es schon; sonst die Fehlermeldung der Datenbank."}},"required":["sku","reason"],"additionalProperties":false},"maxItems":0,"description":"Immer leer, da nichts versucht wurde."},"errors":{"type":"array","items":{"type":"object","properties":{"line":{"type":"integer","minimum":1,"description":"Zeilennummer in der hochgeladenen Datei, 1-basiert (Zeile 1 ist die Kopfzeile)."},"reason":{"type":"string","description":"Grund, z. B. `empty sku`, `sku column missing` oder `invalid price: …`."}},"required":["line","reason"],"additionalProperties":false},"description":"Die abgelehnten Zeilen. Ist auch diese Liste leer, war die Datei nur eine Kopfzeile."}},"required":["imported","skipped","errors"],"additionalProperties":false}]},"example":{"imported":0,"insertedIds":["00000000-0000-4000-8000-000000000000"],"skipped":[{"sku":"string","reason":"string"}],"errors":[{"line":1,"reason":"string"}]}}}},"400":{"description":"ZWEI FORMEN: bei leerem Rumpf nur `{ \"error\": \"empty body\" }`. Fehlt dagegen die Spalte `sku` oder war keine Datenzeile lesbar, kommt das verkuerzte Import-Ergebnis — derselbe Koerper wie bei 200, nur eben mit gefuellter `errors`-Liste.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"empty body","description":"Fester Text. Es wurde kein CSV mitgeschickt."}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"imported":{"type":"number","const":0,"description":"Immer `0` — es gab nichts anzulegen."},"skipped":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string","minLength":1,"description":"Artikelnummer der uebersprungenen Zeile."},"reason":{"type":"string","description":"Grund. `sku exists` heisst: die Nummer gibt es schon; sonst die Fehlermeldung der Datenbank."}},"required":["sku","reason"],"additionalProperties":false},"maxItems":0,"description":"Immer leer, da nichts versucht wurde."},"errors":{"type":"array","items":{"type":"object","properties":{"line":{"type":"integer","minimum":1,"description":"Zeilennummer in der hochgeladenen Datei, 1-basiert (Zeile 1 ist die Kopfzeile)."},"reason":{"type":"string","description":"Grund, z. B. `empty sku`, `sku column missing` oder `invalid price: …`."}},"required":["line","reason"],"additionalProperties":false},"description":"Die abgelehnten Zeilen. Ist auch diese Liste leer, war die Datei nur eine Kopfzeile."}},"required":["imported","skipped","errors"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar oder das Lesen des Rumpfes schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Die durchgereichte Fehlermeldung (englisch), NICHT eine feste Kennung. „DB unavailable\" bedeutet: Datenbank nicht erreichbar."}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1InventoryArticlesByArticleIdVariantsImport","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"articleId","required":true}],"summary":"Bulk-import variants from CSV","description":"W2-D — POST body: text/csv with header `sku,name,color,size,price,barcode,stock`. SKU collisions are skipped (returned in `skipped`). Lines with parse errors are listed in `errors` but the whole import does NOT abort."}},"/api/v1/inventory/categories":{"get":{"responses":{"200":{"description":"Alle Kategorien des Mandanten. Beim ersten Aufruf legt der Endpunkt die Tabelle an und saet die sieben Standard-Kategorien — die Liste ist also nie leer, solange niemand alle geloescht hat.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Kategorie (UUID, vom Server vergeben)"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Angezeigter Name der Kategorie, z. B. „Werkzeuge\". Mandantenweit eindeutig."},"sort_order":{"type":"integer","minimum":0,"description":"Anzeigereihenfolge, aufsteigend. Neu angelegte Kategorien erhalten den bisher hoechsten Wert plus 1 und stehen damit am Ende der Liste."},"created_at":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."}},"required":["id","name","sort_order","created_at"],"additionalProperties":false},"description":"Alle Kategorien des Mandanten, sortiert nach `sort_order`, dann nach `name`."}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","name":"string","sort_order":0,"created_at":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar oder Anlegen der Tabelle fehlgeschlagen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche, inklusive Aufforderung zum erneuten Versuch."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryCategories","tags":["Inventory"],"parameters":[],"summary":"List inventory categories","description":"Tenant-scoped article categories (user-managed). Auto-seeds defaults on first access."},"post":{"responses":{"201":{"description":"Die angelegte Kategorie, unverpackt — kein `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Kategorie (UUID, vom Server vergeben)"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Angezeigter Name der Kategorie, z. B. „Werkzeuge\". Mandantenweit eindeutig."},"sort_order":{"type":"integer","minimum":0,"description":"Anzeigereihenfolge, aufsteigend. Neu angelegte Kategorien erhalten den bisher hoechsten Wert plus 1 und stehen damit am Ende der Liste."},"created_at":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."}},"required":["id","name","sort_order","created_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","sort_order":0,"created_at":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Eine Kategorie dieses Namens gibt es bereits (UNIQUE-Verletzung auf `name`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"duplicate_category","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das INSERT lieferte keine Zeile zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche, inklusive Aufforderung zum erneuten Versuch."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"postApiV1InventoryCategories","tags":["Inventory"],"parameters":[],"summary":"Create inventory category","description":"Creates a new article category. 409 if the name already exists.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100}},"required":["name"]},"example":{"name":"string"}}}}}},"/api/v1/inventory/categories/{id}":{"patch":{"responses":{"200":{"description":"Die umbenannte Kategorie im Zustand nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Kategorie (UUID, vom Server vergeben)"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Angezeigter Name der Kategorie, z. B. „Werkzeuge\". Mandantenweit eindeutig."},"sort_order":{"type":"integer","minimum":0,"description":"Anzeigereihenfolge, aufsteigend. Neu angelegte Kategorien erhalten den bisher hoechsten Wert plus 1 und stehen damit am Ende der Liste."},"created_at":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."}},"required":["id","name","sort_order","created_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","sort_order":0,"created_at":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Kategorie mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Der neue Name ist schon vergeben (UNIQUE-Verletzung auf `name`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"duplicate_category","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Auch eine Kennung, die nicht wie eine UUID aussieht, landet hier — `safeUuid` wirft, und der catch faengt es als 503.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche, inklusive Aufforderung zum erneuten Versuch."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"patchApiV1InventoryCategoriesById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rename inventory category","description":"Renames an existing category. 409 if the new name already exists.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100}},"required":["name"]},"example":{"name":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzdaten — die Kategorie ist endgueltig weg (kein Soft-Delete).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`. Die geloeschte Kategorie wird nicht zurueckgegeben."}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Kategorie mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Auch eine Kennung, die nicht wie eine UUID aussieht, landet hier — `safeUuid` wirft, und der catch faengt es als 503.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche, inklusive Aufforderung zum erneuten Versuch."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"deleteApiV1InventoryCategoriesById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete inventory category","description":"Permanently deletes a category — including seeded defaults (no protection)."}},"/api/v1/inventory/units":{"get":{"responses":{"200":{"description":"Alle Einheiten des Mandanten. Beim ersten Aufruf legt der Endpunkt die Tabelle an und saet die Standard-Einheiten — die Liste ist also nie leer, solange niemand alle geloescht hat.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Einheit (UUID, vom Server vergeben)"},"name":{"type":"string","minLength":1,"maxLength":50,"description":"Angezeigter Name der Einheit, z. B. „Stk\", „m²\" oder „Std\". Mandantenweit eindeutig."},"sort_order":{"type":"integer","minimum":0,"description":"Anzeigereihenfolge, aufsteigend. Neu angelegte Einheiten erhalten den bisher hoechsten Wert plus 1 und stehen damit am Ende der Liste."},"created_at":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."}},"required":["id","name","sort_order","created_at"],"additionalProperties":false},"description":"Alle Einheiten des Mandanten, sortiert nach `sort_order`, dann nach `name`."}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","name":"string","sort_order":0,"created_at":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar oder Anlegen der Tabelle fehlgeschlagen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche, inklusive Aufforderung zum erneuten Versuch."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"getApiV1InventoryUnits","tags":["Inventory"],"parameters":[],"summary":"List inventory units","description":"Tenant-scoped article units (user-managed). Auto-seeds defaults on first access."},"post":{"responses":{"201":{"description":"Die angelegte Einheit, unverpackt — kein `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Einheit (UUID, vom Server vergeben)"},"name":{"type":"string","minLength":1,"maxLength":50,"description":"Angezeigter Name der Einheit, z. B. „Stk\", „m²\" oder „Std\". Mandantenweit eindeutig."},"sort_order":{"type":"integer","minimum":0,"description":"Anzeigereihenfolge, aufsteigend. Neu angelegte Einheiten erhalten den bisher hoechsten Wert plus 1 und stehen damit am Ende der Liste."},"created_at":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."}},"required":["id","name","sort_order","created_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","sort_order":0,"created_at":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Eine Einheit dieses Namens gibt es bereits (UNIQUE-Verletzung auf `name`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"duplicate_unit","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das INSERT lieferte keine Zeile zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche, inklusive Aufforderung zum erneuten Versuch."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"postApiV1InventoryUnits","tags":["Inventory"],"parameters":[],"summary":"Create inventory unit","description":"Creates a new article unit. 409 if the name already exists.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50}},"required":["name"]},"example":{"name":"string"}}}}}},"/api/v1/inventory/units/{id}":{"patch":{"responses":{"200":{"description":"Die umbenannte Einheit im Zustand nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Einheit (UUID, vom Server vergeben)"},"name":{"type":"string","minLength":1,"maxLength":50,"description":"Angezeigter Name der Einheit, z. B. „Stk\", „m²\" oder „Std\". Mandantenweit eindeutig."},"sort_order":{"type":"integer","minimum":0,"description":"Anzeigereihenfolge, aufsteigend. Neu angelegte Einheiten erhalten den bisher hoechsten Wert plus 1 und stehen damit am Ende der Liste."},"created_at":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."}},"required":["id","name","sort_order","created_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","sort_order":0,"created_at":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Einheit mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Der neue Name ist schon vergeben (UNIQUE-Verletzung auf `name`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"duplicate_unit","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Auch eine Kennung, die nicht wie eine UUID aussieht, landet hier — `safeUuid` wirft, und der catch faengt es als 503.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche, inklusive Aufforderung zum erneuten Versuch."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"patchApiV1InventoryUnitsById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rename inventory unit","description":"Renames an existing unit. 409 if the new name already exists.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50}},"required":["name"]},"example":{"name":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzdaten — die Einheit ist endgueltig weg (kein Soft-Delete).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`. Die geloeschte Einheit wird nicht zurueckgegeben."}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Einheit mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Auch eine Kennung, die nicht wie eine UUID aussieht, landet hier — `safeUuid` wirft, und der catch faengt es als 503.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche, inklusive Aufforderung zum erneuten Versuch."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"deleteApiV1InventoryUnitsById","tags":["Inventory"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete inventory unit","description":"Permanently deletes a unit — including seeded defaults (no protection)."}},"/api/v1/payment-terms":{"get":{"responses":{"200":{"description":"Alle Zahlungsziele, sortiert nach Reihenfolge und Name","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Zahlungsziels"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Bezeichnung, mandantenweit eindeutig"},"days":{"type":["integer","null"],"minimum":0,"maximum":365,"description":"Zahlungsziel in Tagen (Faelligkeit = Belegdatum + days); null = nur Beschriftung"},"discount_days":{"type":["integer","null"],"minimum":0,"maximum":365,"description":"Skontofrist in Tagen; null = kein Skonto vereinbart"},"discount_percent":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Skontosatz in Prozent; NUMERIC, kommt je nach Treiber als Zeichenkette"},"sort_order":{"type":"integer","description":"Reihenfolge in der Auswahlliste, aufsteigend"},"created_at":{"type":"string","description":"Anlagezeitpunkt (ISO)"}},"required":["id","name","days","discount_days","discount_percent","sort_order","created_at"],"additionalProperties":false},"description":"Alle Zahlungsziele des Mandanten, sortiert"}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","name":"string","days":0,"discount_days":0,"discount_percent":"string","sort_order":0,"created_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Nicht ladbar — Datenbankfehler oder fehlender Mandant im Kontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"getApiV1Payment-terms","tags":["settings"],"parameters":[],"summary":"List payment terms","description":"Zahlungsziel-Auswahltabelle des Mandanten. Beim ersten Zugriff werden fünf Standardziele angelegt, danach gehört die Tabelle dem Mandanten. Die Felder kommen in snake_case zurück, nicht in camelCase."},"post":{"responses":{"201":{"description":"Angelegt — der neue Datensatz, inklusive vergebener Id und Reihenfolge","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Zahlungsziels"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Bezeichnung, mandantenweit eindeutig"},"days":{"type":["integer","null"],"minimum":0,"maximum":365,"description":"Zahlungsziel in Tagen (Faelligkeit = Belegdatum + days); null = nur Beschriftung"},"discount_days":{"type":["integer","null"],"minimum":0,"maximum":365,"description":"Skontofrist in Tagen; null = kein Skonto vereinbart"},"discount_percent":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Skontosatz in Prozent; NUMERIC, kommt je nach Treiber als Zeichenkette"},"sort_order":{"type":"integer","description":"Reihenfolge in der Auswahlliste, aufsteigend"},"created_at":{"type":"string","description":"Anlagezeitpunkt (ISO)"}},"required":["id","name","days","discount_days","discount_percent","sort_order","created_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","days":0,"discount_days":0,"discount_percent":"string","sort_order":0,"created_at":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Ein Zahlungsziel mit diesem Namen gibt es bereits","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"duplicate_term","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Nicht gespeichert — Datenbankfehler oder fehlender Mandant im Kontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"postApiV1Payment-terms","tags":["settings"],"parameters":[],"summary":"Create payment term","description":"Legt ein neues Zahlungsziel in der Auswahltabelle an. 409 wenn der Name existiert. Die Reihenfolge wird automatisch ans Ende gesetzt. Die Antwort ist der neue Datensatz in snake_case.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"days":{"type":["integer","null"],"minimum":0,"maximum":365},"discountDays":{"type":["integer","null"],"minimum":0,"maximum":365},"discountPercent":{"type":["number","null"],"minimum":0,"maximum":100}},"required":["name"]},"example":{"name":"string","days":0,"discountDays":0,"discountPercent":0}}}}}},"/api/v1/payment-terms/{id}":{"patch":{"responses":{"200":{"description":"Geändert — der Datensatz nach der Änderung, in snake_case","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Zahlungsziels"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Bezeichnung, mandantenweit eindeutig"},"days":{"type":["integer","null"],"minimum":0,"maximum":365,"description":"Zahlungsziel in Tagen (Faelligkeit = Belegdatum + days); null = nur Beschriftung"},"discount_days":{"type":["integer","null"],"minimum":0,"maximum":365,"description":"Skontofrist in Tagen; null = kein Skonto vereinbart"},"discount_percent":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Skontosatz in Prozent; NUMERIC, kommt je nach Treiber als Zeichenkette"},"sort_order":{"type":"integer","description":"Reihenfolge in der Auswahlliste, aufsteigend"},"created_at":{"type":"string","description":"Anlagezeitpunkt (ISO)"}},"required":["id","name","days","discount_days","discount_percent","sort_order","created_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","days":0,"discount_days":0,"discount_percent":"string","sort_order":0,"created_at":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Zahlungsziel nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Ein Zahlungsziel mit diesem Namen gibt es bereits","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"duplicate_term","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Nicht geändert — Datenbankfehler, fehlender Mandant oder ungültiges Id-Format","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"patchApiV1Payment-termsById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update payment term","description":"Benennt ein Zahlungsziel um bzw. ändert Tage und Skonto. 409 bei Namenskollision. Trotz PATCH werden ALLE vier Felder geschrieben: ein nicht mitgeschicktes Tages- oder Skontofeld wird auf null gesetzt, nicht beibehalten. Eine Id, die kein UUID-Format hat, ergibt kein 400, sondern 503.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"days":{"type":["integer","null"],"minimum":0,"maximum":365},"discountDays":{"type":["integer","null"],"minimum":0,"maximum":365},"discountPercent":{"type":["number","null"],"minimum":0,"maximum":100}},"required":["name"]},"example":{"name":"string","days":0,"discountDays":0,"discountPercent":0}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Quittung, der Datensatz kommt nicht zurück","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Das Zahlungsziel wurde geloescht"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Zahlungsziel nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Nicht gelöscht — Datenbankfehler, fehlender Mandant oder ungültiges Id-Format","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"deleteApiV1Payment-termsById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete payment term","description":"Löscht ein Zahlungsziel endgültig aus der Auswahltabelle — auch die beim ersten Zugriff angelegten Standardziele. Kein Soft-Delete. Belege, die dieses Ziel bereits benutzen, behalten ihre gespeicherten Tage. Eine Id, die kein UUID-Format hat, ergibt kein 400, sondern 503."}},"/api/v1/projects/capacity":{"get":{"responses":{"200":{"description":"Wochenraster, Mitarbeiterzeilen und deren Auslastung","content":{"application/json":{"schema":{"type":"object","properties":{"weekStarts":{"type":"array","items":{"type":"string"},"description":"Montage der abgefragten Wochen als ISO-Datum, beginnend mit dem Montag der laufenden Woche"},"requiredSkills":{"type":"array","items":{"type":"string"},"description":"Geforderte Faehigkeiten des per projectId gewaehlten Projekts; leer ohne projectId"},"employees":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Mitarbeiters"},"name":{"type":"string","description":"Anzeigename: Vor- und Nachname, ersatzweise Nachname, E-Mail oder die Kennung"},"skills":{"type":"array","items":{"type":"string"},"description":"Erfasste Faehigkeiten des Mitarbeiters; leer wenn keine hinterlegt"},"skillMatch":{"type":"boolean","description":"true, wenn der Mitarbeiter mindestens eine der geforderten Faehigkeiten mitbringt; ohne projectId immer false"},"load":{"type":"array","items":{"type":"number"},"description":"Auslastung je Woche in derselben Reihenfolge wie weekStarts — 1 entspricht 40 Wochenstunden, Werte ueber 1 sind Ueberlast"}},"required":["id","name","skills","skillMatch","load"],"description":"Ein Mitarbeiter mit seiner Wochenauslastung"},"description":"Die Mitarbeiterzeilen der Heatmap; hoechstens 200"},"meta":{"type":"object","properties":{"weeks":{"type":"integer","minimum":1,"maximum":52,"description":"Anzahl der gelieferten Wochen"},"fullTimeHoursPerWeek":{"type":"number","description":"Wochenstunden, die als Auslastung 1 gelten"}},"required":["weeks","fullTimeHoursPerWeek"],"description":"Fehlt, wenn der Mandant keine Mitarbeiter erfasst hat"}},"required":["weekStarts","requiredSkills","employees"]},"example":{"weekStarts":["string"],"requiredSkills":["string"],"employees":[{"id":"00000000-0000-4000-8000-000000000000","name":"string","skills":["string"],"skillMatch":true,"load":[0]}],"meta":{"weeks":1,"fullTimeHoursPerWeek":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ProjectsCapacity","tags":["Projects · Capacity"],"parameters":[{"in":"query","name":"weeks","schema":{"type":"integer","minimum":1,"maximum":52,"default":12}},{"in":"query","name":"projectId","schema":{"type":"string","format":"uuid"}}],"summary":"Capacity-Heatmap data for the next N weeks","description":"Liest bis zu 200 nicht geloeschte Mitarbeiter und summiert ihre Stunden aus project_assignments je Kalenderwoche. Die Wochen beginnen beim Montag der laufenden Woche; `weeks` (1–52, Vorgabe 12) legt fest, wie viele folgen. Mit `projectId` werden zusaetzlich die geforderten Faehigkeiten des Projekts geladen und je Mitarbeiter als `skillMatch` gemeldet — ist das Projekt unbekannt, antwortet der Endpunkt 404."}},"/api/v1/projects/{projectId}/gantt":{"get":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ProjectsByProjectIdGantt","tags":["Projects","Gantt"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"summary":"Project Gantt data","description":"AK-407 — combined phases/tasks/milestones for Gantt rendering"}},"/api/v1/projects/{projectId}/budget":{"get":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ProjectsByProjectIdBudget","tags":["Projects","Budget"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"summary":"Project budget summary","description":"AK-410 — plan vs. ist plus weekly cumulative burn-rate from time entries"}},"/api/v1/projects/{projectId}/invoice-from-time":{"post":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ProjectsByProjectIdInvoice-from-time","tags":["Projects","TimeTracking","invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"summary":"Create invoice from project time entries","description":"T-W1c — aggregates unbilled billable time entries within a date range, creates a draft invoice and locks the consumed entries via invoice_id.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","minLength":8},"to":{"type":"string","minLength":8},"billableOnly":{"type":"boolean","default":true},"taskGrouping":{"type":"string","enum":["task","user","flat"],"default":"task"},"customerId":{"type":"string","format":"uuid"}},"required":["from","to"]},"example":{"from":"stringxx","to":"stringxx","billableOnly":true,"taskGrouping":"task","customerId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/projects/{projectId}/dossier-aggregate":{"get":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ProjectsByProjectIdDossier-aggregate","tags":["Projects","Dossier"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"summary":"Project dossier aggregate","description":"W3 — full cross-cutting roll-up: project + quotes/orders/invoices/time-entries + KPIs."}},"/api/v1/projects":{"get":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Projects","tags":["Projects"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"phase","schema":{"type":"string"}},{"in":"query","name":"customerId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"List projects","description":"Listet die nicht geloeschten Projekte des Mandanten, neueste zuerst, filterbar nach Phase, Kunde und Namensausschnitt, seitenweise ueber `limit`/`offset`. Schlaegt die Drizzle-Abfrage fehl (Schema-Drift), wird der Fehler gemeldet und die Liste ueber eine rohe SQL-Abfrage nachgeladen; die Antwort traegt dann `degraded: true`. Erst wenn auch das scheitert, kommt 200 mit leerer Liste und `warning`."},"post":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Projects","tags":["Projects"],"parameters":[],"summary":"Create project","description":"Legt ein Projekt an und antwortet 201 mit dem vollstaendigen Datensatz. Fehlt `number`, vergibt die Route eine Nummer der Form `PRJ-JJJJ-…`; leere Datumsfelder werden weggelassen statt als ungueltiges DATE geschrieben. Ein Rumpf, der nicht zum Schema passt, ergibt 400.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string"},"name":{"type":"string","minLength":1},"customerId":{"type":"string","format":"uuid"},"phase":{"type":"string","enum":["planning","in_progress","review","completed","cancelled"],"default":"planning"},"budget":{"type":"number","minimum":0},"spent":{"type":"number","minimum":0},"progressPercent":{"type":"number","minimum":0,"maximum":100},"startDate":{"type":"string"},"endDate":{"type":"string"},"customFields":{"type":"object","additionalProperties":{}}},"required":["name"]},"example":{"number":"string","name":"string","customerId":"00000000-0000-4000-8000-000000000000","phase":"planning","budget":0,"spent":0,"progressPercent":0,"startDate":"string","endDate":"string","customFields":{}}}}}}},"/api/v1/projects/{id}":{"get":{"responses":{"200":{"description":"Das Projekt — auch dann, wenn es geloescht ist (`deletedAt` gesetzt)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Projekt mit dieser Kennung im eigenen Mandanten (`not_found`)"},"500":{"description":"Unerwarteter Serverfehler (`internal_error`)"},"503":{"description":"Nur bei einem echten Verbindungsfehler der Abfrage (`database_unavailable`, mit `retryAfter`) — eine nicht verfuegbare Datenbank kommt als 500"}},"operationId":"getApiV1ProjectsById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get project by id","description":"Liest genau eine Zeile aus der `projects`-Tabelle des Mandanten-Schemas und gibt sie vollstaendig in camelCase zurueck: number, name, customerId, phase, budget, spent, progressPercent, startDate, endDate, defaultHourlyRate, customFields sowie deletedAt, createdAt und updatedAt. Verwandte Datensaetze zaehlt diese Route nicht — dafuer gibt es `GET /projects/{id}/stats`.\n\nEin per DELETE weggeraeumtes Projekt ergibt 404 `not_found` — wie in der Liste. (Bis 01.09.2026 kam es hier mit 200 zurueck und war nur an einem gesetzten `deletedAt` im Rumpf zu erkennen.)\n\nZu einer unbekannten Kennung — auch der eines fremden Mandanten, denn gelesen wird nur im eigenen Schema — kommt 404 `not_found`.\n\n503 IST HIER DIE AUSNAHME, NICHT DIE REGEL. `database_unavailable` mit `retryAfter` kommt NUR, wenn die Abfrage selbst an einem echten Verbindungsfehler stirbt (Postgres-Klasse 08xxx/57Pxx oder ECONNREFUSED und Verwandte). Alles andere — auch eine gar nicht erst verfuegbare Datenbank und jeder Schemafehler — kommt ehrlich als 500 `internal_error` statt als vorgetaeuschte Stoerung, die zum Wiederholen einlaedt."},"put":{"responses":{"200":{"description":"OK"},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"putApiV1ProjectsById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aktualisiert Ressource /:id (projects). Geschrieben werden nur die mitgeschickten Felder, `updatedAt` wird auf jetzt gesetzt; die Antwort ist das geaenderte Projekt. Ein geloeschtes oder unbekanntes Projekt ergibt 404 `not_found`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string"},"name":{"type":"string","minLength":1},"customerId":{"type":"string","format":"uuid"},"phase":{"type":"string","enum":["planning","in_progress","review","completed","cancelled"],"default":"planning"},"budget":{"type":"number","minimum":0},"spent":{"type":"number","minimum":0},"progressPercent":{"type":"number","minimum":0,"maximum":100},"startDate":{"type":"string"},"endDate":{"type":"string"},"customFields":{"type":"object","additionalProperties":{}}}},"example":{"number":"string","name":"string","customerId":"00000000-0000-4000-8000-000000000000","phase":"planning","budget":0,"spent":0,"progressPercent":0,"startDate":"string","endDate":"string","customFields":{}}}}},"summary":"Aktualisiert Ressource /:id (projects)","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Das geaenderte Projekt, vollstaendig"},"400":{"description":"Rumpf entspricht nicht dem Schema"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Projekt mit dieser Kennung im eigenen Mandanten (`not_found`)"},"500":{"description":"Unerwarteter Serverfehler (`internal_error`)"},"503":{"description":"Nur bei einem echten Verbindungsfehler der Abfrage (`database_unavailable`, mit `retryAfter`) — eine nicht verfuegbare Datenbank kommt als 500"}},"operationId":"patchApiV1ProjectsById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Patch project (partial update)","description":"Schreibt nur die Felder, die im Rumpf stehen; weggelassene bleiben unberuehrt, und `updatedAt` wird bei jedem Aufruf auf jetzt gesetzt. Aenderbar sind number, name, customerId, phase, budget, spent, progressPercent, startDate, endDate und customFields. `budget` und `spent` kommen als Zahl herein und werden als Dezimal-Zeichenkette abgelegt. `customFields` wird ERSETZT, nicht zusammengefuehrt: wer das Objekt mitschickt, verliert die darin nicht genannten Schluessel.\n\nEin LEERER Rumpf ist kein Sonderfall — es gibt kein `no_changes`: die Zeile wird trotzdem angefasst und `updatedAt` hochgezogen. Ein geloeschtes Projekt ergibt 404 `not_found` — bis 01.09.2026 liess es sich weiter aendern, und die Aenderung landete auf einer Zeile, die fuer jede Liste nicht mehr existierte.\n\nZurueck kommt die vollstaendige Zeile nach der Aenderung. Passt keine Zeile auf die Kennung, kommt 404 `not_found`. Ein Aufruf mit unzulaessigem Rumpf — etwa `phase` ausserhalb von planning, in_progress, review, completed, cancelled — wird vom Validator mit 400 abgewiesen, bevor der Handler laeuft. 503 `database_unavailable` kommt nur bei einem echten Verbindungsfehler der Abfrage; alles andere, auch eine gar nicht verfuegbare Datenbank, ehrlich als 500 `internal_error`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string"},"name":{"type":"string","minLength":1},"customerId":{"type":"string","format":"uuid"},"phase":{"type":"string","enum":["planning","in_progress","review","completed","cancelled"],"default":"planning"},"budget":{"type":"number","minimum":0},"spent":{"type":"number","minimum":0},"progressPercent":{"type":"number","minimum":0,"maximum":100},"startDate":{"type":"string"},"endDate":{"type":"string"},"customFields":{"type":"object","additionalProperties":{}}}},"example":{"number":"string","name":"string","customerId":"00000000-0000-4000-8000-000000000000","phase":"planning","budget":0,"spent":0,"progressPercent":0,"startDate":"string","endDate":"string","customFields":{}}}}}},"delete":{"responses":{"200":{"description":"OK"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"deleteApiV1ProjectsById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Loescht Ressource /:id (projects). Weiches Loeschen: nur `deletedAt` wird gesetzt, Aufgaben, Zeiten und Belege des Projekts bleiben stehen. Antwortet 200 `ok: true` auch dann, wenn keine Zeile zur Kennung passt.","summary":"Loescht Ressource /:id (projects)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/projects/{id}/stats":{"get":{"responses":{"200":{"description":"Statistiken"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1ProjectsByIdStats","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Verwandte Datensatz-Zähler für SmartButtons auf der Projekt-Detailseite. Gezählt werden nicht gelöschte Aufgaben, gebuchte Stunden (gerundet), Rechnungen und Dokumente des Projekts; `expenses` ist noch nicht angebunden und immer 0. Fehlt eine Tabelle oder die Datenbank, antwortet die Route trotzdem 200 mit Nullwerten, nie mit einem Fehler.","summary":"Verwandte Datensatz-Zähler für SmartButtons auf der Projekt-Detailseite","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/projects/{id}/duplicate":{"post":{"responses":{"201":{"description":"Die angelegte Kopie, vollstaendig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Projekt mit dieser Kennung im eigenen Mandanten (`not_found`)"},"500":{"description":"Unerwarteter Serverfehler (`internal_error`)"},"503":{"description":"Nur bei einem echten Verbindungsfehler der Abfrage (`database_unavailable`, mit `retryAfter`) — eine nicht verfuegbare Datenbank kommt als 500"}},"operationId":"postApiV1ProjectsByIdDuplicate","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Duplicate a project","description":"Legt eine zweite Zeile in `projects` an, die alle Felder des Originals uebernimmt — auch `spent`, `progressPercent` und `customFields`. Die Kopie startet also NICHT bei null: wer eine leere Projekthuelle will, muss diese Werte anschliessend selbst zuruecksetzen. Neu vergeben werden nur Kennung, Zeitstempel, `phase` (immer `planning`) und die Projektnummer.\n\nDIE NUMMER KOMMT NICHT AUS DEM NUMMERNKREIS. Sie wird als `<Nummer-des-Originals>-COPY-<Zeitstempel>` gebildet, ersatzweise `PRJ-COPY-…`, wenn das Original keine hat. Sie passt damit weder zum Format der Einstellungen noch in die fortlaufende Reihe, und eine Kopie der Kopie haengt ein zweites `-COPY-` an.\n\nDer Aufruf ist NICHT gegen Wiederholung abgesichert: jeder weitere Aufruf erzeugt ein weiteres Projekt. Ein geloeschtes Original ergibt 404 `not_found` — bis 01.09.2026 liess es sich kopieren, und die Kopie war dann wieder sichtbar: ein Weg, Geloeschtes zurueckzuholen, den niemand vorsah. Gibt es die Kennung im eigenen Mandanten nicht, kommt ebenfalls 404. Zurueck kommt 201 mit der vollstaendigen neuen Zeile."}},"/api/v1/projects/{id}/convert-to-invoice":{"post":{"responses":{"201":{"description":"Rechnung angelegt — Entwurf ohne Positionen, `tax` = 0"},"400":{"description":"Mandanten-Kuerzel unbrauchbar (`invalid_tenant`)"},"401":{"description":"Kein Mandantenkontext (`tenant_context_required`)"},"404":{"description":"Projekt nicht gefunden oder geloescht (`not_found`)"},"409":{"description":"Projekt wurde bereits fakturiert (`already_invoiced`) — mit `existing_id` und `invoice_number` der bestehenden Rechnung"},"500":{"description":"Unerwarteter Serverfehler (`internal_error`) — hierunter faellt auch ein fehlender Nummernkreis `invoice_number`"},"503":{"description":"Keine Datenbankverbindung, oder die Abfrage stirbt an einem echten Verbindungsfehler (`database_unavailable`, mit `retryAfter`)"}},"operationId":"postApiV1ProjectsByIdConvert-to-invoice","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert project to invoice","description":"Legt zu einem Projekt eine Rechnung im Status `draft` an und verknuepft sie ueber `project_id`. Uebernommen werden nur der Kunde und das Budget: `subtotal` und `total` sind beide das Projektbudget (ersatzweise 0), `tax` ist **0** und `positions` bleibt leer. ES WIRD KEINE STEUER GERECHNET und es entstehen keine Rechnungspositionen — der Beleg ist ein Entwurf zum Nacharbeiten, keine fertige Rechnung. Das Faelligkeitsdatum liegt 30 Tage in der Zukunft. Zeigt das Projekt auf einen Kunden, den es nicht mehr gibt, wird OHNE Kunden fakturiert (`customerId: null`) statt den Vorgang abzubrechen.\n\nEINMAL JE PROJEKT. Ein zweiter Aufruf antwortet 409 `already_invoiced` und nennt `existing_id` und `invoice_number` der bestehenden Rechnung. Der Schutz laeuft in zwei Phasen unter `SELECT … FOR UPDATE`: der zweite gleichzeitige Klick wartet, sieht danach die Rechnung des ersten und faellt auf 409 — OHNE eine Nummer aus dem Nummernkreis zu verbrauchen, die sonst als Luecke in der fortlaufenden Reihe stehen bliebe (GoBD). Massgeblich ist dabei nur die nicht geloeschte Rechnung: wird sie geloescht, ist der Weg wieder frei.\n\nDie Rechnungsnummer kommt aus dem zentralen Nummernkreis `invoice_number` — ohne Ersatzweg: ist dort kein Kreis eingerichtet, bricht der Aufruf mit 500 `internal_error` ab, statt eine erfundene Nummer zu vergeben. Nur ein echter Ausfall der Datenbankverbindung kommt als 503 mit `retryAfter`; alles andere bleibt ehrlich ein 500. Fehlt der Spalte `invoices.project_id` auf einem gedrifteten Mandanten, wird sie vorher angelegt. Gelesen wird das Projekt mit `deleted_at IS NULL`: ein geloeschtes ergibt 404 `not_found`. Ohne Mandantenkontext kommt 401 `tenant_context_required`, bei unbrauchbarem Mandanten-Kuerzel 400 `invalid_tenant`. Zurueck kommt 201 mit der Rechnung in camelCase."}},"/api/v1/project-tasks":{"get":{"responses":{"200":{"description":"Die gefundenen Aufgaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Aufgabe"},"projectId":{"type":"string","format":"uuid","description":"Projekt, zu dem die Aufgabe gehoert"},"phaseId":{"type":["string","null"],"format":"uuid","description":"Projektphase; null wenn keiner zugeordnet"},"title":{"type":"string","description":"Titel der Aufgabe"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst"},"status":{"type":"string","description":"Bearbeitungsstand — angelegt wird nur todo, in_progress, done oder blocked"},"priority":{"type":"string","description":"Dringlichkeit — angelegt wird nur low, normal, high oder urgent"},"assigneeId":{"type":["string","null"],"format":"uuid","description":"Zustaendiger; null wenn niemand zugewiesen"},"dueDate":{"type":["string","null"],"description":"Faelligkeitsdatum; null wenn keines gesetzt"},"estimateHours":{"type":["string","null"],"description":"Geschaetzter Aufwand in Stunden als Dezimalzahl-Zeichenkette; null wenn nicht geschaetzt"},"actualHours":{"type":["string","null"],"description":"Erfasster Aufwand in Stunden als Dezimalzahl-Zeichenkette; null wenn nichts erfasst"},"hourlyRate":{"type":["string","null"],"description":"Stundensatz nur fuer diese Aufgabe; null wenn der Satz von Projekt oder Mandant gilt"},"customFields":{"type":"object","additionalProperties":{},"description":"Eigene Felder des Mandanten; leeres Objekt wenn keine gepflegt"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung"},"source":{"type":"string","const":"global","description":"Nur bei eingemischten Aufgaben aus dem globalen Aufgabenbrett gesetzt; diese werden ueber /api/v1/tasks gepflegt"}},"required":["id","projectId","phaseId","title","description","status","priority","assigneeId","dueDate","estimateHours","actualHours","hourlyRate","customFields","createdAt","updatedAt"],"description":"Eine Projektaufgabe"},"description":"Die gefundenen Aufgaben, neueste zuerst"},"degraded":{"type":"boolean","const":true,"description":"Nur gesetzt, wenn die Liste ueber den Notfallpfad gelesen wurde (Schema-Abweichung)"},"warning":{"type":"string","description":"Nur gesetzt, wenn Tabelle oder Datenbank fehlten und deshalb eine leere Liste geliefert wird"}},"required":["data"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","projectId":"00000000-0000-4000-8000-000000000000","phaseId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","status":"string","priority":"string","assigneeId":"00000000-0000-4000-8000-000000000000","dueDate":"string","estimateHours":"string","actualHours":"string","hourlyRate":"string","customFields":{},"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","source":"global"}],"degraded":true,"warning":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Project-tasks","tags":["Projects"],"parameters":[],"summary":"List tasks","description":"Liest die Aufgaben aus project_tasks, neueste zuerst; die Abfrageparameter projectId, status und assigneeId filtern sie. Wird nach projectId gefiltert, werden zusaetzlich die diesem Projekt zugeordneten Aufgaben des globalen Aufgabenbretts eingemischt — sie tragen `source: \"global\"` und werden ueber /api/v1/tasks gepflegt, PUT und DELETE dieser Route greifen sie nicht. Weicht das Datenbankschema ab, liest ein Notfallpfad die Zeilen roh und setzt `degraded: true`; fehlen Tabelle oder Datenbank ganz, kommt eine leere Liste mit `warning` statt eines Fehlers."},"post":{"responses":{"201":{"description":"Die angelegte Aufgabe","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Aufgabe"},"projectId":{"type":"string","format":"uuid","description":"Projekt, zu dem die Aufgabe gehoert"},"phaseId":{"type":["string","null"],"format":"uuid","description":"Projektphase; null wenn keiner zugeordnet"},"title":{"type":"string","description":"Titel der Aufgabe"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst"},"status":{"type":"string","description":"Bearbeitungsstand — angelegt wird nur todo, in_progress, done oder blocked"},"priority":{"type":"string","description":"Dringlichkeit — angelegt wird nur low, normal, high oder urgent"},"assigneeId":{"type":["string","null"],"format":"uuid","description":"Zustaendiger; null wenn niemand zugewiesen"},"dueDate":{"type":["string","null"],"description":"Faelligkeitsdatum; null wenn keines gesetzt"},"estimateHours":{"type":["string","null"],"description":"Geschaetzter Aufwand in Stunden als Dezimalzahl-Zeichenkette; null wenn nicht geschaetzt"},"actualHours":{"type":["string","null"],"description":"Erfasster Aufwand in Stunden als Dezimalzahl-Zeichenkette; null wenn nichts erfasst"},"hourlyRate":{"type":["string","null"],"description":"Stundensatz nur fuer diese Aufgabe; null wenn der Satz von Projekt oder Mandant gilt"},"customFields":{"type":"object","additionalProperties":{},"description":"Eigene Felder des Mandanten; leeres Objekt wenn keine gepflegt"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung"},"source":{"type":"string","const":"global","description":"Nur bei eingemischten Aufgaben aus dem globalen Aufgabenbrett gesetzt; diese werden ueber /api/v1/tasks gepflegt"}},"required":["id","projectId","phaseId","title","description","status","priority","assigneeId","dueDate","estimateHours","actualHours","hourlyRate","customFields","createdAt","updatedAt"],"description":"Eine Projektaufgabe"},"example":{"id":"00000000-0000-4000-8000-000000000000","projectId":"00000000-0000-4000-8000-000000000000","phaseId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","status":"string","priority":"string","assigneeId":"00000000-0000-4000-8000-000000000000","dueDate":"string","estimateHours":"string","actualHours":"string","hourlyRate":"string","customFields":{},"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","source":"global"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Project-tasks","tags":["Projects"],"parameters":[],"summary":"Create task","description":"Legt eine Zeile in project_tasks an. projectId und title sind Pflicht; ohne Angabe gilt status \"todo\" und priority \"normal\". estimateHours und actualHours werden als Dezimalzahl gespeichert. Die Antwort ist der angelegte Datensatz selbst, ohne Umschlag.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"phaseId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1},"description":{"type":"string"},"status":{"type":"string","enum":["todo","in_progress","done","blocked"],"default":"todo"},"priority":{"type":"string","enum":["low","normal","high","urgent"],"default":"normal"},"assigneeId":{"type":"string","format":"uuid"},"dueDate":{"type":"string"},"estimateHours":{"type":"number","minimum":0},"actualHours":{"type":"number","minimum":0}},"required":["projectId","title"]},"example":{"projectId":"00000000-0000-4000-8000-000000000000","phaseId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","status":"todo","priority":"low","assigneeId":"00000000-0000-4000-8000-000000000000","dueDate":"string","estimateHours":0,"actualHours":0}}}}}},"/api/v1/project-tasks/{id}":{"put":{"responses":{"200":{"description":"Die Aufgabe nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Aufgabe"},"projectId":{"type":"string","format":"uuid","description":"Projekt, zu dem die Aufgabe gehoert"},"phaseId":{"type":["string","null"],"format":"uuid","description":"Projektphase; null wenn keiner zugeordnet"},"title":{"type":"string","description":"Titel der Aufgabe"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst"},"status":{"type":"string","description":"Bearbeitungsstand — angelegt wird nur todo, in_progress, done oder blocked"},"priority":{"type":"string","description":"Dringlichkeit — angelegt wird nur low, normal, high oder urgent"},"assigneeId":{"type":["string","null"],"format":"uuid","description":"Zustaendiger; null wenn niemand zugewiesen"},"dueDate":{"type":["string","null"],"description":"Faelligkeitsdatum; null wenn keines gesetzt"},"estimateHours":{"type":["string","null"],"description":"Geschaetzter Aufwand in Stunden als Dezimalzahl-Zeichenkette; null wenn nicht geschaetzt"},"actualHours":{"type":["string","null"],"description":"Erfasster Aufwand in Stunden als Dezimalzahl-Zeichenkette; null wenn nichts erfasst"},"hourlyRate":{"type":["string","null"],"description":"Stundensatz nur fuer diese Aufgabe; null wenn der Satz von Projekt oder Mandant gilt"},"customFields":{"type":"object","additionalProperties":{},"description":"Eigene Felder des Mandanten; leeres Objekt wenn keine gepflegt"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung"},"source":{"type":"string","const":"global","description":"Nur bei eingemischten Aufgaben aus dem globalen Aufgabenbrett gesetzt; diese werden ueber /api/v1/tasks gepflegt"}},"required":["id","projectId","phaseId","title","description","status","priority","assigneeId","dueDate","estimateHours","actualHours","hourlyRate","customFields","createdAt","updatedAt"],"description":"Eine Projektaufgabe"},"example":{"id":"00000000-0000-4000-8000-000000000000","projectId":"00000000-0000-4000-8000-000000000000","phaseId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","status":"string","priority":"string","assigneeId":"00000000-0000-4000-8000-000000000000","dueDate":"string","estimateHours":"string","actualHours":"string","hourlyRate":"string","customFields":{},"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","source":"global"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"putApiV1Project-tasksById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aktualisiert eine Aufgabe in project_tasks. Uebertragen werden nur die mitgeschickten Felder, alle uebrigen bleiben stehen; updatedAt setzt der Server. Trifft die Kennung keine Zeile, antwortet der Endpunkt 404. Aufgaben mit `source: \"global\"` liegen in einer anderen Tabelle und werden hier nicht gefunden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"phaseId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1},"description":{"type":"string"},"status":{"type":"string","enum":["todo","in_progress","done","blocked"],"default":"todo"},"priority":{"type":"string","enum":["low","normal","high","urgent"],"default":"normal"},"assigneeId":{"type":"string","format":"uuid"},"dueDate":{"type":"string"},"estimateHours":{"type":"number","minimum":0},"actualHours":{"type":"number","minimum":0}}},"example":{"projectId":"00000000-0000-4000-8000-000000000000","phaseId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","status":"todo","priority":"low","assigneeId":"00000000-0000-4000-8000-000000000000","dueDate":"string","estimateHours":0,"actualHours":0}}}},"summary":"Aktualisiert eine Aufgabe in project_tasks","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Der Loeschbefehl wurde ausgefuehrt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Der Loeschbefehl wurde ausgefuehrt"}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"deleteApiV1Project-tasksById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Entfernt die Aufgabe endgueltig aus project_tasks — kein Soft-Delete, kein Wiederherstellen. Der Endpunkt meldet den ausgefuehrten Loeschbefehl auch dann, wenn die Kennung keine Zeile getroffen hat. Aufgaben mit `source: \"global\"` liegen in einer anderen Tabelle und bleiben unberuehrt.","summary":"Entfernt die Aufgabe endgueltig aus project_tasks","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/project-tasks/{id}/assign":{"post":{"responses":{"200":{"description":"Die Aufgabe mit dem neuen Zustaendigen","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Aufgabe"},"projectId":{"type":"string","format":"uuid","description":"Projekt, zu dem die Aufgabe gehoert"},"phaseId":{"type":["string","null"],"format":"uuid","description":"Projektphase; null wenn keiner zugeordnet"},"title":{"type":"string","description":"Titel der Aufgabe"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst"},"status":{"type":"string","description":"Bearbeitungsstand — angelegt wird nur todo, in_progress, done oder blocked"},"priority":{"type":"string","description":"Dringlichkeit — angelegt wird nur low, normal, high oder urgent"},"assigneeId":{"type":["string","null"],"format":"uuid","description":"Zustaendiger; null wenn niemand zugewiesen"},"dueDate":{"type":["string","null"],"description":"Faelligkeitsdatum; null wenn keines gesetzt"},"estimateHours":{"type":["string","null"],"description":"Geschaetzter Aufwand in Stunden als Dezimalzahl-Zeichenkette; null wenn nicht geschaetzt"},"actualHours":{"type":["string","null"],"description":"Erfasster Aufwand in Stunden als Dezimalzahl-Zeichenkette; null wenn nichts erfasst"},"hourlyRate":{"type":["string","null"],"description":"Stundensatz nur fuer diese Aufgabe; null wenn der Satz von Projekt oder Mandant gilt"},"customFields":{"type":"object","additionalProperties":{},"description":"Eigene Felder des Mandanten; leeres Objekt wenn keine gepflegt"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung"},"source":{"type":"string","const":"global","description":"Nur bei eingemischten Aufgaben aus dem globalen Aufgabenbrett gesetzt; diese werden ueber /api/v1/tasks gepflegt"}},"required":["id","projectId","phaseId","title","description","status","priority","assigneeId","dueDate","estimateHours","actualHours","hourlyRate","customFields","createdAt","updatedAt"],"description":"Eine Projektaufgabe"},"example":{"id":"00000000-0000-4000-8000-000000000000","projectId":"00000000-0000-4000-8000-000000000000","phaseId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","status":"string","priority":"string","assigneeId":"00000000-0000-4000-8000-000000000000","dueDate":"string","estimateHours":"string","actualHours":"string","hourlyRate":"string","customFields":{},"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","source":"global"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"postApiV1Project-tasksByIdAssign","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Zustaendigen einer Projektaufgabe setzen","description":"Setzt den Zustaendigen einer Aufgabe auf die uebergebene assigneeId und aktualisiert updatedAt; andere Felder bleiben unberuehrt. Trifft die Kennung keine Zeile, antwortet der Endpunkt 404. Der Aufruf legt nichts an — die Antwort traegt 200, nicht 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"assigneeId":{"type":"string","format":"uuid"}},"required":["assigneeId"]},"example":{"assigneeId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/project-milestones":{"get":{"responses":{"200":{"description":"Meilensteine nach Faelligkeit aufsteigend, optional gefiltert ueber `projectId` und `status`. Weder Blaetterung noch Obergrenze. DREI Formen, alle mit 200: die normale Liste; dieselbe Liste mit `degraded: true`, wenn wegen Schema-Drift roh gelesen werden musste (dann entscheidet die Tabelle ueber die Felder); und eine LEERE Liste mit `warning`, wenn auch das scheiterte. Ohne diese Marken waere ein Datenbankproblem von „keine Meilensteine\" nicht zu unterscheiden.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"dueDate":{},"achievedAt":{},"status":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","projectId","title","description","status"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}},"degraded":{"type":"boolean","const":true}},"required":["data","degraded"],"additionalProperties":false},{"type":"object","properties":{"data":{"type":"array","items":{}},"warning":{"type":"string"}},"required":["data","warning"],"additionalProperties":false}]},"example":{"data":[{"id":"string","projectId":"string","title":"string","description":"string","status":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Project-milestones","tags":["Projects"],"parameters":[],"summary":"List milestones","description":"AK-407 — milestones with due-dates"},"post":{"responses":{"201":{"description":"Der angelegte Meilenstein, flach und vollstaendig.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"dueDate":{},"achievedAt":{},"status":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","projectId","title","description","status"],"additionalProperties":false},"example":{"id":"string","projectId":"string","title":"string","description":"string","status":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1Project-milestones","tags":["Projects"],"parameters":[],"description":"Legt einen Projekt-Meilenstein an. Pflicht sind `projectId`, `title` und `dueDate`; ohne Angabe startet er im Status `open`. Ob das genannte Projekt ueberhaupt existiert, prueft der Aufruf NICHT — ein Meilenstein kann ins Leere zeigen. Scheitert das Schreiben, antwortet die Route pauschal 503 mit dem rohen Fehlertext, auch bei fachlich falschen Werten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1},"description":{"type":"string"},"dueDate":{"type":"string"},"status":{"type":"string","enum":["open","achieved","missed"],"default":"open"}},"required":["projectId","title","dueDate"]},"example":{"projectId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","dueDate":"string","status":"open"}}}},"summary":"Legt einen Projekt-Meilenstein an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/project-milestones/{id}":{"put":{"responses":{"200":{"description":"Der geaenderte Meilenstein, vollstaendig — nicht nur die geaenderten Felder.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"dueDate":{},"achievedAt":{},"status":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","projectId","title","description","status"],"additionalProperties":false},"example":{"id":"string","projectId":"string","title":"string","description":"string","status":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"putApiV1Project-milestonesById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aendert einen Meilenstein. Geschrieben werden nur die mitgeschickten Felder, `updatedAt` zieht immer mit. `achievedAt` wird als Zeitpunkt uebernommen — den `status` setzt der Aufruf dabei NICHT automatisch auf `achieved`, das bleibt Sache des Aufrufers. Gesucht wird allein ueber die Kennung, ohne Projektbezug; eine unbekannte ergibt 404. Jeder Fehler beim Schreiben endet in 503 mit dem rohen Fehlertext.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1},"description":{"type":"string"},"dueDate":{"type":"string"},"status":{"type":"string","enum":["open","achieved","missed"],"default":"open"},"achievedAt":{"type":"string","format":"date-time"}}},"example":{"projectId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","dueDate":"string","status":"open","achievedAt":"2026-01-01T12:00:00.000Z"}}}},"summary":"Aendert einen Meilenstein","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Quittung — sagt NICHT, ob eine Zeile getroffen wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Unauthorized"}},"operationId":"deleteApiV1Project-milestonesById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Loescht einen Meilenstein endgueltig — kein `deleted_at`, kein Zurueckholen. Der Aufruf zaehlt die getroffenen Zeilen NICHT und quittiert deshalb immer mit `{ ok: true }`, auch wenn es die Kennung nie gab; ein 404 kommt hier nie. Jeder Fehler beim Loeschen endet in 503 mit dem rohen Fehlertext.","summary":"Loescht einen Meilenstein endgueltig — kein `deleted_at`, kein Zurueckholen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/project-resources":{"get":{"responses":{"200":{"description":"Liste der Zuordnungen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"resourceType":{"type":"string","description":"user | machine | material"},"resourceId":{"type":["string","null"]},"resourceName":{"type":["string","null"]},"allocationPct":{"type":"string","description":"Auslastung in Prozent, Dezimalzeichenkette"},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"costPerHour":{"type":["string","null"],"description":"Interner Kostensatz je Stunde"},"hourlyRate":{"type":["string","null"],"description":"Abrechenbarer Satz je Stunde auf Projektebene"},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","projectId","resourceType","resourceId","resourceName","allocationPct","startDate","endDate","costPerHour","hourlyRate","notes","createdAt","updatedAt"]}},"degraded":{"type":"boolean","description":"Gesetzt, wenn die Liste ueber die Ersatzabfrage kam"},"warning":{"type":"string","description":"Gesetzt, wenn auch die Ersatzabfrage scheiterte — data ist dann leer"}},"required":["data"]},"example":{"data":[{"id":"string","projectId":"string","resourceType":"string","resourceId":"string","resourceName":"string","allocationPct":"string","startDate":"string","endDate":"string","costPerHour":"string","hourlyRate":"string","notes":"string","createdAt":"string","updatedAt":"string"}],"degraded":true,"warning":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Project-resources","tags":["Projects"],"parameters":[],"summary":"Projekt-Ressourcen auflisten (Person, Maschine, Material)","description":"Listet die Ressourcen-Zuordnungen eines Mandanten aus `project_resources` (Person, Maschine, Material). Die Query-Parameter `projectId` und `resourceType` filtern, ohne Filter kommt die ganze Tabelle — es gibt weder Blaetterung noch Soft-Delete. Scheitert die Abfrage, antwortet der Endpunkt trotzdem mit 200: entweder ueber eine driftfeste Ersatzabfrage mit `degraded: true` (hoechstens 1000 Zeilen) oder mit leerer Liste und `warning`."},"post":{"responses":{"201":{"description":"Angelegte Zuordnung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"resourceType":{"type":"string","description":"user | machine | material"},"resourceId":{"type":["string","null"]},"resourceName":{"type":["string","null"]},"allocationPct":{"type":"string","description":"Auslastung in Prozent, Dezimalzeichenkette"},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"costPerHour":{"type":["string","null"],"description":"Interner Kostensatz je Stunde"},"hourlyRate":{"type":["string","null"],"description":"Abrechenbarer Satz je Stunde auf Projektebene"},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","projectId","resourceType","resourceId","resourceName","allocationPct","startDate","endDate","costPerHour","hourlyRate","notes","createdAt","updatedAt"]},"example":{"id":"string","projectId":"string","resourceType":"string","resourceId":"string","resourceName":"string","allocationPct":"string","startDate":"string","endDate":"string","costPerHour":"string","hourlyRate":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1Project-resources","tags":["Projects"],"parameters":[],"description":"Legt eine Ressourcen-Zuordnung an und gibt den erzeugten Datensatz zurueck — ohne Umschlag, Status 201. Pflicht sind `projectId` und `resourceType`; `allocationPct` steht ohne Angabe auf 100. Die Route prueft nicht, ob Projekt oder Ressource existieren, und laesst mehrere Zuordnungen derselben Ressource zum selben Projekt zu.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"resourceType":{"type":"string","enum":["user","machine","material"]},"resourceId":{"type":"string","format":"uuid"},"resourceName":{"type":"string"},"allocationPct":{"type":"number","minimum":0,"maximum":100,"default":100},"startDate":{"type":"string"},"endDate":{"type":"string"},"costPerHour":{"type":"number","minimum":0},"notes":{"type":"string"}},"required":["projectId","resourceType"]},"example":{"projectId":"00000000-0000-4000-8000-000000000000","resourceType":"user","resourceId":"00000000-0000-4000-8000-000000000000","resourceName":"string","allocationPct":0,"startDate":"string","endDate":"string","costPerHour":0,"notes":"string"}}}},"summary":"Legt eine Ressourcen-Zuordnung an und gibt den erzeugten Datensatz zurueck","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/project-resources/{id}":{"put":{"responses":{"200":{"description":"Geaenderte Zuordnung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"resourceType":{"type":"string","description":"user | machine | material"},"resourceId":{"type":["string","null"]},"resourceName":{"type":["string","null"]},"allocationPct":{"type":"string","description":"Auslastung in Prozent, Dezimalzeichenkette"},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"costPerHour":{"type":["string","null"],"description":"Interner Kostensatz je Stunde"},"hourlyRate":{"type":["string","null"],"description":"Abrechenbarer Satz je Stunde auf Projektebene"},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","projectId","resourceType","resourceId","resourceName","allocationPct","startDate","endDate","costPerHour","hourlyRate","notes","createdAt","updatedAt"]},"example":{"id":"string","projectId":"string","resourceType":"string","resourceId":"string","resourceName":"string","allocationPct":"string","startDate":"string","endDate":"string","costPerHour":"string","hourlyRate":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"putApiV1Project-resourcesById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aendert einzelne Felder einer Ressourcen-Zuordnung. Alle Felder des Rumpfs sind optional; nicht gesendete bleiben unveraendert, `updatedAt` wird immer neu gesetzt. Trifft die Kennung keine Zeile, kommt 404. Antwort ist der geaenderte Datensatz ohne Umschlag.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"resourceType":{"type":"string","enum":["user","machine","material"]},"resourceId":{"type":"string","format":"uuid"},"resourceName":{"type":"string"},"allocationPct":{"type":"number","minimum":0,"maximum":100,"default":100},"startDate":{"type":"string"},"endDate":{"type":"string"},"costPerHour":{"type":"number","minimum":0},"notes":{"type":"string"}}},"example":{"projectId":"00000000-0000-4000-8000-000000000000","resourceType":"user","resourceId":"00000000-0000-4000-8000-000000000000","resourceName":"string","allocationPct":0,"startDate":"string","endDate":"string","costPerHour":0,"notes":"string"}}}},"summary":"Aendert einzelne Felder einer Ressourcen-Zuordnung","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Zuordnung entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"Unauthorized"}},"operationId":"deleteApiV1Project-resourcesById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Entfernt die Zuordnung endgueltig aus `project_resources` — kein Soft-Delete, kein Wiederherstellen. Das Projekt und die Ressource selbst bleiben unberuehrt. Die Antwort ist auch dann `{ ok: true }`, wenn die Kennung keine Zeile getroffen hat.","summary":"Entfernt die Zuordnung endgueltig aus `project_resources`","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/project-phases":{"get":{"responses":{"200":{"description":"Phasen — `degraded` bzw. `warning` sagen, ob wirklich gelesen wurde","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"sortOrder":{"type":"integer"},"status":{"type":"string"},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","projectId","name","sortOrder","status","startDate","endDate","notes","createdAt","updatedAt"],"additionalProperties":true}},"degraded":{"type":"boolean","const":true},"warning":{"type":"string"}},"required":["data"]},"example":{"data":[{"id":"string","projectId":"string","name":"string","sortOrder":0,"status":"string","startDate":"string","endDate":"string","notes":"string","createdAt":"string","updatedAt":"string"}],"degraded":true,"warning":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Project-phases","tags":["Projects"],"parameters":[],"summary":"List project phases","description":"Phasenplan eines Projekts. Ohne `projectId` kommen ALLE Phasen des Mandanten, sortiert nach `sortOrder` aufsteigend — ohne Blaetterung und ohne weitere Filter. Die Sortierung ist projektuebergreifend, ein gemischtes Ergebnis ist also nicht nach Projekt gruppiert. Der Endpunkt scheitert nie mit 5xx: laesst sich die Tabelle nicht ueber das Datenmodell lesen, antwortet ein Ausweichpfad mit einer Rohabfrage (hoechstens 1000 Zeilen) und setzt `degraded: true`; scheitert auch der, kommt eine LEERE Liste mit `warning`. Eine leere Antwort ohne `warning` heiszt „keine Phasen\", mit `warning` heiszt sie „nicht gelesen\"."},"post":{"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"sortOrder":{"type":"integer"},"status":{"type":"string"},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","projectId","name","sortOrder","status","startDate","endDate","notes","createdAt","updatedAt"]},"example":{"id":"string","projectId":"string","name":"string","sortOrder":0,"status":"string","startDate":"string","endDate":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Anlegen fehlgeschlagen"}},"operationId":"postApiV1Project-phases","tags":["Projects"],"parameters":[],"summary":"Projektphase anlegen","description":"Legt eine Phase im Phasenplan an. Pflicht sind `projectId` und `name`; `sortOrder` faellt auf 0 und `status` auf \"planned\" zurueck. Die `projectId` wird NICHT gegen die Projekttabelle geprueft — eine Phase zu einem unbekannten Projekt wird angelegt. Der Sortierwert muss nicht eindeutig sein; bei gleichem Wert ist die Reihenfolge in der Liste offen. Die Kennung vergibt der Server.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1},"sortOrder":{"type":"integer","default":0},"status":{"type":"string","enum":["planned","active","done","skipped"],"default":"planned"},"startDate":{"type":"string"},"endDate":{"type":"string"},"notes":{"type":"string"}},"required":["projectId","name"]},"example":{"projectId":"00000000-0000-4000-8000-000000000000","name":"string","sortOrder":0,"status":"planned","startDate":"string","endDate":"string","notes":"string"}}}}}},"/api/v1/project-phases/{id}":{"put":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"sortOrder":{"type":"integer"},"status":{"type":"string"},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","projectId","name","sortOrder","status","startDate","endDate","notes","createdAt","updatedAt"]},"example":{"id":"string","projectId":"string","name":"string","sortOrder":0,"status":"string","startDate":"string","endDate":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"503":{"description":"Aktualisieren fehlgeschlagen"}},"operationId":"putApiV1Project-phasesById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Projektphase aktualisieren","description":"Teil-Update trotz PUT: geschrieben werden nur die gesendeten Felder, die uebrigen bleiben stehen. `updatedAt` wird immer neu gesetzt. Die Phase laesst sich ueber `projectId` in ein anderes Projekt umhaengen; daran haengende Aufgaben wandern NICHT mit. Ein Statuswechsel loest nichts aus — es werden weder Aufgaben noch das Projekt selbst angefasst. 404, wenn es die Phase nicht gibt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1},"sortOrder":{"type":"integer","default":0},"status":{"type":"string","enum":["planned","active","done","skipped"],"default":"planned"},"startDate":{"type":"string"},"endDate":{"type":"string"},"notes":{"type":"string"}}},"example":{"projectId":"00000000-0000-4000-8000-000000000000","name":"string","sortOrder":0,"status":"planned","startDate":"string","endDate":"string","notes":"string"}}}}},"delete":{"responses":{"200":{"description":"OK — es kann auch nichts getroffen worden sein","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"Unauthorized"},"503":{"description":"Loeschen fehlgeschlagen"}},"operationId":"deleteApiV1Project-phasesById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Projektphase löschen","description":"Entfernt die Phase ENDGUELTIG aus der Tabelle — kein Soft-Delete, kein Rueckgaengig. Aufgaben, die auf die Phase verweisen, werden dabei NICHT geloescht und nicht umgehaengt; ihr Phasenbezug zeigt danach ins Leere. Eine unbekannte Kennung ist kein Fehler: die Antwort ist auch dann 200 mit ok=true und beweist damit keine Loeschung."}},"/api/v1/project-time-entries":{"get":{"responses":{"200":{"description":"Time entries, newest date first. `degraded: true` means the rows came through the drift-proof raw query rather than the ORM. If `warning` is set, `data` is empty BECAUSE the table or the database was missing — not because nothing was booked, and `pagination` is then absent.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"}},"required":["limit","offset"],"additionalProperties":false},"degraded":{"type":"boolean"},"warning":{"type":"string"}},"required":["data"]},"example":{"data":[{}],"pagination":{"limit":0,"offset":0},"degraded":true,"warning":"string"}}}},"400":{"description":"Invalid query parameters"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Project-time-entries","tags":["Projects","TimeTracking"],"parameters":[{"in":"query","name":"projectId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"taskId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"userId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}},{"in":"query","name":"billable","schema":{"type":"string","enum":["true","false","1","0","yes","no","on","off"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":500,"default":100}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List time entries","description":"AK-409 — filter by project/user/date range"},"post":{"responses":{"201":{"description":"The stored entry plus `rateSource` — which step of the hierarchy the persisted `hourlyRate` came from (`explicit` when the request supplied it).","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"taskId":{"type":["string","null"]},"userId":{"type":"string"},"date":{"type":"string"},"hours":{"type":"string"},"description":{"type":["string","null"]},"billable":{"type":"boolean"},"hourlyRate":{"type":["string","null"]},"invoiceId":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"rateSource":{"type":"string"}},"required":["id","projectId","taskId","userId","date","hours","description","billable","hourlyRate","invoiceId","createdAt","updatedAt","rateSource"],"additionalProperties":false},"example":{"id":"string","projectId":"string","taskId":"string","userId":"string","date":"string","hours":"string","description":"string","billable":true,"hourlyRate":"string","invoiceId":"string","createdAt":"string","updatedAt":"string","rateSource":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"`unauthenticated` — no authenticated user to book against"},"404":{"description":"`project_not_found`"},"409":{"description":"`project_completed_frozen` — the project no longer accepts bookings"},"503":{"description":"Database unavailable — nothing was stored"}},"operationId":"postApiV1Project-time-entries","tags":["Projects","TimeTracking"],"parameters":[],"summary":"Create time entry","description":"AK-409 — book hours against project or task. W3: resolves billable rate via hierarchy (task > project-resource > user > project-default > tenant > 80) and persists it on the entry. Freezes new bookings on projects with status='completed'.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"taskId":{"type":"string","format":"uuid"},"date":{"type":"string","minLength":8},"hours":{"type":"number","exclusiveMinimum":0,"maximum":24},"description":{"type":"string"},"billable":{"type":"boolean","default":true},"hourlyRate":{"type":"number","minimum":0}},"required":["projectId","date","hours"]},"example":{"projectId":"00000000-0000-4000-8000-000000000000","taskId":"00000000-0000-4000-8000-000000000000","date":"stringxx","hours":1,"description":"string","billable":true,"hourlyRate":0}}}}}},"/api/v1/project-time-entries/{id}":{"put":{"responses":{"200":{"description":"The stored entry","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"taskId":{"type":["string","null"]},"userId":{"type":"string"},"date":{"type":"string"},"hours":{"type":"string"},"description":{"type":["string","null"]},"billable":{"type":"boolean"},"hourlyRate":{"type":["string","null"]},"invoiceId":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","projectId","taskId","userId","date","hours","description","billable","hourlyRate","invoiceId","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","projectId":"string","taskId":"string","userId":"string","date":"string","hours":"string","description":"string","billable":true,"hourlyRate":"string","invoiceId":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"503":{"description":"Database unavailable"}},"operationId":"putApiV1Project-time-entriesById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update a time entry","description":"Partial update: only the fields present in the body are written, and `updatedAt` is always refreshed. Unlike POST, this route does NOT re-resolve the billable rate and does NOT check the project phase — `hourlyRate` is taken as given, and hours can be moved onto a completed project. Ownership is not checked either, and neither is `invoice_id`: an entry already consumed by an invoice can still be edited. `userId` is not part of the body and therefore cannot be changed. An unknown id returns 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"taskId":{"type":"string","format":"uuid"},"date":{"type":"string","minLength":8},"hours":{"type":"number","exclusiveMinimum":0,"maximum":24},"description":{"type":"string"},"billable":{"type":"boolean","default":true},"hourlyRate":{"type":"number","minimum":0}}},"example":{"projectId":"00000000-0000-4000-8000-000000000000","taskId":"00000000-0000-4000-8000-000000000000","date":"stringxx","hours":1,"description":"string","billable":true,"hourlyRate":0}}}}},"delete":{"responses":{"200":{"description":"Receipt — it does NOT say whether the entry existed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"503":{"description":"Database unavailable"}},"operationId":"deleteApiV1Project-time-entriesById","tags":["Projects"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete a time entry","description":"Hard delete: the row is removed from `project_time_entries`, there is no `deleted_at`. The handler does not check whether a row was hit, so an unknown id is answered with `{ ok: true }` just the same. It does not look at `invoice_id` either — an entry already consumed by an invoice is deleted like any other. No role gate and no ownership check."}},"/api/v1/project-time-entries/summary":{"get":{"responses":{"200":{"description":"One row per project and `billable` value with `projectId`, `billable`, `totalHours` and `entryCount`. With `degraded: true` the rows are the RAW, un-aggregated entries from the drift-proof query — the caller has to aggregate them itself. If `warning` is set, `data` is empty BECAUSE table or database were missing.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}},"degraded":{"type":"boolean"},"warning":{"type":"string"}},"required":["data"]},"example":{"data":[{}],"degraded":true,"warning":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Project-time-entriesSummary","tags":["Projects","TimeTracking"],"parameters":[],"summary":"Time entry summary","description":"AK-409 — aggregated billable/non-billable hours"}},"/api/v1/invoices/currencies":{"get":{"responses":{"200":{"description":"Liste der Währungen","content":{"application/json":{"schema":{"type":"object","properties":{"base":{"type":"string","description":"Basiswaehrung"},"data":{"type":"object","additionalProperties":{},"description":"Kurse je Waehrung"},"updatedAt":{"type":"string","description":"Stand der Kurse (ISO)"}},"required":["base","data","updatedAt"]},"example":{"base":"string","data":{},"updatedAt":"string"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1InvoicesCurrencies","tags":["invoices"],"parameters":[],"summary":"List supported currencies","description":"Liefert die unterstützten Rechnungswährungen samt aktuellem Kurs zur Basis EUR. Reine Auskunft, mandantenunabhängig — es wird nichts umgerechnet und nichts gespeichert."}},"/api/v1/invoices/dunning":{"get":{"responses":{"200":{"description":"Mahnungen des Mandanten, neueste zuerst. Datumsfelder sind ISO-Zeitstempel (auch die Faelligkeit, obwohl sie in der Datenbank ein reines Datum ist). AUSNAHME: `naechsteStufeDatum` ist ein reiner Kalendertag `YYYY-MM-DD` — zusammen mit `naechsteStufeZielLevel` und `naechsteStufeTageVerbleibend` sagt es, wann der Mahnlauf diesen Vorgang in die naechste Stufe hebt.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"invoiceId":{"type":"string"},"invoiceNumber":{"type":["string","null"]},"customerName":{"type":["string","null"]},"level":{"type":"integer"},"amountOpen":{"type":"number"},"fees":{"type":"number"},"feesOpen":{"type":["number","null"]},"dueDate":{"type":["string","null"],"format":"date-time"},"sentAt":{"type":["string","null"],"format":"date-time"},"status":{"type":"string","enum":["open","sent","escalated","paid","cancelled"]},"sendError":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"naechsteStufeDatum":{"type":["string","null"]},"naechsteStufeZielLevel":{"type":["integer","null"]},"naechsteStufeTageVerbleibend":{"type":["integer","null"]}},"required":["id","tenantId","invoiceId","invoiceNumber","customerName","level","amountOpen","fees","feesOpen","dueDate","sentAt","status","sendError","notes","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","tenantId":"string","invoiceId":"string","invoiceNumber":"string","customerName":"string","level":0,"amountOpen":0,"fees":0,"feesOpen":0,"dueDate":"2026-01-01T12:00:00.000Z","sentAt":"2026-01-01T12:00:00.000Z","status":"open","sendError":"string","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","naechsteStufeDatum":"string","naechsteStufeZielLevel":0,"naechsteStufeTageVerbleibend":0}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"tenantId":"string","source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1InvoicesDunning","tags":["invoices"],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["open","sent","escalated","paid","cancelled"]}},{"in":"query","name":"level","schema":{"type":"number","minimum":1,"maximum":4}},{"in":"query","name":"invoiceId","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List dunning records","description":"Listet Mahnungen und Mahnstufen des Mandanten, neueste zuerst. Filter: status, level, invoiceId (letzteres liefert die Mahnhistorie einer einzelnen Rechnung)."},"post":{"responses":{"201":{"description":"Mahnung angelegt — der Datensatz nackt, ohne `data`-Hülle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"invoiceId":{"type":"string"},"invoiceNumber":{"type":["string","null"]},"customerName":{"type":["string","null"]},"level":{"type":"integer"},"amountOpen":{"type":"number"},"fees":{"type":"number"},"feesOpen":{"type":["number","null"]},"dueDate":{"type":["string","null"],"format":"date-time"},"sentAt":{"type":["string","null"],"format":"date-time"},"status":{"type":"string","enum":["open","sent","escalated","paid","cancelled"]},"sendError":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"naechsteStufeDatum":{"type":["string","null"]},"naechsteStufeZielLevel":{"type":["integer","null"]},"naechsteStufeTageVerbleibend":{"type":["integer","null"]},"documentId":{"type":["string","null"],"description":"DMS-Beleg des verschickten Mahnschreibens; null bei Stufe 1 oder Versandfehler"}},"required":["id","tenantId","invoiceId","invoiceNumber","customerName","level","amountOpen","fees","feesOpen","dueDate","sentAt","status","sendError","notes","createdAt","updatedAt","documentId"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","invoiceId":"string","invoiceNumber":"string","customerName":"string","level":0,"amountOpen":0,"fees":0,"feesOpen":0,"dueDate":"2026-01-01T12:00:00.000Z","sentAt":"2026-01-01T12:00:00.000Z","status":"open","sendError":"string","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","naechsteStufeDatum":"string","naechsteStufeZielLevel":0,"naechsteStufeTageVerbleibend":0,"documentId":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"},"409":{"description":"Diese Stufe ist zu der Rechnung bereits gemahnt — `dunning_exists`"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1InvoicesDunning","tags":["invoices"],"parameters":[],"summary":"Create dunning record","description":"Legt eine Mahnung zu einer Rechnung an (Status `sent`) UND verschickt sie über denselben Weg wie der Nachtlauf. Gebühr und Zahlungsfrist kommen aus den Mahnstufen-Einstellungen des Mandanten; `fees` im Body übersteuert die Gebühr für diesen Einzelfall. Die Frist zählt ab heute, nicht ab Fälligkeit der Rechnung. Je Rechnung und Stufe ist nur eine Mahnung möglich — ein zweiter Aufruf liefert 409 `dunning_exists` und mahnt nicht doppelt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string"},"level":{"type":"integer","minimum":1,"maximum":4,"default":1},"fees":{"type":"number","minimum":0},"notes":{"type":"string"}},"required":["invoiceId"]},"example":{"invoiceId":"string","level":1,"fees":0,"notes":"string"}}}}}},"/api/v1/invoices/{id}/dunning-enabled":{"get":{"responses":{"200":{"description":"Der wirksame Zustand samt Herkunft: `master` ist der Mandanten-Schalter, `override` die Abweichung fuer genau diesen Beleg. `enabled` ist das Ergebnis aus beidem — nur dieses Feld beantwortet die Frage „wird gemahnt?\".","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Wird dieser Beleg gemahnt?"},"master":{"type":"boolean","description":"Zustand am Mandanten-Schalter"},"override":{"type":["boolean","null"],"description":"Abweichung fuer diesen Beleg"},"reason":{"type":["string","null"],"description":"Begruendung der Abweichung"},"resumeOn":{"type":["string","null"],"description":"Ab wann wieder gemahnt wird (ISO)"},"hinweis":{"type":["string","null"],"description":"Klartext-Hinweis fuer die Maske"}},"required":["enabled","master","override","reason","resumeOn","hinweis"]},"example":{"enabled":true,"master":true,"override":true,"reason":"string","resumeOn":"string","hinweis":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Rechnung nicht gefunden"}},"operationId":"getApiV1InvoicesByIdDunning-enabled","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get dunning switch for invoice","description":"Sagt, ob dieser Beleg gemahnt wird — und woher die Entscheidung kommt"},"put":{"responses":{"200":{"description":"Der jetzt gueltige Zustand. `resumeOn` null bedeutet unbefristet gesperrt — ohne dieses Feld liesse sich eine befristete nicht von einer dauerhaften Sperre unterscheiden.","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"reason":{"type":["string","null"]},"resumeOn":{"type":["string","null"]}},"required":["enabled","reason","resumeOn"]},"example":{"enabled":true,"reason":"string","resumeOn":"string"}}}},"400":{"description":"Ungültige Eingabe"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle reicht nicht (Manager+ erforderlich)"},"404":{"description":"Rechnung nicht gefunden"}},"operationId":"putApiV1InvoicesByIdDunning-enabled","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Set dunning block for invoice","description":"Setzt oder loest die Mahnsperre fuer genau diesen Beleg (Manager+). `enabled: false` sperrt, optional mit Grund und Frist (`resumeOn`); `enabled: true` gibt wieder frei und loescht Grund und Frist mit. Der Schalter ueberstimmt den Mandanten-Master fuer diese eine Rechnung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"reason":{"type":["string","null"],"maxLength":300},"resumeOn":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["enabled"]},"example":{"enabled":true,"reason":"string","resumeOn":"2026-01-01"}}}}}},"/api/v1/invoices/dunning/candidates":{"get":{"responses":{"200":{"description":"Mahnkandidaten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Eintraege"},"meta":{"type":"object","additionalProperties":{},"description":"Kennzahlen zur Abfrage"}},"required":["data","meta"]},"example":{"data":[{}],"meta":{}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1InvoicesDunningCandidates","tags":["invoices"],"parameters":[],"summary":"List dunning candidates","description":"Liefert mahnfähige Rechnungen: fällig oder überfällig, nicht bezahlt/storniert/Entwurf, ohne laufende Mahnung und nicht per Mahnsperre ausgenommen. Sortiert nach ältester Fälligkeit, mit Vorschlagsstufe und Termin der ersten Mahnung. Reine Auskunft — es wird nichts gemahnt und nichts geschrieben. Höchstens 1000 Einträge; greift die Grenze, steht `meta.truncated: true` in der Antwort."}},"/api/v1/invoices/dunning/blocked":{"get":{"responses":{"200":{"description":"Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Datensaetze"}},"required":["data"]},"example":{"data":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1InvoicesDunningBlocked","tags":["invoices"],"parameters":[],"summary":"List invoices with dunning blocked","description":"Rechnungen mit ausgesetzter Mahnung (Grund + Frist) — also offene Forderungen, die das Mahnwesen bewusst auslässt. Nur explizite Sperren am Beleg, nicht der Mandanten-Master. Höchstens 200 Einträge, älteste Fälligkeit zuerst."}},"/api/v1/invoices/{id}/inkasso-paket":{"get":{"responses":{"200":{"description":"ZIP-Datei — rohe Bytes, kein JSON","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}},"headers":{"Content-Disposition":{"description":"Dateiname des Pakets","schema":{"type":"string"}},"X-Uebergabe-Vollstaendig":{"description":"1, wenn alle Pflichtangaben vorliegen, sonst 0 — das Paket kommt in beiden Faellen mit 200","schema":{"type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1InvoicesByIdInkasso-paket","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Uebergabepaket fuer ein Inkassobuero","description":"Liefert ein ZIP mit allem, was ein Inkassobuero zur Uebernahme der Forderung braucht: forderung.json (strukturierter Kern), forderung.csv, eine lesbare Aufstellung, die verschickten Mahnschreiben als Anlage und ein Manifest mit SHA-256 je Anlage. Anbieterneutral: es gibt keinen Standard fuer diese Strecke. Fehlende Pflichtangaben stehen als Warnung IM Paket, der Kopf X-Uebergabe-Vollstaendig sagt es vorab."}},"/api/v1/invoices/dunning/{id}/zuruecknehmen":{"post":{"responses":{"200":{"description":"Die zurückgenommene Mahnung unter `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"invoiceId":{"type":"string"},"invoiceNumber":{"type":["string","null"]},"customerName":{"type":["string","null"]},"level":{"type":"integer"},"amountOpen":{"type":"number"},"fees":{"type":"number"},"feesOpen":{"type":["number","null"]},"dueDate":{"type":["string","null"],"format":"date-time"},"sentAt":{"type":["string","null"],"format":"date-time"},"status":{"type":"string","enum":["open","sent","escalated","paid","cancelled"]},"sendError":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"naechsteStufeDatum":{"type":["string","null"]},"naechsteStufeZielLevel":{"type":["integer","null"]},"naechsteStufeTageVerbleibend":{"type":["integer","null"]},"documentId":{"type":["string","null"],"description":"DMS-Beleg des verschickten Mahnschreibens; null bei Stufe 1 oder Versandfehler"}},"required":["id","tenantId","invoiceId","invoiceNumber","customerName","level","amountOpen","fees","feesOpen","dueDate","sentAt","status","sendError","notes","createdAt","updatedAt","documentId"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"id":"string","tenantId":"string","invoiceId":"string","invoiceNumber":"string","customerName":"string","level":0,"amountOpen":0,"fees":0,"feesOpen":0,"dueDate":"2026-01-01T12:00:00.000Z","sentAt":"2026-01-01T12:00:00.000Z","status":"open","sendError":"string","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","naechsteStufeDatum":"string","naechsteStufeZielLevel":0,"naechsteStufeTageVerbleibend":0,"documentId":"string"}}}}},"400":{"description":"Validierungsfehler — `grund` länger als 500 Zeichen."},"401":{"description":"Nicht authentifiziert."},"403":{"description":"Keine Manager-Rolle oder kein Schreibrecht im Modul `invoices`."},"404":{"description":"Keine Mahnung mit dieser Kennung — `dunning_not_found`."},"409":{"description":"Nicht zurücknehmbar (`nicht_zuruecknehmbar`): höhere Stufe vorhanden, bereits zurückgenommen oder bereits bezahlt. `message` nennt den Grund im Klartext."},"503":{"description":"Datenbank nicht erreichbar ODER ein anderer Fehler — `db_unavailable`."}},"operationId":"postApiV1InvoicesDunningByIdZuruecknehmen","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Mahnung zurücknehmen","description":"Setzt EINE Mahnung auf `cancelled`, stellt ihre offene Mahngebühr auf 0 (`fees_open`; die ursprünglich berechnete `fees` bleibt als Beleg stehen) und hängt einen Vermerk mit Datum — und, falls mitgegeben, dem Grund — an die Notizen an.\n\nDIE RECHNUNG SELBST BLEIBT UNBERÜHRT. Hauptforderung, Status und Fälligkeit ändern sich nicht; zurückgenommen wird nur die Mahnung samt ihrer Gebühr. Für die Rechnung gibt es den eigenen Storno.\n\nNUR DIE HÖCHSTE STUFE. Gibt es zu derselben Rechnung eine höhere, noch nicht zurückgenommene Mahnstufe, kommt 409 — dann zuerst diese zurücknehmen. Ebenfalls 409 bei einer Mahnung, die bereits zurückgenommen (`cancelled`), bezahlt (`paid`) oder bereits eskaliert (`escalated`) ist. In allen drei Fällen wird nichts geändert.\n\nNICHT UMKEHRBAR: es gibt keine Route, die eine zurückgenommene Mahnung wieder aktiviert. Gemahnt wird stattdessen neu.\n\nWar es die Inkasso-Übergabe (Stufe 4 und höher), wird zusätzlich die Mahnsperre an der Rechnung gelöst — sonst bliebe sie liegen: nicht mehr übergeben, aber auch nie wieder gemahnt. Scheitert das Lösen, bleibt die Rücknahme trotzdem bestehen; die Rechnung ist dann weiter gesperrt, und es steht nur eine Warnung im Log.\n\nACHTUNG BEIM 503: der Sammel-Fang dieser Route meldet JEDEN unerwarteten Fehler als `db_unavailable`, auch wenn es kein Verbindungsproblem ist. Ein 503 hier ist kein Beweis für einen Datenbankausfall.\n\nMindestrolle `manager`, zusätzlich greift die Modul-Wache `invoices`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"grund":{"type":["string","null"],"maxLength":500}}},"example":{"grund":"string"}}}}}},"/api/v1/invoices/dunning/{id}/inkasso":{"post":{"responses":{"200":{"description":"Der aktualisierte Mahnvorgang. Die Uebergabe selbst passiert ausserhalb des Systems — festgehalten wird, DASS und WANN sie stattfand und an wen.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Eintraege"}},"required":["data"]},"example":{"data":[{}]}}}},"400":{"description":"Ungültige Eingabe"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle reicht nicht (Manager+ erforderlich)"},"404":{"description":"Mahnvorgang nicht gefunden"},"409":{"description":"Noch nicht uebergebbar (`nicht_uebergebbar`), bereits uebergeben (`bereits_uebergeben`) oder die Rechnung steht in einem Status, aus dem „in_collection\" laut Transition-Matrix nicht erreichbar ist (`invalid_status_transition`). Die ersten beiden gab die Route schon immer zurueck, nur stand keiner davon in der Spec."}},"operationId":"postApiV1InvoicesDunningByIdInkasso","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Record debt collection handover","description":"Haelt fest, dass eine Forderung an ein Inkasso uebergeben wurde (Manager+). Es wird NICHTS verschickt und kein Inkassobuero angebunden — die Uebergabe passiert ausserhalb des Systems, hier wird nur festgehalten, DASS und WANN sie stattfand und an wen. Nebenwirkungen: eine Mahnstufe „Inkasso\" entsteht, die Rechnung wird auf `in_collection` gesetzt und dauerhaft von der Mahnung ausgenommen. Antwortet mit 201, nicht mit 200.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"empfaenger":{"type":["string","null"],"maxLength":200},"notiz":{"type":["string","null"],"maxLength":1000}}},"example":{"empfaenger":"string","notiz":"string"}}}}}},"/api/v1/invoices/dunning/stats":{"get":{"responses":{"200":{"description":"Mahn-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"level1Count":{"type":"integer","description":"Belege in Stufe 1"},"level2Count":{"type":"integer","description":"Belege in Stufe 2"},"level3Count":{"type":"integer","description":"Belege in Stufe 3"},"inkassoCount":{"type":"integer","description":"An Inkasso uebergeben"},"totalOpen":{"type":"integer","description":"Anzahl offener Belege"},"totalAmountOpen":{"type":"number","description":"Offener Betrag gesamt"},"totalFees":{"type":"number","description":"Summe der Mahngebuehren"},"meta":{"type":"object","additionalProperties":{},"description":"Angaben zur Abfrage"}},"required":["level1Count","level2Count","level3Count","inkassoCount","totalOpen","totalAmountOpen","totalFees","meta"]},"example":{"level1Count":0,"level2Count":0,"level3Count":0,"inkassoCount":0,"totalOpen":0,"totalAmountOpen":0,"totalFees":0,"meta":{}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1InvoicesDunningStats","tags":["invoices"],"parameters":[],"summary":"Get dunning statistics","description":"Kennzahlen zum Mahnwesen: Anzahl je Mahnstufe, offene Beträge und Mahngebühren. Gezählt werden nur laufende Mahnungen (Status open/sent) — bezahlte und stornierte bleiben aussen vor."}},"/api/v1/invoices/dunning/auto-escalate":{"post":{"responses":{"200":{"description":"Auto-Eskalation erfolgreich","content":{"application/json":{"schema":{"type":"object","properties":{"dryRun":{"type":"boolean","description":"true = nur durchgerechnet, nichts erzeugt"},"candidates":{"type":"integer","description":"Gefundene Kandidaten"},"created":{"type":"integer","description":"Tatsaechlich erzeugte Mahnungen"},"items":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die betroffenen Belege"}},"required":["dryRun","candidates","created","items"]},"example":{"dryRun":true,"candidates":0,"created":0,"items":[{}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"}},"operationId":"postApiV1InvoicesDunningAuto-escalate","tags":["invoices"],"parameters":[],"summary":"Auto-escalate overdue invoices","description":"Legt für überfällige Rechnungen ohne laufende Mahnung je eine Mahnung in der passenden Stufe an und verschickt sie. `dryRun: true` liefert nur die Vorschau (`created: 0`, `items` gefüllt) und schreibt nichts. `maxLevel` deckelt die Stufe, `fees` übersteuert die Gebühren der Mandanten-Einstellung. Der Lauf sieht dieselbe Menge wie GET /invoices/dunning/candidates: alle fälligen, unbezahlten, nicht gesperrten und noch nicht gemahnten Rechnungen, älteste Fälligkeit zuerst. Höchstens 1000 je Lauf; greift die Grenze, steht `truncated: true` in der Antwort und ein zweiter Aufruf holt den Rest (bereits gemahnte Belege fallen heraus). Antwortet mit 200, auch wenn nichts angelegt wurde; bei einem Fehler 500 `auto_escalate_failed`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"dryRun":{"type":"boolean","default":false},"maxLevel":{"type":"integer","minimum":1,"maximum":4,"default":4},"fees":{"type":"object","additionalProperties":{"type":"number","minimum":0}}}},"example":{"dryRun":true,"maxLevel":1,"fees":{"beispiel":0}}}}}}},"/api/v1/invoices/dunning/smart-message":{"post":{"responses":{"200":{"description":"Generiertes Mahnschreiben (Betreff + Text)","content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string"},"invoiceNumber":{"type":"string"},"level":{"type":"integer","description":"Mahnstufe"},"daysOverdue":{"type":"integer","description":"Tage ueberfaellig"},"amountOpen":{"type":"number","description":"Offener Betrag"},"subject":{"type":"string","description":"Betreff"},"body":{"type":"string","description":"Nachrichtentext"},"source":{"type":"string","description":"Woher der Text stammt (Vorlage oder KI)"},"generatedAt":{"type":"string","description":"Zeitpunkt der Erzeugung (ISO)"}},"required":["invoiceId","invoiceNumber","level","daysOverdue","amountOpen","subject","body","source","generatedAt"]},"example":{"invoiceId":"string","invoiceNumber":"string","level":0,"daysOverdue":0,"amountOpen":0,"subject":"string","body":"string","source":"string","generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"}},"operationId":"postApiV1InvoicesDunningSmart-message","tags":["invoices","ai"],"parameters":[],"summary":"Generate dunning letter text","description":"Erzeugt Betreff und Text eines Mahnschreibens, im Ton der angegebenen Stufe. Der Text wird NUR zurückgegeben: es entsteht keine Mahnung, es wird nichts verschickt und an der Rechnung ändert sich nichts. Ist kein KI-Zugang konfiguriert, liefert der Handler trotzdem 200 — dann aber einen Textbaustein statt eines generierten Schreibens; `source` sagt, was es wirklich war (`template` oder das Modell).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string","minLength":1},"level":{"type":"integer","minimum":1,"maximum":4,"default":1},"language":{"type":"string","enum":["de","en"],"default":"de"},"senderName":{"type":"string","maxLength":200}},"required":["invoiceId"]},"example":{"invoiceId":"string","level":1,"language":"de","senderName":"string"}}}}}},"/api/v1/invoices/dunning/{id}/status":{"patch":{"responses":{"200":{"description":"Mahn-Status aktualisiert — der Datensatz nackt, ohne `data`-Hülle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"invoiceId":{"type":"string"},"invoiceNumber":{"type":["string","null"]},"customerName":{"type":["string","null"]},"level":{"type":"integer"},"amountOpen":{"type":"number"},"fees":{"type":"number"},"feesOpen":{"type":["number","null"]},"dueDate":{"type":["string","null"],"format":"date-time"},"sentAt":{"type":["string","null"],"format":"date-time"},"status":{"type":"string","enum":["open","sent","escalated","paid","cancelled"]},"sendError":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"naechsteStufeDatum":{"type":["string","null"]},"naechsteStufeZielLevel":{"type":["integer","null"]},"naechsteStufeTageVerbleibend":{"type":["integer","null"]},"documentId":{"type":["string","null"],"description":"DMS-Beleg des verschickten Mahnschreibens; null bei Stufe 1 oder Versandfehler"}},"required":["id","tenantId","invoiceId","invoiceNumber","customerName","level","amountOpen","fees","feesOpen","dueDate","sentAt","status","sendError","notes","createdAt","updatedAt","documentId"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","invoiceId":"string","invoiceNumber":"string","customerName":"string","level":0,"amountOpen":0,"fees":0,"feesOpen":0,"dueDate":"2026-01-01T12:00:00.000Z","sentAt":"2026-01-01T12:00:00.000Z","status":"open","sendError":"string","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","naechsteStufeDatum":"string","naechsteStufeZielLevel":0,"naechsteStufeTageVerbleibend":0,"documentId":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Mahnung nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"patchApiV1InvoicesDunningByIdStatus","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Change dunning status","description":"Setzt den Status einer Mahnung (sent/paid/cancelled). Bei `sent` wird der Versandzeitpunkt gestempelt; bei `paid` und `cancelled` fällt die offene Mahngebühr auf 0 — das ist der Weg, eine Gebühr zu erlassen. Der Status der zugehörigen RECHNUNG bleibt unberührt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["sent","paid","cancelled"]}},"required":["status"]},"example":{"status":"sent"}}}}}},"/api/v1/invoices/gaps":{"get":{"responses":{"200":{"description":"Lückenliste + Statistik","content":{"application/json":{"schema":{"type":"object","properties":{"gobd_compliant":{"type":"boolean","description":"Keine Luecke gefunden?"},"gap_count":{"type":"integer","description":"Anzahl Luecken"},"gaps":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Luecken"},"total_invoices":{"type":"integer","description":"Geprüfte Belege"},"expected_count":{"type":"integer","description":"Erwartete Anzahl"},"meta":{"type":"object","additionalProperties":{},"description":"Angaben zur Pruefung"}},"required":["gobd_compliant","gap_count","gaps","total_invoices","expected_count","meta"]},"example":{"gobd_compliant":true,"gap_count":0,"gaps":[{}],"total_invoices":0,"expected_count":0,"meta":{}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1InvoicesGaps","tags":["invoices"],"parameters":[],"summary":"Check invoice numbering for gaps","description":"GoBD-Lückenprüfung (§146 AO) über ALLE Rechnungen des Mandanten: meldet fehlende laufende Nummern je Nummernkreis. Gruppiert wird nach dem Präfix vor der Endziffernfolge, ein Jahreswechsel mit Rücksetzung auf 1 gilt deshalb nicht als Lücke. `gap_count > 0` heisst: die Nummerierung ist nicht lückenlos. Reine Prüfung, ändert nichts."}},"/api/v1/invoices/{id}/versions":{"get":{"responses":{"200":{"description":"Liste der Versionen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Datensaetze"}},"required":["data"]},"example":{"data":[{}]}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1InvoicesByIdVersions","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List invoice versions","description":"Listet alle revisionssicheren Versions-Snapshots einer Rechnung (alt → neu). Die Snapshots entstehen automatisch vor jeder Änderung an einer nicht mehr entwurfshaften Rechnung; `reason` nennt den Anlass. Append-only."}},"/api/v1/invoices/{id}/versions/{version}":{"get":{"responses":{"200":{"description":"Versions-Snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"version":{"type":"integer","description":"Fortlaufende Versionsnummer"},"reason":{"type":["string","null"],"description":"Warum diese Version entstand"},"snapshot":{"type":"object","additionalProperties":{},"description":"Der Stand zu diesem Zeitpunkt"},"createdAt":{"type":"string"},"createdBy":{"type":["string","null"]}},"required":["id","version","reason","snapshot","createdAt","createdBy"]},"example":{"id":"string","version":0,"reason":"string","snapshot":{},"createdAt":"string","createdBy":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Version nicht gefunden"}},"operationId":"getApiV1InvoicesByIdVersionsByVersion","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"version","required":true}],"summary":"Get invoice version","description":"Liefert einen einzelnen revisionssicheren Versions-Snapshot einer Rechnung. `version` ist die fortlaufende Nummer ab 1; alles andere liefert 400 `invalid_version`. Es gibt bewusst KEINEN Weg, eine Rechnungs-Version zurückzuschreiben (GoBD)."}},"/api/v1/invoices":{"get":{"responses":{"200":{"description":"Liste der Rechnungen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":["string","null"]},"customerId":{"type":["string","null"]},"status":{"type":["string","null"]},"positions":{"type":["array","null"],"items":{}},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"paidAmount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"invoiceDate":{"type":"null"},"dueDate":{"type":"null"},"paidAt":{"type":"null"},"lockedAt":{"type":"null"},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"customFields":{"type":["object","null"],"additionalProperties":{}},"leistungsTyp":{"type":["string","null"]},"customerName":{"type":"null"},"orderId":{"type":"null"},"title":{"type":"null"},"introText":{"type":"null"},"footerText":{"type":"null"},"notes":{"type":"null"},"leistungszeitraumVon":{"type":"null"},"leistungszeitraumBis":{"type":"null"},"lieferdatum":{"type":"null"},"leistungsdatum":{"type":"null"},"paymentTermsDays":{"type":"null"},"skontoDays":{"type":"null"},"skontoPercent":{"type":"null"},"language":{"type":"null"},"currency":{"type":["string","null"]},"salutation":{"type":"null"},"recipientName":{"type":"null"},"recipientStreet":{"type":"null"},"recipientZip":{"type":"null"},"recipientCity":{"type":"null"},"recipientCountry":{"type":"null"},"deliveryName":{"type":"null"},"deliveryCompany":{"type":"null"},"deliveryStreet":{"type":"null"},"deliveryZip":{"type":"null"},"deliveryCity":{"type":"null"},"deliveryCountry":{"type":"null"},"assignedToUserId":{"type":"null"},"assignedToName":{"type":"null"},"sourceDocumentId":{"type":"null"},"sourceDocumentType":{"type":"null"}},"required":["id"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"pages":{"type":"integer"}},"required":["page","limit","total","pages"]},"meta":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","number":"string","customerId":"string","status":"string","positions":[],"subtotal":"string","tax":"string","total":"string","paidAmount":"string","discount":"string","invoiceDate":null,"dueDate":null,"paidAt":null,"lockedAt":null,"createdAt":null,"updatedAt":null,"customFields":{},"leistungsTyp":"string","customerName":null,"orderId":null,"title":null,"introText":null,"footerText":null,"notes":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null,"language":null,"currency":"string","salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"sourceDocumentId":null,"sourceDocumentType":null}],"pagination":{"page":0,"limit":0,"total":0,"pages":0},"meta":{}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Invoices","tags":["invoices"],"parameters":[{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","sent","open","partially_paid","paid","overdue","in_collection","cancelled"]}},{"in":"query","name":"customerId","schema":{"type":"string"}},{"in":"query","name":"projectId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"search","schema":{"type":"string","minLength":1,"maxLength":120}},{"in":"query","name":"order","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}],"summary":"List invoices","description":"Listet die Rechnungen des Mandanten mit Filter und Paginierung (status, customerId, search auf die Belegnummer). Gelöschte Rechnungen erscheinen nie. Die Liste wird bis zu 5 Minuten zwischengespeichert; jede Änderung an einer Rechnung verwirft den Zwischenspeicher."},"post":{"responses":{"201":{"description":"Rechnung angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string","description":"Waehrung des neuen Belegs"}},"required":["currency"]},"example":{"currency":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"422":{"description":"`entity_rule_violation` — eine Feld-/Entscheidungsregel wurde verletzt (siehe entityRuleViolationBody)"},"423":{"description":"`period_closed` — die Buchungsperiode ist geschlossen (nur bei Anlage mit Status ungleich draft)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"invoices.create","tags":["invoices"],"parameters":[],"summary":"Create invoice","description":"Legt eine neue Rechnung an (Manager+ mit Schreibrecht auf die Buchhaltung). Die Belegnummer vergibt der zentrale Nummernkreis, die Summen rechnet der Server aus den Positionen. Wird die Rechnung nicht als Entwurf angelegt, bucht sie sofort ins Journal — trifft das eine geschlossene Buchungsperiode, bricht der Aufruf mit 423 `period_closed` ab. Eine verletzte Geschäftsregel liefert 422 vor jedem Schreiben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string"},"orderId":{"type":"string"},"projectId":{"type":["string","null"],"format":"uuid"},"title":{"type":"string"},"status":{"type":"string","enum":["draft","sent","open","partially_paid","paid","overdue","in_collection","cancelled"],"default":"draft"},"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"quantity":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"discount":{"type":"number","minimum":0,"maximum":100},"lineType":{"type":"string","enum":["standard","section","note"],"default":"standard"},"optional":{"type":"boolean"},"isAlternative":{"type":"boolean"},"articleId":{"type":["string","null"],"format":"uuid"}},"required":["quantity","unitPrice"]},"maxItems":1000,"default":[]},"subtotal":{"type":"number","minimum":0},"tax":{"type":"number","minimum":0},"total":{"type":"number","minimum":0},"dueDate":{"type":"string"},"paymentTermsDays":{"type":"integer","minimum":0,"maximum":365},"skontoDays":{"type":["integer","null"],"minimum":0,"maximum":365},"skontoPercent":{"type":["number","null"],"minimum":0,"maximum":100},"notes":{"type":"string"},"currency":{"type":"string","enum":["EUR","USD","GBP","CHF","JPY","PLN","CZK","HUF"]},"introText":{"type":"string"},"footerText":{"type":"string"},"priceMode":{"type":"string","enum":["net","gross"],"default":"net"},"salutation":{"type":"string"},"recipientName":{"type":"string"},"recipientStreet":{"type":"string"},"recipientZip":{"type":"string"},"recipientCity":{"type":"string"},"recipientCountry":{"type":"string"},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"assignedToUserId":{"type":["string","null"],"minLength":1},"leistungszeitraumVon":{"type":["string","null"],"format":"date"},"leistungszeitraumBis":{"type":["string","null"],"format":"date"},"lieferdatum":{"type":["string","null"],"format":"date"},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"],"format":"date"},"invoiceDate":{"type":"string","format":"date"},"language":{"type":"string","enum":["de","en","fr","es","pl","nl","da","cs","zh"]},"customFields":{"type":"object","additionalProperties":{}},"discount":{"type":"number","minimum":0,"maximum":100}},"required":["customerId","dueDate"]},"example":{"customerId":"string","orderId":"string","projectId":"00000000-0000-4000-8000-000000000000","title":"string","status":"draft","positions":[{"title":"string","description":"string","name":"string","quantity":0,"unit":"string","unitPrice":0,"taxRate":0,"discount":0,"lineType":"standard","optional":true,"isAlternative":true,"articleId":"00000000-0000-4000-8000-000000000000"}],"subtotal":0,"tax":0,"total":0,"dueDate":"string","paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"notes":"string","currency":"EUR","introText":"string","footerText":"string","priceMode":"net","salutation":"string","recipientName":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","assignedToUserId":"string","leistungszeitraumVon":"2026-01-01","leistungszeitraumBis":"2026-01-01","lieferdatum":"2026-01-01","leistungsTyp":"leistungsdatum","leistungsdatum":"2026-01-01","invoiceDate":"2026-01-01","language":"de","customFields":{},"discount":0}}}}}},"/api/v1/invoices/check-numbering-gaps":{"get":{"responses":{"200":{"description":"Lückenliste + Angabe, worauf die Aussage beruht","content":{"application/json":{"schema":{"type":"object","properties":{"gobdCompliant":{"type":"boolean","description":"Nur true, wenn ALLES geprueft werden konnte"},"gapCount":{"type":"integer","description":"Anzahl fehlender Nummern (auch wenn die Liste gekuerzt ist)"},"gaps":{"type":"array","items":{"type":"string"},"description":"Die fehlenden Belegnummern"},"total":{"type":"integer","description":"Belege im Zeitraum"},"checkedCount":{"type":"integer","description":"Davon wirklich geprueft"},"unrecognizedCount":{"type":"integer","description":"Belege ohne laufende Nummer — nicht pruefbar"},"unrecognizedSamples":{"type":"array","items":{"type":"string"},"description":"Bis zu fuenf davon beim Namen"},"ranges":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Je Nummernkreis: von, bis, Anzahl, geprueft"},"gapsTruncated":{"type":"boolean","description":"Liste gekuerzt? gapCount bleibt vollstaendig"},"basis":{"type":"string","description":"Worauf die Aussage beruht, im Klartext"},"from":{"type":["string","null"],"description":"Beginn des geprueften Bereichs"},"to":{"type":["string","null"],"description":"Ende des geprueften Bereichs"}},"required":["gobdCompliant","gapCount","gaps","total","checkedCount","unrecognizedCount","unrecognizedSamples","ranges","gapsTruncated","basis","from","to"]},"example":{"gobdCompliant":true,"gapCount":0,"gaps":["string"],"total":0,"checkedCount":0,"unrecognizedCount":0,"unrecognizedSamples":["string"],"ranges":[{}],"gapsTruncated":true,"basis":"string","from":"string","to":"string"}}}},"400":{"description":"from/to ist kein lesbares Datum"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1InvoicesCheck-numbering-gaps","tags":["invoices"],"parameters":[],"summary":"Check invoice numbering for gaps in a date range","description":"Prüft auf fehlende Rechnungsnummern (GoBD §146 AO) im über `?from=`/`?to=` begrenzten Zeitraum, über ALLE Belege des Zeitraums und über jeden Nummernkreis (gruppiert nach dem Präfix vor der Endziffernfolge, ein Jahreswechsel mit Rücksetzung auf 1 ist deshalb keine Lücke). `gobdCompliant: true` wird nur gesetzt, wenn wirklich alles geprüft werden konnte: keine Lücke, kein Beleg ohne laufende Nummer, kein übersprungener Nummernkreis und mindestens ein geprüfter Beleg. `basis` sagt im Klartext, worauf die Aussage beruht; `ranges`, `unrecognizedCount` und `gapsTruncated` liefern dieselbe Auskunft maschinenlesbar. Unlesbares `from`/`to` ergibt 400 (vorher: eine Zusage über null Belege)."}},"/api/v1/invoices/{id}":{"get":{"responses":{"200":{"description":"Rechnungs-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":["string","null"]},"customerId":{"type":["string","null"]},"status":{"type":["string","null"]},"positions":{"type":["array","null"],"items":{}},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"paidAmount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"invoiceDate":{"type":"null"},"dueDate":{"type":"null"},"paidAt":{"type":"null"},"lockedAt":{"type":"null"},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"customFields":{"type":["object","null"],"additionalProperties":{}},"leistungsTyp":{"type":["string","null"]},"customerName":{"type":"null"},"orderId":{"type":"null"},"title":{"type":"null"},"introText":{"type":"null"},"footerText":{"type":"null"},"notes":{"type":"null"},"leistungszeitraumVon":{"type":"null"},"leistungszeitraumBis":{"type":"null"},"lieferdatum":{"type":"null"},"leistungsdatum":{"type":"null"},"paymentTermsDays":{"type":"null"},"skontoDays":{"type":"null"},"skontoPercent":{"type":"null"},"language":{"type":"null"},"currency":{"type":["string","null"]},"salutation":{"type":"null"},"recipientName":{"type":"null"},"recipientStreet":{"type":"null"},"recipientZip":{"type":"null"},"recipientCity":{"type":"null"},"recipientCountry":{"type":"null"},"deliveryName":{"type":"null"},"deliveryCompany":{"type":"null"},"deliveryStreet":{"type":"null"},"deliveryZip":{"type":"null"},"deliveryCity":{"type":"null"},"deliveryCountry":{"type":"null"},"assignedToUserId":{"type":"null"},"assignedToName":{"type":"null"},"sourceDocumentId":{"type":"null"},"sourceDocumentType":{"type":"null"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","status":"string","positions":[],"subtotal":"string","tax":"string","total":"string","paidAmount":"string","discount":"string","invoiceDate":null,"dueDate":null,"paidAt":null,"lockedAt":null,"createdAt":null,"updatedAt":null,"customFields":{},"leistungsTyp":"string","customerName":null,"orderId":null,"title":null,"introText":null,"footerText":null,"notes":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null,"language":null,"currency":"string","salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"sourceDocumentId":null,"sourceDocumentType":null}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Rechnung nicht gefunden"}},"operationId":"getApiV1InvoicesById","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get invoice","description":"Liefert eine einzelne Rechnung anhand der ID, inklusive Positionen, Belegtexten, abweichender Empfänger-/Lieferadresse und aufgelöstem Kundennamen. Gelöschte Rechnungen liefern 404."},"put":{"responses":{"200":{"description":"Rechnung aktualisiert — der vollstaendige Datensatz, ohne Huelle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":["string","null"]},"customerId":{"type":["string","null"]},"status":{"type":["string","null"]},"positions":{"type":["array","null"],"items":{}},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"paidAmount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"invoiceDate":{"type":"null"},"dueDate":{"type":"null"},"paidAt":{"type":"null"},"lockedAt":{"type":"null"},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"customFields":{"type":["object","null"],"additionalProperties":{}},"leistungsTyp":{"type":["string","null"]},"customerName":{"type":"null"},"orderId":{"type":"null"},"title":{"type":"null"},"introText":{"type":"null"},"footerText":{"type":"null"},"notes":{"type":"null"},"leistungszeitraumVon":{"type":"null"},"leistungszeitraumBis":{"type":"null"},"lieferdatum":{"type":"null"},"leistungsdatum":{"type":"null"},"paymentTermsDays":{"type":"null"},"skontoDays":{"type":"null"},"skontoPercent":{"type":"null"},"language":{"type":"null"},"currency":{"type":["string","null"]},"salutation":{"type":"null"},"recipientName":{"type":"null"},"recipientStreet":{"type":"null"},"recipientZip":{"type":"null"},"recipientCity":{"type":"null"},"recipientCountry":{"type":"null"},"deliveryName":{"type":"null"},"deliveryCompany":{"type":"null"},"deliveryStreet":{"type":"null"},"deliveryZip":{"type":"null"},"deliveryCity":{"type":"null"},"deliveryCountry":{"type":"null"},"assignedToUserId":{"type":"null"},"assignedToName":{"type":"null"},"sourceDocumentId":{"type":"null"},"sourceDocumentType":{"type":"null"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","status":"string","positions":[],"subtotal":"string","tax":"string","total":"string","paidAmount":"string","discount":"string","invoiceDate":null,"dueDate":null,"paidAt":null,"lockedAt":null,"createdAt":null,"updatedAt":null,"customFields":{},"leistungsTyp":"string","customerName":null,"orderId":null,"title":null,"introText":null,"footerText":null,"notes":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null,"language":null,"currency":"string","salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"sourceDocumentId":null,"sourceDocumentType":null}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"},"409":{"description":"Voll-Edit an einer festgeschriebenen Rechnung (`invoice_not_editable`) oder ein unzulaessiger Statuswechsel"}},"operationId":"putApiV1InvoicesById","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace invoice","description":"Ersetzt die Inhalte einer Rechnung (Positionen, Preise, Kunde, Beleg-/Fälligkeitsdatum, Texte) und rechnet die Summen neu. GoBD-GRENZE: das geht NUR bei Entwürfen. Trägt der Body eines dieser Felder und ist die Rechnung bereits festgeschrieben (Status ungleich draft oder gesperrt), antwortet der Server mit 409 `invoice_not_editable` — Korrekturen laufen über Storno plus Korrektur-/Gutschriftrechnung. Ein reiner Status-Wechsel im Body wird gegen dieselbe Statusmatrix geprüft wie PATCH /invoices/{id}/status (409 bei unzulässigem Übergang).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","sent","open","partially_paid","paid","overdue","in_collection","cancelled"]},"paidAmount":{"type":"number","minimum":0},"paidAt":{"type":"string","format":"date-time"},"notes":{"type":"string"},"title":{"type":"string"},"projectId":{"type":["string","null"],"format":"uuid"},"salutation":{"type":"string"},"recipientName":{"type":"string"},"recipientStreet":{"type":"string"},"recipientZip":{"type":"string"},"recipientCity":{"type":"string"},"recipientCountry":{"type":"string"},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"assignedToUserId":{"type":["string","null"],"minLength":1},"leistungszeitraumVon":{"type":["string","null"],"format":"date"},"leistungszeitraumBis":{"type":["string","null"],"format":"date"},"lieferdatum":{"type":["string","null"],"format":"date"},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"],"format":"date"},"language":{"type":"string","enum":["de","en","fr","es","pl","nl","da","cs","zh"]},"currency":{"type":"string","enum":["EUR","USD","GBP","CHF","JPY","PLN","CZK","HUF"]},"customerId":{"type":"string","minLength":1},"customerName":{"type":"string"},"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"quantity":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"discount":{"type":"number","minimum":0,"maximum":100},"lineType":{"type":"string","enum":["standard","section","note"],"default":"standard"},"optional":{"type":"boolean"},"isAlternative":{"type":"boolean"},"articleId":{"type":["string","null"],"format":"uuid"}},"required":["quantity","unitPrice"]},"maxItems":1000,"default":[]},"priceMode":{"type":"string","enum":["net","gross"]},"introText":{"type":"string"},"footerText":{"type":"string"},"invoiceDate":{"type":"string","format":"date"},"dueDate":{"type":"string"},"paymentTermsDays":{"type":"integer","minimum":0,"maximum":365},"skontoDays":{"type":["integer","null"],"minimum":0,"maximum":365},"skontoPercent":{"type":["number","null"],"minimum":0,"maximum":100},"discount":{"type":"number","minimum":0,"maximum":100}}},"example":{"status":"draft","paidAmount":0,"paidAt":"2026-01-01T12:00:00.000Z","notes":"string","title":"string","projectId":"00000000-0000-4000-8000-000000000000","salutation":"string","recipientName":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","assignedToUserId":"string","leistungszeitraumVon":"2026-01-01","leistungszeitraumBis":"2026-01-01","lieferdatum":"2026-01-01","leistungsTyp":"leistungsdatum","leistungsdatum":"2026-01-01","language":"de","currency":"EUR","customerId":"string","customerName":"string","positions":[{"title":"string","description":"string","name":"string","quantity":0,"unit":"string","unitPrice":0,"taxRate":0,"discount":0,"lineType":"standard","optional":true,"isAlternative":true,"articleId":"00000000-0000-4000-8000-000000000000"}],"priceMode":"net","introText":"string","footerText":"string","invoiceDate":"2026-01-01","dueDate":"string","paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"discount":0}}}}},"patch":{"responses":{"200":{"description":"Rechnung aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":["string","null"]},"customerId":{"type":["string","null"]},"status":{"type":["string","null"]},"positions":{"type":["array","null"],"items":{}},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"paidAmount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"invoiceDate":{"type":"null"},"dueDate":{"type":"null"},"paidAt":{"type":"null"},"lockedAt":{"type":"null"},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"customFields":{"type":["object","null"],"additionalProperties":{}},"leistungsTyp":{"type":["string","null"]},"customerName":{"type":"null"},"orderId":{"type":"null"},"title":{"type":"null"},"introText":{"type":"null"},"footerText":{"type":"null"},"notes":{"type":"null"},"leistungszeitraumVon":{"type":"null"},"leistungszeitraumBis":{"type":"null"},"lieferdatum":{"type":"null"},"leistungsdatum":{"type":"null"},"paymentTermsDays":{"type":"null"},"skontoDays":{"type":"null"},"skontoPercent":{"type":"null"},"language":{"type":"null"},"currency":{"type":["string","null"]},"salutation":{"type":"null"},"recipientName":{"type":"null"},"recipientStreet":{"type":"null"},"recipientZip":{"type":"null"},"recipientCity":{"type":"null"},"recipientCountry":{"type":"null"},"deliveryName":{"type":"null"},"deliveryCompany":{"type":"null"},"deliveryStreet":{"type":"null"},"deliveryZip":{"type":"null"},"deliveryCity":{"type":"null"},"deliveryCountry":{"type":"null"},"assignedToUserId":{"type":"null"},"assignedToName":{"type":"null"},"sourceDocumentId":{"type":"null"},"sourceDocumentType":{"type":"null"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","status":"string","positions":[],"subtotal":"string","tax":"string","total":"string","paidAmount":"string","discount":"string","invoiceDate":null,"dueDate":null,"paidAt":null,"lockedAt":null,"createdAt":null,"updatedAt":null,"customFields":{},"leistungsTyp":"string","customerName":null,"orderId":null,"title":null,"introText":null,"footerText":null,"notes":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null,"language":null,"currency":"string","salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"sourceDocumentId":null,"sourceDocumentType":null}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"},"409":{"description":"`invoice_locked` — nur Entwürfe können editiert werden (GoBD: eine finalisierte Rechnung bleibt inhaltlich unveränderlich)"},"422":{"description":"`entity_rule_violation` — eine Feld-/Entscheidungsregel wurde verletzt (siehe entityRuleViolationBody)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"invoices.update","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update invoice","description":"Ändert einzelne Felder einer Rechnung (notes, due_date, paidAmount, customFields). Buchhaltungsrelevante Felder gehen NUR am Entwurf — an einer festgeschriebenen Rechnung liefert der Aufruf 409 `invoice_locked` (GoBD). AUSNAHME: ein Body, der ausschliesslich `customFields` trägt, ist GoBD-neutral und wird auch an versendeten/bezahlten Rechnungen mit 200 angenommen. Für `customFields` gilt: fehlender Schlüssel = behalten, `null` = löschen, Wert = setzen. Eine verletzte Geschäftsregel liefert 422 vor jedem Schreiben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string"},"due_date":{"type":"string","format":"date"},"paidAmount":{"type":"number","minimum":0},"customFields":{"type":"object","additionalProperties":{}}}},"example":{"notes":"string","due_date":"2026-01-01","paidAmount":0,"customFields":{}}}}}},"delete":{"responses":{"200":{"description":"Rechnung gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Ergebnis im Klartext"}},"required":["message"]},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Admin-Rolle"},"404":{"description":"Rechnung nicht gefunden"}},"operationId":"deleteApiV1InvoicesById","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Soft-delete draft invoice","description":"Setzt einen Löschstempel (`deleted_at`) — der Datensatz bleibt bestehen und lässt sich über POST /invoices/{id}/restore zurückholen. Nur Admin. GoBD: löschbar sind ausschliesslich ungesperrte ENTWÜRFE; eine festgeschriebene Rechnung liefert 409 `invoice_locked` und muss storniert oder per Gutschrift korrigiert werden. Nebenwirkungen: offene Mahnungen werden storniert, gematchte Bankbuchungen und abgerechnete Zeiten wieder freigegeben."}},"/api/v1/invoices/{id}/convert":{"get":{"responses":{"200":{"description":"Konvertierungs-Daten","content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string"},"from":{"type":"string","description":"Ausgangswaehrung"},"to":{"type":"string","description":"Zielwaehrung"},"rate":{"type":"number","description":"Verwendeter Kurs"},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"paidAmount":{"type":"number","description":"Bereits gezahlt, umgerechnet"}},"required":["invoiceId","from","to","rate","subtotal","tax","total","paidAmount"]},"example":{"invoiceId":"string","from":"string","to":"string","rate":0,"subtotal":0,"tax":0,"total":0,"paidAmount":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Rechnung nicht gefunden"}},"operationId":"getApiV1InvoicesByIdConvert","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert invoice amounts to another currency","description":"Rechnet die Beträge einer Rechnung in eine andere WÄHRUNG um — kein Formatwechsel und keine Belegumwandlung. Zielwährung über `?to=` (Vorgabe EUR), Ausgangswährung ist die am Beleg gespeicherte. Die Antwort ist eine reine Auskunft: die Rechnung bleibt unverändert, es wird nichts gespeichert. Eine nicht unterstützte Währung oder ein fehlender Kurs liefert 400 `unsupported_currency` — es wird bewusst kein geschätzter Betrag zurückgegeben."}},"/api/v1/invoices/{id}/status":{"patch":{"responses":{"200":{"description":"Status aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":["string","null"]},"customerId":{"type":["string","null"]},"status":{"type":["string","null"]},"positions":{"type":["array","null"],"items":{}},"subtotal":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tax":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"paidAmount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"discount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"invoiceDate":{"type":"null"},"dueDate":{"type":"null"},"paidAt":{"type":"null"},"lockedAt":{"type":"null"},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"customFields":{"type":["object","null"],"additionalProperties":{}},"leistungsTyp":{"type":["string","null"]},"customerName":{"type":"null"},"orderId":{"type":"null"},"title":{"type":"null"},"introText":{"type":"null"},"footerText":{"type":"null"},"notes":{"type":"null"},"leistungszeitraumVon":{"type":"null"},"leistungszeitraumBis":{"type":"null"},"lieferdatum":{"type":"null"},"leistungsdatum":{"type":"null"},"paymentTermsDays":{"type":"null"},"skontoDays":{"type":"null"},"skontoPercent":{"type":"null"},"language":{"type":"null"},"currency":{"type":["string","null"]},"salutation":{"type":"null"},"recipientName":{"type":"null"},"recipientStreet":{"type":"null"},"recipientZip":{"type":"null"},"recipientCity":{"type":"null"},"recipientCountry":{"type":"null"},"deliveryName":{"type":"null"},"deliveryCompany":{"type":"null"},"deliveryStreet":{"type":"null"},"deliveryZip":{"type":"null"},"deliveryCity":{"type":"null"},"deliveryCountry":{"type":"null"},"assignedToUserId":{"type":"null"},"assignedToName":{"type":"null"},"sourceDocumentId":{"type":"null"},"sourceDocumentType":{"type":"null"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","number":"string","customerId":"string","status":"string","positions":[],"subtotal":"string","tax":"string","total":"string","paidAmount":"string","discount":"string","invoiceDate":null,"dueDate":null,"paidAt":null,"lockedAt":null,"createdAt":null,"updatedAt":null,"customFields":{},"leistungsTyp":"string","customerName":null,"orderId":null,"title":null,"introText":null,"footerText":null,"notes":null,"leistungszeitraumVon":null,"leistungszeitraumBis":null,"lieferdatum":null,"leistungsdatum":null,"paymentTermsDays":null,"skontoDays":null,"skontoPercent":null,"language":null,"currency":"string","salutation":null,"recipientName":null,"recipientStreet":null,"recipientZip":null,"recipientCity":null,"recipientCountry":null,"deliveryName":null,"deliveryCompany":null,"deliveryStreet":null,"deliveryZip":null,"deliveryCity":null,"deliveryCountry":null,"assignedToUserId":null,"assignedToName":null,"sourceDocumentId":null,"sourceDocumentType":null}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"},"409":{"description":"`invalid_status_transition` — der Zielstatus ist von hier aus nicht erlaubt (GoBD-Statusmatrix); seltener ein Sperr-Konflikt aus der Repo-Schicht"},"423":{"description":"`period_closed` — die Buchungsperiode ist geschlossen (nur beim Aktivieren eines Entwurfs)"},"500":{"description":"`status_update_failed` — unerwarteter Fehler beim Statuswechsel (kein DB-Ausfall)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"invoices.updateStatus","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Change invoice status","description":"Setzt den Status einer Rechnung entlang der GoBD-Statusmatrix; Rückwärts-Übergänge ab sent/paid enden in 409. Nebenwirkungen: beim Verlassen des Entwurfs entsteht die Journalbuchung (geschlossene Periode → 423 `period_closed`, der Status bleibt dann stehen) und die Rechnung wird revisionssicher gesperrt; `paid` setzt den Zahlbetrag auf die Gesamtsumme und schliesst offene Mahnungen; `cancelled` storniert die Mahnungen, gibt gebuchte Zeiten und gematchte Bankbuchungen frei. Vor jeder Änderung wird ein Versions-Snapshot geschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","sent","open","partially_paid","paid","overdue","in_collection","cancelled"]}},"required":["status"]},"example":{"status":"draft"}}}}}},"/api/v1/invoices/bulk-close":{"post":{"responses":{"200":{"description":"Ergebnisreport (abgeschlossen / übersprungen)","content":{"application/json":{"schema":{"type":"object","properties":{"closedCount":{"type":"integer","description":"Abgeschlossene Belege"},"closed":{"type":"array","items":{"type":"string"},"description":"Ids der abgeschlossenen Belege"},"skippedCount":{"type":"integer","description":"Uebersprungene Belege"},"skipped":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Uebersprungen samt Grund"}},"required":["closedCount","closed","skippedCount","skipped"]},"example":{"closedCount":0,"closed":["string"],"skippedCount":0,"skipped":[{}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"}},"operationId":"postApiV1InvoicesBulk-close","tags":["invoices"],"parameters":[],"summary":"Mark multiple invoices as paid","description":"Setzt bis zu 200 Rechnungen auf `paid` (Zahlbetrag = Gesamtsumme, Zahldatum jetzt) — dieselbe Buchung wie der Einzelweg PATCH /invoices/{id}/status. NICHT storniert. Jede Rechnung wird für sich behandelt: Entwürfe, stornierte, bereits bezahlte, unbekannte und gesperrte Belege werden mit Grund ÜBERSPRUNGEN. Die Antwort ist immer 200 mit `closed`/`skipped` — ein leeres `closed` ist kein Fehler, sondern heisst, dass nichts abgeschlossen wurde. Vor jedem Wechsel entsteht ein Versions-Snapshot.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"invoiceIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":200}},"required":["invoiceIds"]},"example":{"invoiceIds":["string"]}}}}}},"/api/v1/invoices/{id}/payments":{"post":{"responses":{"200":{"description":"Zahlung erfasst","content":{"application/json":{"schema":{"type":"object","properties":{"fullyPaid":{"type":"boolean","description":"Ist der Beleg jetzt vollstaendig bezahlt?"},"remainingAmount":{"type":"number","description":"Noch offener Betrag"},"feesPaid":{"type":"boolean","description":"Sind die Mahngebuehren beglichen?"}},"required":["fullyPaid","remainingAmount","feesPaid"]},"example":{"fullyPaid":true,"remainingAmount":0,"feesPaid":true}}}},"400":{"description":"Validierungsfehler (Betrag <= 0)"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"},"409":{"description":"Zahlung in diesem Status nicht möglich (draft/paid/cancelled)"}},"operationId":"postApiV1InvoicesByIdPayments","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Record invoice payment","description":"Bucht eine Teil- oder Restzahlung auf eine Rechnung: der Zahlbetrag wird aufaddiert und der Status daraus abgeleitet (`partially_paid`, bei vollem Ausgleich `paid`; Mahn-/Inkassostatus bleiben bei Teilzahlung erhalten). Offene MAHNGEBÜHREN werden zuerst getilgt (§ 367 BGB) — deshalb kann nach einer Zahlung „in Höhe der Rechnung\" ein Rest offen bleiben; `feesPaid` und `remainingAmount` in der Antwort machen das nachvollziehbar. Positionen und Beträge werden nie verändert, gesperrte Rechnungen dürfen Zahlungen empfangen. Entwürfe, bereits bezahlte und stornierte Rechnungen liefern 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"amount":{"type":"number","exclusiveMinimum":0},"date":{"type":"string"},"note":{"type":"string"}},"required":["amount"]},"example":{"amount":1,"date":"string","note":"string"}}}}}},"/api/v1/invoices/{id}/restore":{"post":{"responses":{"200":{"description":"Rechnung wiederhergestellt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"id":{"type":"string"}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Admin-Rolle"},"404":{"description":"Rechnung nicht gefunden oder nicht gelöscht"},"409":{"description":"Nur Entwürfe können wiederhergestellt werden"}},"operationId":"postApiV1InvoicesByIdRestore","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Restore deleted draft invoice","description":"Nimmt den Löschstempel einer Entwurfs-Rechnung zurück. Reines Rückgängigmachen — es entsteht kein Storno und kein Statuswechsel. Eine Rechnung, die nicht gelöscht ist, liefert 404; alles ausser einem ungesperrten Entwurf 409."}},"/api/v1/invoices/{id}/pdf":{"get":{"responses":{"200":{"description":"PDF-Stream — Cache-Treffer (Zeile ~5824 ff.) oder frisch gerendert (Zeile ~5965 ff.)","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}},"headers":{"Content-Disposition":{"description":"Dateiname zum Download, z. B. attachment; filename=\"Rechnung-<Nummer>.pdf\"","schema":{"type":"string"}},"X-ZUGFeRD-Profile":{"description":"Eingebettetes ZUGFeRD/Factur-X-Profil der XML (aktuell immer EN16931)","schema":{"type":"string"}},"X-PDF-Engine":{"description":"Renderpfad dieser Antwort: cache (aus dem Cache bedient) oder frisch mit react-pdf/custom/zugferd-legacy (Notfall-Renderer)","schema":{"type":"string"}},"X-PDF-Cache":{"description":"Herkunft der Bytes: Cache-Treffer (memory/s3/r2/local) oder miss bei frischem Rendering","schema":{"type":"string"}},"ETag":{"description":"Schwacher ETag für If-None-Match/304. Fehlt nur, wenn frisch UND über den Notfall-Renderer zugferd-legacy erzeugt wurde","schema":{"type":"string"}}}},"304":{"description":"Not modified — ETag matches","headers":{"ETag":{"description":"Derselbe ETag wie im If-None-Match-Request-Header","schema":{"type":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Rechnung nicht gefunden"},"500":{"description":"`Failed to generate PDF` — echter Rendering-Fehler; Wiederholen hilft nicht"},"503":{"description":"Datenbank nicht erreichbar (database_unavailable, Kopf `Retry-After`) — voruebergehend, Wiederholen sinnvoll"}},"operationId":"invoices.pdf","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Download invoice PDF","description":"Liefert die Rechnung als PDF-Datei mit eingebettetem ZUGFeRD/Factur-X-XML (EN16931) — rohe Bytes mit `Content-Type: application/pdf`, KEINE JSON-Antwort. Ergebnisse werden gecacht; `If-None-Match` mit dem ETag beantwortet der Server mit 304. Optional `?lang=` für die Belegsprache (jede Sprache wird getrennt gecacht). Die Kopfzeilen X-PDF-Engine und X-PDF-Cache sagen, woher die Bytes kommen."}},"/api/v1/invoices/{id}/send":{"post":{"responses":{"200":{"description":"Rechnung versendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"messageId":{"type":["string","null"],"description":"Id beim Mailversender"},"recipients":{"type":"array","items":{"type":"string"},"description":"Tatsaechliche Empfaenger"},"sentAt":{"type":"string","description":"Zeitpunkt (ISO)"},"simuliert":{"type":"boolean","description":"true = nur simuliert, es ging keine Mail raus"}},"required":["ok","messageId","recipients","sentAt","simuliert"]},"example":{"ok":true,"messageId":"string","recipients":["string"],"sentAt":"string","simuliert":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"},"502":{"description":"E-Mail-Versand fehlgeschlagen"}},"operationId":"postApiV1InvoicesByIdSend","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Send invoice by email","description":"Versendet die Rechnung per E-Mail an den Kunden, standardmaessig mit dem ZUGFeRD-PDF im Anhang (`attachPdf: false` laesst ihn weg). War die Rechnung ein Entwurf, steht sie danach auf `sent`. WICHTIG: das Antwortfeld `simuliert` sagt, ob wirklich eine Mail hinausging — auf Umgebungen mit Mail-Attrappe ist es `true` und es wurde NICHTS versendet, obwohl die Antwort 200 mit `ok: true` lautet. 402 bei erschoepftem Mail-Kontingent, 502 wenn der Mailversender ablehnt (das Kontingent wird dann zurueckgebucht).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"cc":{"type":"array","items":{"type":"string","format":"email"}},"bcc":{"type":"array","items":{"type":"string","format":"email"}},"subject":{"type":"string","minLength":1,"maxLength":255},"message":{"type":"string","maxLength":4000},"attachPdf":{"type":"boolean"},"includePortalLink":{"type":"boolean","default":true},"lang":{"type":"string","enum":["de","en","fr","es","nl","da","pl","cs","zh"]},"template":{"type":"string","enum":["doc-quote","doc-order","doc-delivery","doc-invoice","dunning-level1","dunning-level2","dunning-level3"]},"extraAttachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"contentBase64":{"type":"string","minLength":1},"contentType":{"type":"string","maxLength":100}},"required":["filename","contentBase64"]},"maxItems":10},"attachmentMode":{"type":"string","enum":["separate","merge"]}},"required":["to"]},"example":{"to":["beispiel@example.com"],"cc":["beispiel@example.com"],"bcc":["beispiel@example.com"],"subject":"string","message":"string","attachPdf":true,"includePortalLink":true,"lang":"de","template":"doc-quote","extraAttachments":[{"filename":"string","contentBase64":"string","contentType":"string"}],"attachmentMode":"separate"}}}}}},"/api/v1/invoices/{id}/dunning":{"post":{"responses":{"200":{"description":"Mahnung versendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"messageId":{"type":["string","null"],"description":"Id beim Mailversender"},"recipients":{"type":"array","items":{"type":"string"},"description":"Tatsaechliche Empfaenger"},"sentAt":{"type":"string","description":"Zeitpunkt (ISO)"},"simuliert":{"type":"boolean","description":"true = nur simuliert, es ging keine Mail raus"},"level":{"type":"integer","description":"Erreichte Mahnstufe"},"feeApplied":{"type":"number","description":"Berechnete Mahngebuehr"},"newDeadline":{"type":"string","description":"Neue Zahlungsfrist (ISO)"},"totalDueEur":{"type":"number","description":"Gesamtforderung inkl. Gebuehren"}},"required":["ok","messageId","recipients","sentAt","simuliert","level","feeApplied","newDeadline","totalDueEur"]},"example":{"ok":true,"messageId":"string","recipients":["string"],"sentAt":"string","simuliert":true,"level":0,"feeApplied":0,"newDeadline":"string","totalDueEur":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"},"502":{"description":"E-Mail-Versand fehlgeschlagen"}},"operationId":"postApiV1InvoicesByIdDunning","tags":["invoices"],"parameters":[{"in":"query","name":"level","schema":{"type":"integer","minimum":1,"maximum":3,"default":1}},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Send dunning letter by email","description":"Versendet ein Mahnschreiben (Stufe 1/2/3, `?level=`) per E-Mail an den Kunden. Was dabei NICHT passiert: es entsteht KEINE Mahnung in der Mahnliste und kein Eintrag in den Mahn-Kennzahlen (dafuer ist POST /invoices/dunning da), der Rechnungsstatus bleibt unveraendert, und der PDF-Anhang wird zwar erzeugt, aber nicht mitgeschickt — die Mail geht ohne Beleg hinaus, auch bei `attachPdf: true`. Die ausgewiesene Mahngebuehr stammt aus einer festen Staffel (0/5/15 EUR), NICHT aus den Mahnstufen-Einstellungen des Mandanten. WICHTIG: das Antwortfeld `simuliert` sagt, ob wirklich eine Mail hinausging — auf Umgebungen mit Mail-Attrappe ist es `true` und es wurde nichts versendet. Entwuerfe werden mit 409 `invoice_draft` abgelehnt, ein erschoepftes Mail-Kontingent mit 402.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"cc":{"type":"array","items":{"type":"string","format":"email"}},"bcc":{"type":"array","items":{"type":"string","format":"email"}},"subject":{"type":"string","minLength":1,"maxLength":255},"message":{"type":"string","maxLength":4000},"attachPdf":{"type":"boolean"},"includePortalLink":{"type":"boolean","default":true},"lang":{"type":"string","enum":["de","en","fr","es","nl","da","pl","cs","zh"]},"template":{"type":"string","enum":["doc-quote","doc-order","doc-delivery","doc-invoice","dunning-level1","dunning-level2","dunning-level3"]},"extraAttachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"contentBase64":{"type":"string","minLength":1},"contentType":{"type":"string","maxLength":100}},"required":["filename","contentBase64"]},"maxItems":10},"attachmentMode":{"type":"string","enum":["separate","merge"]}},"required":["to"]},"example":{"to":["beispiel@example.com"],"cc":["beispiel@example.com"],"bcc":["beispiel@example.com"],"subject":"string","message":"string","attachPdf":true,"includePortalLink":true,"lang":"de","template":"doc-quote","extraAttachments":[{"filename":"string","contentBase64":"string","contentType":"string"}],"attachmentMode":"separate"}}}}}},"/api/v1/invoices/{id}/zugferd":{"get":{"responses":{"200":{"description":"ZUGFeRD-Datensatz — hybride PDF/A-3-Bytes, kein JSON","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}},"headers":{"Content-Disposition":{"description":"Dateiname zum Download","schema":{"type":"string"}},"X-ZUGFeRD-Profile":{"description":"Das erzeugte Profil — der Wert aus `?profile=`, sonst EN16931","schema":{"type":"string"}},"X-ZUGFeRD-Valid":{"description":"1, wenn das eingebettete XML die Pruefung bestanden hat, sonst 0 — die Bytes kommen in beiden Faellen mit 200","schema":{"type":"string"}}}},"400":{"description":"Unbekanntes Profil in `?profile=`"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Rechnung nicht gefunden"}},"operationId":"getApiV1InvoicesByIdZugferd","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Download ZUGFeRD hybrid PDF","description":"Liefert die Rechnung als hybride PDF/A-3-DATEI mit eingebettetem ZUGFeRD-XML — rohe Bytes mit `Content-Type: application/pdf`, KEINE JSON- und keine reine XML-Antwort (das eigenständige XML liefert GET /invoices/{id}/xrechnung). Profil über `?profile=` (Vorgabe EN16931); ein unbekanntes Profil liefert 400. Die Kopfzeile `X-ZUGFeRD-Valid` sagt, ob das erzeugte XML die Prüfung bestanden hat — bei `0` kommen die Bytes trotzdem mit 200 zurück."}},"/api/v1/invoices/{id}/validate":{"post":{"responses":{"200":{"description":"Validierungsergebnis { valid, errors, warnings, profile }","content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string"},"invoiceNumber":{"type":"string"},"checkedAt":{"type":"string","description":"Zeitpunkt der Pruefung (ISO)"}},"required":["invoiceId","invoiceNumber","checkedAt"]},"example":{"invoiceId":"string","invoiceNumber":"string","checkedAt":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Rechnung nicht gefunden"}},"operationId":"postApiV1InvoicesByIdValidate","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Validate invoice against EN 16931","description":"Prüft eine Rechnung gegen die EN-16931-/ZUGFeRD-Regeln und liefert Fehler und Warnungen als Liste. Trotz POST wird nichts geschrieben: die Rechnung bleibt unverändert, das Ergebnis wird nicht gespeichert. `valid: false` ist ein gültiges Ergebnis mit Status 200, kein Fehler."}},"/api/v1/invoices/{id}/xrechnung":{"get":{"responses":{"200":{"description":"XRechnung XML — Rohtext, kein JSON und kein PDF","content":{"application/xml":{"schema":{"type":"string"}}},"headers":{"Content-Disposition":{"description":"Dateiname zum Download, `xrechnung-<Nummer>.xml`","schema":{"type":"string"}},"X-Invoice-Standard":{"description":"Fest `XRechnung-3.0` — auch dann, wenn der Handler auf das ZUGFeRD-XML ausweichen musste","schema":{"type":"string"}},"X-Invoice-Syntax":{"description":"Fest `UN-CEFACT-CII`","schema":{"type":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Rechnung nicht gefunden"},"500":{"description":"XML konnte nicht erzeugt werden"}},"operationId":"getApiV1InvoicesByIdXrechnung","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Download XRechnung 3.0 XML","description":"Liefert die Rechnung als eigenständige XRechnung-3.0-XML-Datei (B2G-Pflichtformat, EN16931, UN/CEFACT-CII) — Rohtext mit `Content-Type: application/xml`, KEINE JSON-Antwort und kein PDF. Ohne den XRechnung-Erzeuger fällt der Handler auf das ZUGFeRD-XML zurück; die Antwort bleibt dann 200, die Kopfzeilen behaupten aber weiterhin `X-Invoice-Standard: XRechnung-3.0`."}},"/api/v1/invoices/{id}/positions":{"patch":{"responses":{"200":{"description":"Positionen aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string","description":"Belegnummer"},"status":{"type":"string"},"positions":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Positionen"},"position_count":{"type":"integer"},"subtotal":{"type":"number"},"discount":{"type":"number","description":"Kopfrabatt"},"tax":{"type":"number"},"total":{"type":"number"},"taxNote":{"type":["string","null"],"description":"Pflichthinweis, z. B. bei Steuerbefreiung"},"priceMode":{"type":"string","description":"netto oder brutto"},"updatedAt":{"type":"string"}},"required":["id","number","status","positions","position_count","subtotal","discount","tax","total","taxNote","priceMode","updatedAt"]},"example":{"id":"string","number":"string","status":"string","positions":[{}],"position_count":0,"subtotal":0,"discount":0,"tax":0,"total":0,"taxNote":"string","priceMode":"string","updatedAt":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"},"409":{"description":"Rechnung festgeschrieben (GoBD) — nur Entwürfe editierbar"}},"operationId":"patchApiV1InvoicesByIdPositions","tags":["invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace invoice positions","description":"Ersetzt NUR die Positionsliste einer Rechnung; Kunde, Texte, Netto-/Brutto-Modus und Beleg-Rabatt bleiben unangetastet, die Summen werden neu gerechnet. Die Liste wird ersetzt, nicht ergänzt. GoBD-GRENZE wie beim PUT: nur ungesperrte ENTWÜRFE — eine festgeschriebene Rechnung liefert 409 `invoice_not_editable`, Korrekturen laufen über Storno plus Korrektur-/Gutschriftrechnung. Ein zwischengespeichertes PDF wird verworfen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"quantity":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"discount":{"type":"number","minimum":0,"maximum":100},"lineType":{"type":"string","enum":["standard","section","note"],"default":"standard"},"optional":{"type":"boolean"},"isAlternative":{"type":"boolean"},"articleId":{"type":["string","null"],"format":"uuid"}},"required":["quantity","unitPrice"]},"maxItems":1000,"default":[]}}},"example":{"positions":[{"title":"string","description":"string","name":"string","quantity":0,"unit":"string","unitPrice":0,"taxRate":0,"discount":0,"lineType":"standard","optional":true,"isAlternative":true,"articleId":"00000000-0000-4000-8000-000000000000"}]}}}}}},"/api/v1/workflows/pending-approvals":{"get":{"responses":{"200":{"description":"Offene Freigaben. `meta.source` unterscheidet „nichts zu tun\" (db) von „konnte nicht nachsehen\" (unavailable) — beide liefern eine leere Liste.","content":{"application/json":{"schema":{"type":"object","properties":{"approvals":{"type":"array","items":{"type":"object","properties":{"runId":{"type":"string"},"workflowId":{"type":"string"},"workflowName":{"type":["string","null"]},"status":{"type":"string"},"triggerData":{},"startedAt":{},"createdAt":{}},"required":["runId","workflowId","workflowName","status"],"additionalProperties":false}},"total":{"type":"number"},"meta":{"type":"object","properties":{"source":{"type":"string","enum":["db","unavailable"]},"tenantId":{"type":"string"},"note":{"type":"string"}},"required":["source"],"additionalProperties":false}},"required":["approvals","total","meta"],"additionalProperties":false},"example":{"approvals":[{"runId":"string","workflowId":"string","workflowName":"string","status":"string"}],"total":0,"meta":{"source":"db","tenantId":"string","note":"string"}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1WorkflowsPending-approvals","tags":["workflows"],"parameters":[],"summary":"List runs awaiting approval","description":"Workflow-Runs auflisten, deren aktueller Schritt eine Approval-Aktion ist und auf Entscheidung wartet. Höchstens 100 Einträge, älteste zuerst; die Liste ist NICHT auf den Aufrufer gefiltert, sie zeigt alle wartenden Läufe des Mandanten. Ohne Datenbankverbindung antwortet die Route 200 mit leerer Liste und meta.source=unavailable — das ist kein „nichts zu tun\"."}},"/api/v1/workflows/runs/{runId}/approve":{"post":{"responses":{"200":{"description":"Run genehmigt","content":{"application/json":{"schema":{"type":"object","properties":{"runId":{"type":"string"},"decision":{"type":"string","const":"approved"},"comment":{"type":["string","null"]}},"required":["runId","decision","comment"],"additionalProperties":false},"example":{"runId":"string","decision":"approved","comment":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Nicht in approvers-Liste","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}},"404":{"description":"Run nicht im Wait-Status","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Unerwarteter Fehler — error traegt die technische Meldung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}},"503":{"description":"DB nicht verfuegbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1WorkflowsRunsByRunIdApprove","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"runId","required":true}],"summary":"Approve a waiting workflow run","description":"Wartenden Workflow-Run genehmigen und Ausfuehrung fortsetzen. Erfordert Rolle manager oder hoeher; traegt der Approval-Schritt eine approvers-Liste, muss der Aufrufer darin stehen (sonst 403). Die Antwort ist eine Quittung, kein Ergebnis: 200 heisst „Entscheidung gespeichert und Fortsetzung angestossen\", nicht „Workflow fertig\" — die restlichen Schritte laufen im Hintergrund. Nur Laeufe im Status pending_approval sind entscheidbar, sonst 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","maxLength":2000}}},"example":{"comment":"string"}}}}}},"/api/v1/workflows/runs/{runId}/reject":{"post":{"responses":{"200":{"description":"Run abgelehnt","content":{"application/json":{"schema":{"type":"object","properties":{"runId":{"type":"string"},"decision":{"type":"string","const":"rejected"},"reason":{"type":"string"},"comment":{"type":["string","null"]}},"required":["runId","decision","reason","comment"],"additionalProperties":false},"example":{"runId":"string","decision":"rejected","reason":"string","comment":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Nicht in approvers-Liste","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}},"404":{"description":"Run nicht im Wait-Status","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Unerwarteter Fehler — error traegt die technische Meldung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}},"503":{"description":"DB nicht verfuegbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1WorkflowsRunsByRunIdReject","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"runId","required":true}],"summary":"Reject a waiting workflow run","description":"Wartenden Workflow-Run ablehnen mit Begruendung (reason ist Pflicht). Die Ablehnung ist ENDGUELTIG: der Lauf endet im Status rejected, die folgenden Schritte laufen nie. Erfordert Rolle manager oder hoeher; traegt der Approval-Schritt eine approvers-Liste, muss der Aufrufer darin stehen (sonst 403). Nur Laeufe im Status pending_approval sind entscheidbar, sonst 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","maxLength":2000},"reason":{"type":"string","minLength":1,"maxLength":500}},"required":["reason"]},"example":{"comment":"string","reason":"string"}}}}}},"/api/v1/workflows/recent-runs":{"get":{"responses":{"200":{"description":"Liste der letzten Runs. `meta.source` unterscheidet „keine Laeufe\" (db) von „konnte nicht nachsehen\" (unavailable) — beide liefern eine leere Liste.","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"type":"object","additionalProperties":{}}},"total":{"type":"number"},"meta":{"type":"object","properties":{"source":{"type":"string","enum":["db","unavailable"]},"tenantId":{"type":"string"},"note":{"type":"string"}},"required":["source"],"additionalProperties":false}},"required":["runs","total","meta"],"additionalProperties":false},"example":{"runs":[{}],"total":0,"meta":{"source":"db","tenantId":"string","note":"string"}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1WorkflowsRecent-runs","tags":["workflows"],"parameters":[],"summary":"List recent workflow runs","description":"Die letzten Workflow-Runs des Mandanten ueber alle Workflows hinweg, neueste zuerst. Query-Parameter limit, Vorgabe 20, hoechstens 100. Ohne Datenbankverbindung antwortet die Route 200 mit leerer Liste und meta.source=unavailable — das ist kein „keine Laeufe\"."}},"/api/v1/workflows":{"get":{"responses":{"200":{"description":"Liste Workflows","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"active":{"type":["boolean","null"]},"trigger":{},"conditions":{},"actions":{},"createdByAI":{"type":["boolean","null"]},"tags":{"type":["array","null"],"items":{"type":"string"}},"runCount":{"type":["number","null"]},"lastRunAt":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["id","name","description","active","createdByAI","tags","runCount","lastRunAt","createdAt","updatedAt"],"additionalProperties":false}},"total":{"type":"number"},"meta":{"type":"object","properties":{"source":{"type":"string"}},"required":["source"]}},"required":["data","total","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","description":"string","active":true,"createdByAI":true,"tags":["string"],"runCount":0,"lastRunAt":"string","createdAt":"string","updatedAt":"string"}],"total":0,"meta":{"source":"string"}}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Workflows","tags":["workflows"],"parameters":[],"summary":"List workflows","description":"Workflows auflisten mit Filter (active, createdByAi, type). Die Route kennt KEINE Paginierung: data enthaelt hoechstens die 25 neuesten Workflows, waehrend total die ungekuerzte Anzahl nennt — data.length und total koennen auseinanderfallen. type=approval filtert erst nach dem Lesen im Speicher; total ist dann die Anzahl der gefilterten Zeilen."},"post":{"responses":{"201":{"description":"Angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"active":{"type":["boolean","null"]},"trigger":{},"conditions":{},"actions":{},"createdByAI":{"type":["boolean","null"]},"tags":{"type":["array","null"],"items":{"type":"string"}},"runCount":{"type":["number","null"]},"lastRunAt":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["id","name","description","active","createdByAI","tags","runCount","lastRunAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","description":"string","active":true,"createdByAI":true,"tags":["string"],"runCount":0,"lastRunAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"503":{"description":"Anlegen fehlgeschlagen — es wurde NICHTS gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"detail":{"type":"string"}},"required":["error","message","detail"],"additionalProperties":false}}}}},"operationId":"postApiV1Workflows","tags":["workflows"],"parameters":[],"summary":"Create workflow","description":"Workflow anlegen. Erfordert eine Anmeldung (Rolle user genuegt). Ohne active-Feld ist der Workflow SOFORT aktiv (Vorgabe true) und laeuft bei passendem Trigger an.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"active":{"type":"boolean","default":true},"trigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"event"},"event":{"type":"string"},"filters":{"type":"object","additionalProperties":{}}},"required":["type","event"]},{"type":"object","properties":{"type":{"type":"string","const":"schedule"},"cron":{"type":"string"},"timezone":{"type":"string","default":"Europe/Berlin"}},"required":["type","cron"]},{"type":"object","properties":{"type":{"type":"string","const":"webhook_in"},"webhookId":{"type":"string"},"secret":{"type":"string"}},"required":["type","webhookId"]},{"type":"object","properties":{"type":{"type":"string","const":"manual"}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","const":"record_change"},"entity":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}},"operation":{"type":"string","enum":["create","update","delete"]}},"required":["type","entity","operation"]}]},"conditions":{"type":"array","items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"],"default":"AND"},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]}}},"required":["rules"]}},"actions":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"email"},"to":{"type":"string"},"subject":{"type":"string"},"template":{"type":"string"},"body":{"type":"string"}},"required":["type","to","subject"]},{"type":"object","properties":{"type":{"type":"string","const":"webhook_out"},"url":{"type":"string","format":"uri"},"method":{"type":"string","enum":["GET","POST","PUT"],"default":"POST"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"payload":{"type":"object","additionalProperties":{}}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","const":"set_field"},"entity":{"type":"string"},"field":{"type":"string"},"value":{}},"required":["type","entity","field"]},{"type":"object","properties":{"type":{"type":"string","const":"create_record"},"entity":{"type":"string"},"data":{"type":"object","additionalProperties":{}}},"required":["type","entity","data"]},{"type":"object","properties":{"type":{"type":"string","const":"notify"},"channel":{"type":"string","enum":["in_app","push","email","slack","teams"]},"message":{"type":"string"},"recipients":{"type":"array","items":{"type":"string"}},"title":{"type":"string","maxLength":200}},"required":["type","channel","message"]},{"type":"object","properties":{"type":{"type":"string","const":"ai_action"},"prompt":{"type":"string"},"outputField":{"type":"string"},"model":{"type":"string","default":"claude-haiku-4-5-20251001"}},"required":["type","prompt","outputField"]},{"type":"object","properties":{"type":{"type":"string","const":"wait"},"duration":{"type":"string"}},"required":["type","duration"]},{"type":"object","properties":{"type":{"type":"string","const":"approval"},"approvers":{"type":"array","items":{"type":"string"}},"timeout":{"type":"string"},"escalateTo":{"type":"string"}},"required":["type","approvers"]}]},"minItems":1},"createdByAI":{"type":"boolean","default":false},"tags":{"type":"array","items":{"type":"string"}}},"required":["name","trigger","actions"]},"example":{"name":"string","description":"string","active":true,"trigger":{"type":"event","event":"string","filters":{}},"conditions":[{"logic":"AND","rules":[{"field":"string","operator":"eq"}]}],"actions":[{"type":"email","to":"string","subject":"string","template":"string","body":"string"}],"createdByAI":true,"tags":["string"]}}}}}},"/api/v1/workflows/{id}":{"get":{"responses":{"200":{"description":"Workflow","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"active":{"type":["boolean","null"]},"trigger":{},"conditions":{},"actions":{},"createdByAI":{"type":["boolean","null"]},"tags":{"type":["array","null"],"items":{"type":"string"}},"runCount":{"type":["number","null"]},"lastRunAt":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["id","name","description","active","createdByAI","tags","runCount","lastRunAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","description":"string","active":true,"createdByAI":true,"tags":["string"],"runCount":0,"lastRunAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Workflow konnte nicht geladen werden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"detail":{"type":"string"}},"required":["error","message","detail"],"additionalProperties":false}}}}},"operationId":"getApiV1WorkflowsById","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get workflow","description":"Einzelnen Workflow abrufen."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"active":{"type":["boolean","null"]},"trigger":{},"conditions":{},"actions":{},"createdByAI":{"type":["boolean","null"]},"tags":{"type":["array","null"],"items":{"type":"string"}},"runCount":{"type":["number","null"]},"lastRunAt":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["id","name","description","active","createdByAI","tags","runCount","lastRunAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","description":"string","active":true,"createdByAI":true,"tags":["string"],"runCount":0,"lastRunAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"},"503":{"description":"Aktualisieren fehlgeschlagen — es wurde NICHTS geaendert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"detail":{"type":"string"}},"required":["error","message","detail"],"additionalProperties":false}}}}},"operationId":"putApiV1WorkflowsById","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace workflow","description":"Workflow vollstaendig ersetzen. Weggelassene Felder mit Vorgabe werden GESETZT, nicht bewahrt: ohne active wird der Workflow aktiviert, ohne createdByAI gilt er als nicht KI-erzeugt, ohne conditions sind die Bedingungen leer. Nur description und tags bleiben unveraendert, wenn sie fehlen. Fuer ein echtes Teil-Update PATCH nehmen. Eine unbekannte id endet in 503 WORKFLOW_UPDATE_FAILED, nicht in 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"active":{"type":"boolean","default":true},"trigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"event"},"event":{"type":"string"},"filters":{"type":"object","additionalProperties":{}}},"required":["type","event"]},{"type":"object","properties":{"type":{"type":"string","const":"schedule"},"cron":{"type":"string"},"timezone":{"type":"string","default":"Europe/Berlin"}},"required":["type","cron"]},{"type":"object","properties":{"type":{"type":"string","const":"webhook_in"},"webhookId":{"type":"string"},"secret":{"type":"string"}},"required":["type","webhookId"]},{"type":"object","properties":{"type":{"type":"string","const":"manual"}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","const":"record_change"},"entity":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}},"operation":{"type":"string","enum":["create","update","delete"]}},"required":["type","entity","operation"]}]},"conditions":{"type":"array","items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"],"default":"AND"},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]}}},"required":["rules"]}},"actions":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"email"},"to":{"type":"string"},"subject":{"type":"string"},"template":{"type":"string"},"body":{"type":"string"}},"required":["type","to","subject"]},{"type":"object","properties":{"type":{"type":"string","const":"webhook_out"},"url":{"type":"string","format":"uri"},"method":{"type":"string","enum":["GET","POST","PUT"],"default":"POST"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"payload":{"type":"object","additionalProperties":{}}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","const":"set_field"},"entity":{"type":"string"},"field":{"type":"string"},"value":{}},"required":["type","entity","field"]},{"type":"object","properties":{"type":{"type":"string","const":"create_record"},"entity":{"type":"string"},"data":{"type":"object","additionalProperties":{}}},"required":["type","entity","data"]},{"type":"object","properties":{"type":{"type":"string","const":"notify"},"channel":{"type":"string","enum":["in_app","push","email","slack","teams"]},"message":{"type":"string"},"recipients":{"type":"array","items":{"type":"string"}},"title":{"type":"string","maxLength":200}},"required":["type","channel","message"]},{"type":"object","properties":{"type":{"type":"string","const":"ai_action"},"prompt":{"type":"string"},"outputField":{"type":"string"},"model":{"type":"string","default":"claude-haiku-4-5-20251001"}},"required":["type","prompt","outputField"]},{"type":"object","properties":{"type":{"type":"string","const":"wait"},"duration":{"type":"string"}},"required":["type","duration"]},{"type":"object","properties":{"type":{"type":"string","const":"approval"},"approvers":{"type":"array","items":{"type":"string"}},"timeout":{"type":"string"},"escalateTo":{"type":"string"}},"required":["type","approvers"]}]},"minItems":1},"createdByAI":{"type":"boolean","default":false},"tags":{"type":"array","items":{"type":"string"}}},"required":["name","trigger","actions"]},"example":{"name":"string","description":"string","active":true,"trigger":{"type":"event","event":"string","filters":{}},"conditions":[{"logic":"AND","rules":[{"field":"string","operator":"eq"}]}],"actions":[{"type":"email","to":"string","subject":"string","template":"string","body":"string"}],"createdByAI":true,"tags":["string"]}}}}},"patch":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"active":{"type":["boolean","null"]},"trigger":{},"conditions":{},"actions":{},"createdByAI":{"type":["boolean","null"]},"tags":{"type":["array","null"],"items":{"type":"string"}},"runCount":{"type":["number","null"]},"lastRunAt":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["id","name","description","active","createdByAI","tags","runCount","lastRunAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","description":"string","active":true,"createdByAI":true,"tags":["string"],"runCount":0,"lastRunAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Aktualisieren fehlgeschlagen — es wurde NICHTS geaendert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"detail":{"type":"string"}},"required":["error","message","detail"],"additionalProperties":false}}}}},"operationId":"patchApiV1WorkflowsById","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update workflow partially","description":"Partielles Update: nicht gesendete Felder behalten ihren gespeicherten Wert. Anders als PUT prueft diese Route den Workflow vorher und antwortet bei unbekannter id 404. Gesendete Listen (actions, conditions, tags) ersetzen die bisherigen als Ganzes.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"active":{"type":"boolean"},"trigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"event"},"event":{"type":"string"},"filters":{"type":"object","additionalProperties":{}}},"required":["type","event"]},{"type":"object","properties":{"type":{"type":"string","const":"schedule"},"cron":{"type":"string"},"timezone":{"type":"string","default":"Europe/Berlin"}},"required":["type","cron"]},{"type":"object","properties":{"type":{"type":"string","const":"webhook_in"},"webhookId":{"type":"string"},"secret":{"type":"string"}},"required":["type","webhookId"]},{"type":"object","properties":{"type":{"type":"string","const":"manual"}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","const":"record_change"},"entity":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}},"operation":{"type":"string","enum":["create","update","delete"]}},"required":["type","entity","operation"]}]},"conditions":{"type":"array","items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"],"default":"AND"},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]}}},"required":["rules"]}},"actions":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"email"},"to":{"type":"string"},"subject":{"type":"string"},"template":{"type":"string"},"body":{"type":"string"}},"required":["type","to","subject"]},{"type":"object","properties":{"type":{"type":"string","const":"webhook_out"},"url":{"type":"string","format":"uri"},"method":{"type":"string","enum":["GET","POST","PUT"],"default":"POST"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"payload":{"type":"object","additionalProperties":{}}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","const":"set_field"},"entity":{"type":"string"},"field":{"type":"string"},"value":{}},"required":["type","entity","field"]},{"type":"object","properties":{"type":{"type":"string","const":"create_record"},"entity":{"type":"string"},"data":{"type":"object","additionalProperties":{}}},"required":["type","entity","data"]},{"type":"object","properties":{"type":{"type":"string","const":"notify"},"channel":{"type":"string","enum":["in_app","push","email","slack","teams"]},"message":{"type":"string"},"recipients":{"type":"array","items":{"type":"string"}},"title":{"type":"string","maxLength":200}},"required":["type","channel","message"]},{"type":"object","properties":{"type":{"type":"string","const":"ai_action"},"prompt":{"type":"string"},"outputField":{"type":"string"},"model":{"type":"string","default":"claude-haiku-4-5-20251001"}},"required":["type","prompt","outputField"]},{"type":"object","properties":{"type":{"type":"string","const":"wait"},"duration":{"type":"string"}},"required":["type","duration"]},{"type":"object","properties":{"type":{"type":"string","const":"approval"},"approvers":{"type":"array","items":{"type":"string"}},"timeout":{"type":"string"},"escalateTo":{"type":"string"}},"required":["type","approvers"]}]},"minItems":1},"createdByAI":{"type":"boolean"},"tags":{"type":"array","items":{"type":"string"}}}},"example":{"name":"string","description":"string","active":true,"trigger":{"type":"event","event":"string","filters":{}},"conditions":[{"logic":"AND","rules":[{"field":"string","operator":"eq"}]}],"actions":[{"type":"email","to":"string","subject":"string","template":"string","body":"string"}],"createdByAI":true,"tags":["string"]}}}}},"delete":{"responses":{"200":{"description":"Geloescht — nur eine Quittung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"503":{"description":"Loeschen fehlgeschlagen — der Workflow existiert NOCH","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"detail":{"type":"string"}},"required":["error","message","detail"],"additionalProperties":false}}}}},"operationId":"deleteApiV1WorkflowsById","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete workflow","description":"Workflow endgueltig loeschen (echtes DELETE, kein Soft-Delete). Erfordert Rolle admin oder hoeher. Eine unbekannte id quittiert ebenfalls 200 — die Route prueft nicht, ob eine Zeile entfernt wurde. Bereits gelaufene Eintraege in workflow_runs bleiben bestehen."}},"/api/v1/workflows/{id}/run":{"post":{"responses":{"202":{"description":"Angenommen — der Lauf ist gestartet, nicht zwingend beendet. Der Rumpf ist der Lauf-Datensatz; `stepsCompleted` steht beim Quittieren noch auf 0 und `output` fehlt.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Laufs"},"workflowId":{"type":"string","description":"Workflow, zu dem der Lauf gehoert"},"status":{"type":"string","enum":["pending","running","success","failed","cancelled","waiting","pending_approval","rejected"],"description":"`waiting` = auf einem langen `wait`-Schritt angehalten, `pending_approval` = auf einer Freigabe, `rejected` = Freigabe abgelehnt (endgueltig)"},"triggerSource":{"type":"string","enum":["manual","webhook","schedule","api","event"],"description":"Wodurch der Lauf ausgeloest wurde"},"triggerData":{"type":"object","additionalProperties":{},"description":"Der Rumpf des Ausloesers, unveraendert; fehlt, wenn keiner mitkam"},"startedAt":{"type":"string","description":"Startzeitpunkt — Form siehe oben, kein garantiertes ISO 8601"},"finishedAt":{"type":"string","description":"Endzeitpunkt; fehlt, solange der Lauf laeuft"},"durationMs":{"type":"number","description":"Dauer in Millisekunden; fehlt, solange der Lauf laeuft"},"stepsTotal":{"type":"integer","description":"Anzahl der Schritte des Workflows"},"stepsCompleted":{"type":"integer","description":"Bereits abgearbeitete Schritte"},"error":{"type":"string","description":"Technische Fehlermeldung; fehlt bei einem Lauf ohne Fehler"},"output":{"type":"object","additionalProperties":{},"description":"Ergebnis des Laufs; fehlt, solange keines vorliegt"},"stepResults":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Nullbasierte Nummer des Schritts"},"actionType":{"type":"string","description":"Art der Aktion, wie im Workflow hinterlegt"},"ok":{"type":"boolean"},"status":{"type":"string","enum":["success","failed","skipped"],"description":"`skipped` = Bedingungs-Gate nicht erfuellt, bewusst uebersprungen"},"durationMs":{"type":"number"},"error":{"type":"string"},"output":{"type":"object","additionalProperties":{}}},"required":["index","actionType","ok","status"],"additionalProperties":true},"description":"Ergebnis je Schritt fuer die Lauf-Ansicht; fehlt, solange kein Schritt lief"},"createdAt":{"type":"string","description":"Anlagezeitpunkt — Form wie `startedAt`"}},"required":["id","workflowId","status","triggerSource","startedAt","stepsTotal","stepsCompleted","createdAt"],"additionalProperties":false},"example":{"id":"string","workflowId":"string","status":"pending","triggerSource":"manual","triggerData":{},"startedAt":"string","finishedAt":"string","durationMs":0,"stepsTotal":0,"stepsCompleted":0,"error":"string","output":{},"stepResults":[{"index":0,"actionType":"string","ok":true,"status":"success","durationMs":0,"error":"string","output":{}}],"createdAt":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"},"500":{"description":"Start fehlgeschlagen — error traegt die technische Meldung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1WorkflowsByIdRun","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Start workflow run","description":"Manuell ausführen — wirkungsgleich mit POST /{id}/trigger. Die Antwort ist eine QUITTUNG, kein Ergebnis: 202 mit dem Lauf-Datensatz, die Schritte laufen danach im Hintergrund weiter. Das Ergebnis holt GET /{id}/runs/{runId}. Ein unbekannter oder abgeschalteter Workflow endet in 500 mit der technischen Meldung, nicht in 404. Ist die Datenbank nicht erreichbar, antwortet die Route trotzdem 202 — mit einem erfundenen Lauf (id run_…, Status pending), der nirgends gespeichert ist und nie ausgeführt wird."}},"/api/v1/workflows/{id}/toggle":{"post":{"responses":{"200":{"description":"Umgeschaltet — active ist der NEUE Zustand","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"active":{"type":"boolean"},"message":{"type":"string"}},"required":["id","active","message"],"additionalProperties":false},"example":{"id":"string","active":true,"message":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"},"503":{"description":"Umschalten fehlgeschlagen — der Zustand blieb unveraendert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"detail":{"type":"string"}},"required":["error","message","detail"],"additionalProperties":false}}}}},"operationId":"postApiV1WorkflowsByIdToggle","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Toggle workflow active state","description":"Workflow an- bzw. abschalten. Kein Körper nötig — die Route kippt den gespeicherten Zustand; active in der Antwort ist der NEUE Zustand. Eine unbekannte id endet in 503 WORKFLOW_TOGGLE_FAILED, nicht in 404."}},"/api/v1/workflows/{id}/trigger":{"post":{"responses":{"202":{"description":"Angenommen — der Lauf ist gestartet, nicht zwingend beendet. Der Rumpf ist der Lauf-Datensatz; `stepsCompleted` steht beim Quittieren noch auf 0 und `output` fehlt.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Laufs"},"workflowId":{"type":"string","description":"Workflow, zu dem der Lauf gehoert"},"status":{"type":"string","enum":["pending","running","success","failed","cancelled","waiting","pending_approval","rejected"],"description":"`waiting` = auf einem langen `wait`-Schritt angehalten, `pending_approval` = auf einer Freigabe, `rejected` = Freigabe abgelehnt (endgueltig)"},"triggerSource":{"type":"string","enum":["manual","webhook","schedule","api","event"],"description":"Wodurch der Lauf ausgeloest wurde"},"triggerData":{"type":"object","additionalProperties":{},"description":"Der Rumpf des Ausloesers, unveraendert; fehlt, wenn keiner mitkam"},"startedAt":{"type":"string","description":"Startzeitpunkt — Form siehe oben, kein garantiertes ISO 8601"},"finishedAt":{"type":"string","description":"Endzeitpunkt; fehlt, solange der Lauf laeuft"},"durationMs":{"type":"number","description":"Dauer in Millisekunden; fehlt, solange der Lauf laeuft"},"stepsTotal":{"type":"integer","description":"Anzahl der Schritte des Workflows"},"stepsCompleted":{"type":"integer","description":"Bereits abgearbeitete Schritte"},"error":{"type":"string","description":"Technische Fehlermeldung; fehlt bei einem Lauf ohne Fehler"},"output":{"type":"object","additionalProperties":{},"description":"Ergebnis des Laufs; fehlt, solange keines vorliegt"},"stepResults":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Nullbasierte Nummer des Schritts"},"actionType":{"type":"string","description":"Art der Aktion, wie im Workflow hinterlegt"},"ok":{"type":"boolean"},"status":{"type":"string","enum":["success","failed","skipped"],"description":"`skipped` = Bedingungs-Gate nicht erfuellt, bewusst uebersprungen"},"durationMs":{"type":"number"},"error":{"type":"string"},"output":{"type":"object","additionalProperties":{}}},"required":["index","actionType","ok","status"],"additionalProperties":true},"description":"Ergebnis je Schritt fuer die Lauf-Ansicht; fehlt, solange kein Schritt lief"},"createdAt":{"type":"string","description":"Anlagezeitpunkt — Form wie `startedAt`"}},"required":["id","workflowId","status","triggerSource","startedAt","stepsTotal","stepsCompleted","createdAt"],"additionalProperties":false},"example":{"id":"string","workflowId":"string","status":"pending","triggerSource":"manual","triggerData":{},"startedAt":"string","finishedAt":"string","durationMs":0,"stepsTotal":0,"stepsCompleted":0,"error":"string","output":{},"stepResults":[{"index":0,"actionType":"string","ok":true,"status":"success","durationMs":0,"error":"string","output":{}}],"createdAt":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"},"500":{"description":"Start fehlgeschlagen — error traegt die technische Meldung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1WorkflowsByIdTrigger","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Trigger workflow run","description":"Workflow auslösen; der Rumpf wird als triggerData übergeben. Wirkungsgleich mit POST /{id}/run — die Redis-Warteschlange ändert am Verhalten nichts, beide Zweige rufen denselben Runner. Die Antwort ist eine QUITTUNG, kein Ergebnis: 202 mit dem Lauf-Datensatz, die Schritte laufen danach im Hintergrund. Ein unbekannter oder abgeschalteter Workflow endet in 500, nicht in 404; ohne Datenbank kommt trotzdem 202 mit einem erfundenen, nicht gespeicherten Lauf."}},"/api/v1/workflows/{id}/runs":{"get":{"responses":{"200":{"description":"Laeufe — anders als GET /runs OHNE meta-Feld","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Laufs"},"workflowId":{"type":"string","description":"Workflow, zu dem der Lauf gehoert"},"status":{"type":"string","enum":["pending","running","success","failed","cancelled","waiting","pending_approval","rejected"],"description":"`waiting` = auf einem langen `wait`-Schritt angehalten, `pending_approval` = auf einer Freigabe, `rejected` = Freigabe abgelehnt (endgueltig)"},"triggerSource":{"type":"string","enum":["manual","webhook","schedule","api","event"],"description":"Wodurch der Lauf ausgeloest wurde"},"triggerData":{"type":"object","additionalProperties":{},"description":"Der Rumpf des Ausloesers, unveraendert; fehlt, wenn keiner mitkam"},"startedAt":{"type":"string","description":"Startzeitpunkt — Form siehe oben, kein garantiertes ISO 8601"},"finishedAt":{"type":"string","description":"Endzeitpunkt; fehlt, solange der Lauf laeuft"},"durationMs":{"type":"number","description":"Dauer in Millisekunden; fehlt, solange der Lauf laeuft"},"stepsTotal":{"type":"integer","description":"Anzahl der Schritte des Workflows"},"stepsCompleted":{"type":"integer","description":"Bereits abgearbeitete Schritte"},"error":{"type":"string","description":"Technische Fehlermeldung; fehlt bei einem Lauf ohne Fehler"},"output":{"type":"object","additionalProperties":{},"description":"Ergebnis des Laufs; fehlt, solange keines vorliegt"},"stepResults":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Nullbasierte Nummer des Schritts"},"actionType":{"type":"string","description":"Art der Aktion, wie im Workflow hinterlegt"},"ok":{"type":"boolean"},"status":{"type":"string","enum":["success","failed","skipped"],"description":"`skipped` = Bedingungs-Gate nicht erfuellt, bewusst uebersprungen"},"durationMs":{"type":"number"},"error":{"type":"string"},"output":{"type":"object","additionalProperties":{}}},"required":["index","actionType","ok","status"],"additionalProperties":true},"description":"Ergebnis je Schritt fuer die Lauf-Ansicht; fehlt, solange kein Schritt lief"},"createdAt":{"type":"string","description":"Anlagezeitpunkt — Form wie `startedAt`"}},"required":["id","workflowId","status","triggerSource","startedAt","stepsTotal","stepsCompleted","createdAt"],"additionalProperties":false}},"total":{"type":"number"}},"required":["runs","total"],"additionalProperties":false},"example":{"runs":[{"id":"string","workflowId":"string","status":"pending","triggerSource":"manual","triggerData":{},"startedAt":"string","finishedAt":"string","durationMs":0,"stepsTotal":0,"stepsCompleted":0,"error":"string","output":{},"stepResults":[{"index":0,"actionType":"string","ok":true,"status":"success","durationMs":0,"error":"string","output":{}}],"createdAt":"string"}],"total":0}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar — NICHT als „keine Laeufe\" lesen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1WorkflowsByIdRuns","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List runs of a workflow","description":"Laufhistorie eines Workflows, neueste zuerst. Query-Parameter limit, Vorgabe 20, hoechstens 100. Hat der Mandant noch nie einen Lauf gehabt, fehlt die Tabelle und die Route antwortet 200 mit leerer Liste; ein echter Datenbankfehler gibt 503."}},"/api/v1/workflows/{id}/runs/{runId}":{"get":{"responses":{"200":{"description":"Der Lauf in seinem aktuellen Stand. `finishedAt`, `durationMs`, `output` und `error` FEHLEN, solange es sie nicht gibt — sie kommen nicht als `null`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Laufs"},"workflowId":{"type":"string","description":"Workflow, zu dem der Lauf gehoert"},"status":{"type":"string","enum":["pending","running","success","failed","cancelled","waiting","pending_approval","rejected"],"description":"`waiting` = auf einem langen `wait`-Schritt angehalten, `pending_approval` = auf einer Freigabe, `rejected` = Freigabe abgelehnt (endgueltig)"},"triggerSource":{"type":"string","enum":["manual","webhook","schedule","api","event"],"description":"Wodurch der Lauf ausgeloest wurde"},"triggerData":{"type":"object","additionalProperties":{},"description":"Der Rumpf des Ausloesers, unveraendert; fehlt, wenn keiner mitkam"},"startedAt":{"type":"string","description":"Startzeitpunkt — Form siehe oben, kein garantiertes ISO 8601"},"finishedAt":{"type":"string","description":"Endzeitpunkt; fehlt, solange der Lauf laeuft"},"durationMs":{"type":"number","description":"Dauer in Millisekunden; fehlt, solange der Lauf laeuft"},"stepsTotal":{"type":"integer","description":"Anzahl der Schritte des Workflows"},"stepsCompleted":{"type":"integer","description":"Bereits abgearbeitete Schritte"},"error":{"type":"string","description":"Technische Fehlermeldung; fehlt bei einem Lauf ohne Fehler"},"output":{"type":"object","additionalProperties":{},"description":"Ergebnis des Laufs; fehlt, solange keines vorliegt"},"stepResults":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Nullbasierte Nummer des Schritts"},"actionType":{"type":"string","description":"Art der Aktion, wie im Workflow hinterlegt"},"ok":{"type":"boolean"},"status":{"type":"string","enum":["success","failed","skipped"],"description":"`skipped` = Bedingungs-Gate nicht erfuellt, bewusst uebersprungen"},"durationMs":{"type":"number"},"error":{"type":"string"},"output":{"type":"object","additionalProperties":{}}},"required":["index","actionType","ok","status"],"additionalProperties":true},"description":"Ergebnis je Schritt fuer die Lauf-Ansicht; fehlt, solange kein Schritt lief"},"createdAt":{"type":"string","description":"Anlagezeitpunkt — Form wie `startedAt`"}},"required":["id","workflowId","status","triggerSource","startedAt","stepsTotal","stepsCompleted","createdAt"],"additionalProperties":false},"example":{"id":"string","workflowId":"string","status":"pending","triggerSource":"manual","triggerData":{},"startedAt":"string","finishedAt":"string","durationMs":0,"stepsTotal":0,"stepsCompleted":0,"error":"string","output":{},"stepResults":[{"index":0,"actionType":"string","ok":true,"status":"success","durationMs":0,"error":"string","output":{}}],"createdAt":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Unerwarteter Fehler — error traegt die technische Meldung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1WorkflowsByIdRunsByRunId","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"runId","required":true}],"summary":"Get workflow run","description":"Einzelnen Lauf abrufen — hier steht das Ergebnis, das POST /{id}/trigger nur quittiert hat (status, Schrittzaehler, error). Nachgeschlagen wird allein ueber runId; der Pfadteil {id} wird nicht geprueft."}},"/api/v1/workflows/{id}/dry-run":{"post":{"responses":{"200":{"description":"Simulationsergebnis. Es wurde nichts ausgefuehrt und nichts geschrieben — auch kein Lauf-Eintrag.","content":{"application/json":{"schema":{"type":"object","properties":{"workflowId":{"type":"string"},"workflowName":{"type":"string"},"active":{"type":"boolean","description":"Ob der Workflow scharf ist — die Simulation laeuft auch fuer inaktive"},"dryRun":{"type":"boolean","const":true},"wouldRun":{"type":"boolean"},"conditionsTrace":{"type":"array","items":{"type":"object","properties":{"condition":{"type":"string","description":"Die Regel in Worten, mit deutschen Operatorzeichen"},"matched":{"type":"boolean","description":"Ob diese eine Regel auf das Beispiel passt"}},"required":["condition","matched"]}},"actions":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"preview":{"anyOf":[{"type":"object","additionalProperties":{}},{"type":"string"}],"description":"Vorschau je Aktionsart, oder der Text \"nicht simulierbar\""}},"required":["type","preview"]},"description":"Nur Vorschau — nichts davon wurde ausgefuehrt"},"summary":{"type":"string","description":"Ein deutscher Satz, der wouldRun erklaert"}},"required":["workflowId","workflowName","active","dryRun","wouldRun","conditionsTrace","actions","summary"]},"example":{"workflowId":"string","workflowName":"string","active":true,"dryRun":true,"wouldRun":true,"conditionsTrace":[{"condition":"string","matched":true}],"actions":[{"type":"string","preview":{}}],"summary":"string"}}}},"400":{"description":"Ungültiger samplePayload"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden (manager+ erforderlich)"},"404":{"description":"Workflow nicht gefunden"},"503":{"description":"DB nicht verfügbar"}},"operationId":"postApiV1WorkflowsByIdDry-run","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Simuliert einen Workflow gegen ein Beispiel, ohne ihn auszufuehren","description":"Workflow gegen ein Beispiel-Payload simulieren (Dry-Run) — keine Ausführung, keine Schreibzugriffe. Liefert Bedingungs-Trace und Aktions-Vorschau."}},"/api/v1/entity-rules":{"get":{"responses":{"200":{"description":"Liste der Regeln","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"entity":{"type":"string"},"ruleType":{"type":"string","enum":["required_if","validate","computed","visible_if"]},"targetField":{"type":"string","description":"Feldname nach der Auflösung, so wie er gespeichert wurde"},"condition":{"type":["array","null"],"items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"]},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":200},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]},"minItems":1}},"required":["rules"]},"minItems":1,"maxItems":20},"expression":{"type":["object","null"],"properties":{"op":{"type":"string","enum":["multiply","add","subtract","divide","percent_of"]},"fields":{"type":"array","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":10},"factor":{"type":"number"}},"required":["op","fields"]},"messageDe":{"type":["string","null"]},"active":{"type":"boolean"},"version":{"type":"integer","description":"Beginnt bei 1 und steigt mit jedem PATCH um 1"},"owner":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","entity","ruleType","targetField","condition","expression","messageDe","active","version","owner","createdAt","updatedAt"]}},"total":{"type":"integer","description":"Anzahl der zurückgegebenen Regeln, es wird nicht geblättert"}},"required":["data","total"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","entity":"string","ruleType":"required_if","targetField":"string","condition":null,"expression":{"op":"multiply","fields":["string"],"factor":0},"messageDe":"string","active":true,"version":0,"owner":"string","createdAt":"string","updatedAt":"string"}],"total":0}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Entity-rules","tags":["entity-rules"],"parameters":[{"in":"query","name":"entity","schema":{"type":"string","minLength":1,"maxLength":64}}],"summary":"Entitätsregeln des Mandanten auflisten","description":"Liest `entity_rules` aus dem Schema des Mandanten, nur Regeln ohne `deleted_at`, neueste zuerst. Der optionale Filter `entity` vergleicht exakt. Die Antwort ist nicht geblättert: `total` nennt die Zahl der mitgelieferten Regeln, nicht einen Gesamtbestand."},"post":{"responses":{"201":{"description":"Regel angelegt, mit Bericht über die Feld-Auflösung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"entity":{"type":"string"},"ruleType":{"type":"string","enum":["required_if","validate","computed","visible_if"]},"targetField":{"type":"string","description":"Feldname nach der Auflösung, so wie er gespeichert wurde"},"condition":{"type":["array","null"],"items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"]},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":200},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]},"minItems":1}},"required":["rules"]},"minItems":1,"maxItems":20},"expression":{"type":["object","null"],"properties":{"op":{"type":"string","enum":["multiply","add","subtract","divide","percent_of"]},"fields":{"type":"array","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":10},"factor":{"type":"number"}},"required":["op","fields"]},"messageDe":{"type":["string","null"]},"active":{"type":"boolean"},"version":{"type":"integer","description":"Beginnt bei 1 und steigt mit jedem PATCH um 1"},"owner":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"fieldResolution":{"type":"object","properties":{"rewrites":{"type":"object","additionalProperties":{"type":"string"},"description":"Übergebenes Token zum tatsächlich gespeicherten Feldnamen"},"unresolved":{"type":"array","items":{"type":"string"},"description":"Tokens, die keinem Feld zugeordnet werden konnten und roh gespeichert wurden"}},"required":["rewrites","unresolved"]}},"required":["id","entity","ruleType","targetField","condition","expression","messageDe","active","version","owner","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","entity":"string","ruleType":"required_if","targetField":"string","condition":[{"logic":"AND","rules":[{"field":"string","operator":"eq"}]}],"expression":{"op":"multiply","fields":["string"],"factor":0},"messageDe":"string","active":true,"version":0,"owner":"string","createdAt":"string","updatedAt":"string","fieldResolution":{"rewrites":{"beispiel":"string"},"unresolved":["string"]}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"}},"operationId":"postApiV1Entity-rules","tags":["entity-rules"],"parameters":[],"summary":"Entitätsregel anlegen und sofort scharf schalten","description":"Legt eine Regel in `entity_rules` an (`version` startet bei 1) und leert danach den Enforcement-Cache dieser Entität, sodass die Regel sofort greift. Vor dem Speichern werden die Feld-Tokens aus `targetField`, `condition.rules[].field` und `expression.fields` gegen die Custom-Field-Registry und die Kernspalten der Entität aufgelöst; was umgeschrieben oder nicht zugeordnet werden konnte, steht in `fieldResolution`, das sonst ganz fehlt. Regeln vom Typ `computed` brauchen eine `expression`, `validate` eine `condition`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","minLength":1,"maxLength":64},"ruleType":{"type":"string","enum":["required_if","validate","computed","visible_if"]},"targetField":{"type":"string","minLength":1,"maxLength":120},"condition":{"type":"array","items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"]},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":200},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]},"minItems":1}},"required":["rules"]},"minItems":1,"maxItems":20},"expression":{"type":"object","properties":{"op":{"type":"string","enum":["multiply","add","subtract","divide","percent_of"]},"fields":{"type":"array","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":10},"factor":{"type":"number"}},"required":["op","fields"]},"messageDe":{"type":"string","maxLength":300},"active":{"type":"boolean","default":true},"owner":{"type":"string","maxLength":120}},"required":["entity","ruleType","targetField"]},"example":{"entity":"string","ruleType":"required_if","targetField":"string","condition":[{"logic":"AND","rules":[{"field":"string","operator":"eq"}]}],"expression":{"op":"multiply","fields":["string"],"factor":0},"messageDe":"string","active":true,"owner":"string"}}}}}},"/api/v1/entity-rules/{id}":{"patch":{"responses":{"200":{"description":"Regel aktualisiert, mit Bericht über die Feld-Auflösung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"entity":{"type":"string"},"ruleType":{"type":"string","enum":["required_if","validate","computed","visible_if"]},"targetField":{"type":"string","description":"Feldname nach der Auflösung, so wie er gespeichert wurde"},"condition":{"type":["array","null"],"items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"]},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":200},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]},"minItems":1}},"required":["rules"]},"minItems":1,"maxItems":20},"expression":{"type":["object","null"],"properties":{"op":{"type":"string","enum":["multiply","add","subtract","divide","percent_of"]},"fields":{"type":"array","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":10},"factor":{"type":"number"}},"required":["op","fields"]},"messageDe":{"type":["string","null"]},"active":{"type":"boolean"},"version":{"type":"integer","description":"Beginnt bei 1 und steigt mit jedem PATCH um 1"},"owner":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"fieldResolution":{"type":"object","properties":{"rewrites":{"type":"object","additionalProperties":{"type":"string"},"description":"Übergebenes Token zum tatsächlich gespeicherten Feldnamen"},"unresolved":{"type":"array","items":{"type":"string"},"description":"Tokens, die keinem Feld zugeordnet werden konnten und roh gespeichert wurden"}},"required":["rewrites","unresolved"]}},"required":["id","entity","ruleType","targetField","condition","expression","messageDe","active","version","owner","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","entity":"string","ruleType":"required_if","targetField":"string","condition":[{"logic":"AND","rules":[{"field":"string","operator":"eq"}]}],"expression":{"op":"multiply","fields":["string"],"factor":0},"messageDe":"string","active":true,"version":0,"owner":"string","createdAt":"string","updatedAt":"string","fieldResolution":{"rewrites":{"beispiel":"string"},"unresolved":["string"]}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Regel nicht gefunden"}},"operationId":"patchApiV1Entity-rulesById","tags":["entity-rules"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Führt den Rumpf mit der bestehenden Regel zusammen und erhöht `version` um 1: weggelassene Felder bleiben stehen, ausdrücklich auf null gesetzte werden geleert. Nach dem Zusammenführen gilt weiter, dass `computed` eine `expression` und `validate` eine `condition` braucht, sonst 400. Die Feld-Tokens werden erneut aufgelöst, was auch Altregeln mit rohen Tokens heilt, und der Enforcement-Cache wird für die alte wie für die neue Entität geleert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","minLength":1,"maxLength":64},"ruleType":{"type":"string","enum":["required_if","validate","computed","visible_if"]},"targetField":{"type":"string","minLength":1,"maxLength":120},"condition":{"type":["array","null"],"items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"]},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":200},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]},"minItems":1}},"required":["rules"]},"minItems":1,"maxItems":20},"expression":{"type":["object","null"],"properties":{"op":{"type":"string","enum":["multiply","add","subtract","divide","percent_of"]},"fields":{"type":"array","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":10},"factor":{"type":"number"}},"required":["op","fields"]},"messageDe":{"type":["string","null"],"maxLength":300},"active":{"type":"boolean"},"owner":{"type":["string","null"],"maxLength":120}}},"example":{"entity":"string","ruleType":"required_if","targetField":"string","condition":[{"logic":"AND","rules":[{"field":"string","operator":"eq"}]}],"expression":{"op":"multiply","fields":["string"],"factor":0},"messageDe":"string","active":true,"owner":"string"}}}},"summary":"Führt den Rumpf mit der bestehenden Regel zusammen und erhöht `version` um 1","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Regel gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Deutscher Bestätigungstext mit der id der Regel"}},"required":["message"]},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Regel nicht gefunden"}},"operationId":"deleteApiV1Entity-rulesById","tags":["entity-rules"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Entitätsregel löschen und aus dem Enforcement nehmen","description":"Setzt `deleted_at` und `updated_at`; die Zeile bleibt in `entity_rules` stehen und verschwindet aus der Liste und aus dem Enforcement. Danach wird der Enforcement-Cache der betroffenen Entität geleert, damit die Regel sofort nicht mehr greift. Eine unbekannte oder bereits gelöschte Regel ergibt 404; sonst kommt ein deutscher Bestätigungstext im Feld `message` zurück."}},"/api/v1/ai/recipe-suggestions":{"get":{"responses":{"200":{"description":"Liste der Vorschläge — leer heiszt auch „nicht lesbar\"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"recipeKey":{"type":"string"},"nameDe":{"type":"string"},"reasonDe":{"type":"string"}},"required":["recipeKey","nameDe","reasonDe"]}}},"required":["data"]},"example":{"data":[{"recipeKey":"string","nameDe":"string","reasonDe":"string"}]}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AiRecipe-suggestions","tags":["ai"],"parameters":[],"summary":"Schlaegt Regel-Rezepte fuer Felder ohne passende Regel vor","description":"Proaktive, lücken-bewusste Rezept-Vorschläge: erkennt vorhandene Felder ohne passende Regel. Der Endpunkt liest drei Dinge zum aktuellen Ausbaustand — die vorhandenen Feld-Kennungen, die Ziele der aktiven Regeln und die installierten Branchenpakete — und meldet die Rezepte, deren Feld schon da, deren Regel aber noch nicht angelegt ist; was ein Branchenpaket bereits abdeckt, bleibt auszen vor. REIN LESEND und ohne Modellaufruf: es wird nichts angelegt, nichts geaendert und keine Tabelle erzeugt. Alle drei Lesevorgaenge sind fehlertolerant, und ohne ein einziges eigenes Feld kommt bewusst nichts — eine leere Liste heiszt darum „nichts zu empfehlen ODER nichts lesbar\", nicht „alles vollstaendig\". Ein Vorschlag ist ein Hinweis; angelegt wird das Rezept erst ueber den Bau-Ablauf."}},"/api/v1/decision-tables":{"get":{"responses":{"200":{"description":"Tabellen der Seite plus Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Entscheidungstabelle"},"name":{"type":"string","description":"Name der Tabelle; NICHT auf Eindeutigkeit geprueft"},"entity":{"type":["string","null"],"description":"Entitaet, an der die Tabelle haengt; null wenn keine"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst ist"},"inputs":{"type":"array","items":{},"description":"Die Eingangsgroessen (je `key` und `type`); leere Liste wenn keine definiert sind"},"rows":{"type":"array","items":{},"description":"Die Regelzeilen (je `when`, `then` und wahlweise `label`); leere Liste wenn keine"},"defaultResult":{"type":["object","null"],"additionalProperties":{},"description":"Ergebnis, wenn keine Zeile trifft; null wenn keins hinterlegt ist"},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"description":"FIRST nimmt den ersten Treffer, UNIQUE meldet mehrere Treffer als Konflikt. Nie null — Altbestand liest sich als FIRST"},"version":{"type":"integer","description":"Versionszaehler; jede Aenderung erhoeht ihn um eins"},"active":{"type":"boolean","description":"Ob die Tabelle bei Schreibvorgaengen wirklich greift"},"owner":{"type":["string","null"],"description":"Zustaendige Person; null wenn keine erfasst ist"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","name","entity","description","inputs","rows","defaultResult","hitPolicy","version","active","owner","createdAt","updatedAt"]},"description":"Die Tabellen der Seite, neueste zuerst"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Treffer der Filter"}},"required":["limit","offset","total"],"description":"Seitenangaben"}},"required":["data","pagination"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","name":"string","entity":"string","description":"string","inputs":[],"rows":[],"defaultResult":{},"hitPolicy":"FIRST","version":0,"active":true,"owner":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":1,"offset":0,"total":0}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Decision-tables","tags":["decision-tables"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"entity","schema":{"type":"string"}},{"in":"query","name":"active","schema":{"type":"string","enum":["true","false"]}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"Entscheidungstabellen des Mandanten blaettern, filtern und durchsuchen","description":"Blaettert durch die nicht stillgelegten Entscheidungstabellen des Mandanten, neueste zuerst. `entity` und `active` (Zeichenkette \"true\"/\"false\") filtern exakt, `search` sucht als Teiltext in Name ODER Beschreibung; `limit` (1-200, Vorgabe 50) und `offset` blaettern, `total` zaehlt alle Treffer der Filter. Stillgelegte Tabellen (`deleted_at`) erscheinen NIE — auch nicht mit einem Filter. Jede Zeile kommt vollstaendig samt `inputs` und `rows`; die Versionshistorie ist nicht dabei. Fehlende Tabellen legt der Aufruf leer an."},"post":{"responses":{"201":{"description":"Die angelegte Tabelle mit Version 1, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Entscheidungstabelle"},"name":{"type":"string","description":"Name der Tabelle; NICHT auf Eindeutigkeit geprueft"},"entity":{"type":["string","null"],"description":"Entitaet, an der die Tabelle haengt; null wenn keine"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst ist"},"inputs":{"type":"array","items":{},"description":"Die Eingangsgroessen (je `key` und `type`); leere Liste wenn keine definiert sind"},"rows":{"type":"array","items":{},"description":"Die Regelzeilen (je `when`, `then` und wahlweise `label`); leere Liste wenn keine"},"defaultResult":{"type":["object","null"],"additionalProperties":{},"description":"Ergebnis, wenn keine Zeile trifft; null wenn keins hinterlegt ist"},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"description":"FIRST nimmt den ersten Treffer, UNIQUE meldet mehrere Treffer als Konflikt. Nie null — Altbestand liest sich als FIRST"},"version":{"type":"integer","description":"Versionszaehler; jede Aenderung erhoeht ihn um eins"},"active":{"type":"boolean","description":"Ob die Tabelle bei Schreibvorgaengen wirklich greift"},"owner":{"type":["string","null"],"description":"Zustaendige Person; null wenn keine erfasst ist"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","name","entity","description","inputs","rows","defaultResult","hitPolicy","version","active","owner","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","entity":"string","description":"string","inputs":[],"rows":[],"defaultResult":{},"hitPolicy":"FIRST","version":0,"active":true,"owner":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Anlegen fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Decision-tables","tags":["decision-tables"],"parameters":[],"description":"Legt eine Entscheidungstabelle mit Version 1 an. Sie ist INAKTIV, solange nicht ausdruecklich `active: \"true\"` mitgegeben wird — erst simulieren, dann schalten. `hitPolicy` ist ohne Angabe `FIRST`. Der Name wird NICHT auf Eindeutigkeit geprueft; zwei Tabellen duerfen gleich heissen. Es entsteht KEIN Versions-Schnappschuss — die Historie beginnt erst mit der ersten Aenderung. Wird sie aktiv angelegt und haengt an einer Entitaet, wird der Auswertungs-Zwischenspeicher fuer diese Entitaet verworfen. Die Antwort ist die Tabelle SELBST, ohne umschliessendes Feld. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":160},"entity":{"type":"string","minLength":1,"maxLength":64},"description":{"type":"string","maxLength":4000},"inputs":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":80},"type":{"type":"string","enum":["number","text","boolean"]}},"required":["key","type"]},"default":[]},"rows":{"type":"array","items":{"type":"object","properties":{"when":{"anyOf":[{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"]},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":200},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]}}},"required":["rules"]},{"type":"array","items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"]},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":200},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]}}},"required":["rules"]}}]},"then":{"type":"object","additionalProperties":{}},"label":{"type":"string","maxLength":160}},"required":["then"]},"default":[]},"defaultResult":{"type":["object","null"],"additionalProperties":{}},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"default":"FIRST"},"owner":{"type":"string","maxLength":120},"active":{"type":"string","enum":["true","false"]}},"required":["name"]},"example":{"name":"string","entity":"string","description":"string","inputs":[{"key":"string","type":"number"}],"rows":[{"when":{"logic":"AND","rules":[{"field":"string","operator":"eq"}]},"then":{},"label":"string"}],"defaultResult":{},"hitPolicy":"FIRST","owner":"string","active":"true"}}}},"summary":"Legt eine Entscheidungstabelle mit Version 1 an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/decision-tables/{id}":{"get":{"responses":{"200":{"description":"Die Entscheidungstabelle, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Entscheidungstabelle"},"name":{"type":"string","description":"Name der Tabelle; NICHT auf Eindeutigkeit geprueft"},"entity":{"type":["string","null"],"description":"Entitaet, an der die Tabelle haengt; null wenn keine"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst ist"},"inputs":{"type":"array","items":{},"description":"Die Eingangsgroessen (je `key` und `type`); leere Liste wenn keine definiert sind"},"rows":{"type":"array","items":{},"description":"Die Regelzeilen (je `when`, `then` und wahlweise `label`); leere Liste wenn keine"},"defaultResult":{"type":["object","null"],"additionalProperties":{},"description":"Ergebnis, wenn keine Zeile trifft; null wenn keins hinterlegt ist"},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"description":"FIRST nimmt den ersten Treffer, UNIQUE meldet mehrere Treffer als Konflikt. Nie null — Altbestand liest sich als FIRST"},"version":{"type":"integer","description":"Versionszaehler; jede Aenderung erhoeht ihn um eins"},"active":{"type":"boolean","description":"Ob die Tabelle bei Schreibvorgaengen wirklich greift"},"owner":{"type":["string","null"],"description":"Zustaendige Person; null wenn keine erfasst ist"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","name","entity","description","inputs","rows","defaultResult","hitPolicy","version","active","owner","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","entity":"string","description":"string","inputs":[],"rows":[],"defaultResult":{},"hitPolicy":"FIRST","version":0,"active":true,"owner":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Unbekannt ODER bereits stillgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["decision_table_not_found","not_found"]}},"required":["error"]}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Decision-tablesById","tags":["decision-tables"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liefert EINE Entscheidungstabelle samt `inputs`, `rows` und `defaultResult`. Eine stillgelegte Tabelle (`deleted_at`) wird NICHT geliefert und ergibt 404 — genau wie eine unbekannte Kennung. Die Versionshistorie ist nicht dabei; die liefert `GET /decision-tables/{id}/versions`. Die Antwort ist die Tabelle SELBST, ohne umschliessendes Feld.","summary":"Liefert EINE Entscheidungstabelle samt `inputs`, `rows` und `defaultResult`","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Die geaenderte Tabelle mit erhoehter Version, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Entscheidungstabelle"},"name":{"type":"string","description":"Name der Tabelle; NICHT auf Eindeutigkeit geprueft"},"entity":{"type":["string","null"],"description":"Entitaet, an der die Tabelle haengt; null wenn keine"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst ist"},"inputs":{"type":"array","items":{},"description":"Die Eingangsgroessen (je `key` und `type`); leere Liste wenn keine definiert sind"},"rows":{"type":"array","items":{},"description":"Die Regelzeilen (je `when`, `then` und wahlweise `label`); leere Liste wenn keine"},"defaultResult":{"type":["object","null"],"additionalProperties":{},"description":"Ergebnis, wenn keine Zeile trifft; null wenn keins hinterlegt ist"},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"description":"FIRST nimmt den ersten Treffer, UNIQUE meldet mehrere Treffer als Konflikt. Nie null — Altbestand liest sich als FIRST"},"version":{"type":"integer","description":"Versionszaehler; jede Aenderung erhoeht ihn um eins"},"active":{"type":"boolean","description":"Ob die Tabelle bei Schreibvorgaengen wirklich greift"},"owner":{"type":["string","null"],"description":"Zustaendige Person; null wenn keine erfasst ist"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","name","entity","description","inputs","rows","defaultResult","hitPolicy","version","active","owner","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","entity":"string","description":"string","inputs":[],"rows":[],"defaultResult":{},"hitPolicy":"FIRST","version":0,"active":true,"owner":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Unbekannt ODER bereits stillgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["decision_table_not_found","not_found"]}},"required":["error"]}}}},"503":{"description":"Aenderung fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"patchApiV1Decision-tablesById","tags":["decision-tables"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aendert eine Entscheidungstabelle und schreibt einen Versions-Schnappschuss","description":"Aktualisiert eine Entscheidungstabelle (nur Manager+, erhöht die Version und schreibt einen Snapshot in die Historie)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":160},"entity":{"type":["string","null"],"minLength":1,"maxLength":64},"description":{"type":["string","null"],"maxLength":4000},"inputs":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":80},"type":{"type":"string","enum":["number","text","boolean"]}},"required":["key","type"]}},"rows":{"type":"array","items":{"type":"object","properties":{"when":{"anyOf":[{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"]},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":200},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]}}},"required":["rules"]},{"type":"array","items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"]},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":200},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]}}},"required":["rules"]}}]},"then":{"type":"object","additionalProperties":{}},"label":{"type":"string","maxLength":160}},"required":["then"]}},"defaultResult":{"type":["object","null"],"additionalProperties":{}},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"]},"owner":{"type":["string","null"],"maxLength":120}}},"example":{"name":"string","entity":"string","description":"string","inputs":[{"key":"string","type":"number"}],"rows":[{"when":{"logic":"AND","rules":[{"field":"string","operator":"eq"}]},"then":{},"label":"string"}],"defaultResult":{},"hitPolicy":"FIRST","owner":"string"}}}}},"delete":{"responses":{"200":{"description":"Stillgelegt. Die Zeile bleibt mit gesetztem `deleted_at` stehen und verschwindet nur aus Liste und Einzelabruf; war sie aktiv und an eine Entitaet gebunden, wird deren Auswertungs-Zwischenspeicher verworfen.","content":{"application/json":{"schema":{"type":"object","properties":{"geloescht":{"type":"boolean","const":true,"description":"Die Tabelle traegt jetzt ein `deleted_at`; ihre Versionen bleiben"}},"required":["geloescht"]},"example":{"geloescht":true}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Nicht gefunden oder schon stillgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["decision_table_not_found","not_found"]}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar; `sqlState` nennt den Grund, sofern vorhanden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"deleteApiV1Decision-tablesById","tags":["decision-tables"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Legt eine Entscheidungstabelle still (Soft Delete über `deleted_at`). Die Versionen bleiben — sie sind die einzige Spur, wie hier entschieden wurde. Ein zweiter Aufruf ist ein 404, kein zweiter Erfolg.","summary":"Legt eine Entscheidungstabelle still (Soft Delete über `deleted_at`)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/decision-tables/{id}/simulate":{"post":{"responses":{"200":{"description":"Ergebnis samt `trace`. Der Trace fuehrt JEDE Zeile auf, auch die von einem frueheren Treffer verdeckten — daran ist zu sehen, welche Regeln nie greifen koennen. `matchedIndex` und `result` sind null, wenn keine Zeile traf und kein `defaultResult` hinterlegt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"tableId":{"type":"string","description":"Die simulierte Tabelle"},"version":{"type":"integer","description":"Version, gegen die simuliert wurde"},"active":{"type":"boolean","description":"Ob die Tabelle scharf geschaltet ist — die Simulation laeuft auch bei false"},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"description":"Angewandte Trefferregel"},"input":{"type":"object","additionalProperties":{},"description":"Der uebergebene Test-Eingang, unveraendert zurueckgegeben"},"trace":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"0-basierter Index der Zeile"},"label":{"type":"string","description":"Beschriftung der Zeile; fehlt wenn keine gesetzt ist"},"matched":{"type":"boolean","description":"Ob diese Zeile zugetroffen haette"}},"required":["index","matched"]},"description":"JEDE Zeile der Tabelle, auch die von einem frueheren Treffer verdeckten"},"matchedIndex":{"type":["integer","null"],"description":"0-basierter Index der treffenden Zeile; null wenn keine traf"},"result":{"type":["object","null"],"additionalProperties":{},"description":"Das `then` der treffenden Zeile, sonst `defaultResult`; null wenn beides fehlt"}},"required":["tableId","version","active","hitPolicy","input","trace","matchedIndex","result"]},"example":{"tableId":"string","version":0,"active":true,"hitPolicy":"FIRST","input":{},"trace":[{"index":0,"label":"string","matched":true}],"matchedIndex":0,"result":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Unbekannt ODER bereits stillgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["decision_table_not_found","not_found"]}},"required":["error"]}}}},"409":{"description":"Hit policy UNIQUE violated: more than one row matches the input. The response names every colliding row (conflictRows, 1-based) and still carries the full trace.","content":{"application/json":{"schema":{"type":"object","properties":{"tableId":{"type":"string","description":"Die simulierte Tabelle"},"version":{"type":"integer","description":"Version, gegen die simuliert wurde"},"active":{"type":"boolean","description":"Ob die Tabelle scharf geschaltet ist — die Simulation laeuft auch bei false"},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"description":"Angewandte Trefferregel"},"input":{"type":"object","additionalProperties":{},"description":"Der uebergebene Test-Eingang, unveraendert zurueckgegeben"},"trace":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"0-basierter Index der Zeile"},"label":{"type":"string","description":"Beschriftung der Zeile; fehlt wenn keine gesetzt ist"},"matched":{"type":"boolean","description":"Ob diese Zeile zugetroffen haette"}},"required":["index","matched"]},"description":"JEDE Zeile der Tabelle, auch die von einem frueheren Treffer verdeckten"},"error":{"type":"string","const":"decision_table_hit_policy_conflict"},"message":{"type":"string","description":"Englischer Text, der jede kollidierende Zeilennummer nennt"},"conflictRows":{"type":"array","items":{"type":"integer"},"description":"Die kollidierenden Zeilen als 1-basierte Nummern"},"conflictRowIndexes":{"type":"array","items":{"type":"integer"},"description":"Dieselben Zeilen als 0-basierte Indizes"},"conflictLabels":{"type":"array","items":{"type":["string","null"]},"description":"Beschriftung je kollidierender Zeile; null wo keine gesetzt ist"},"matchedIndex":{"type":"null","description":"Immer null — bei einem Konflikt wird keine Zeile gewaehlt"},"result":{"type":"null","description":"Immer null — es wird kein Ergebnis geraten"}},"required":["tableId","version","active","hitPolicy","input","trace","error","message","conflictRows","conflictRowIndexes","conflictLabels","matchedIndex","result"]}}}},"503":{"description":"Simulation fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Decision-tablesByIdSimulate","tags":["decision-tables"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Simuliert eine Entscheidungstabelle gegen einen Test-Eingang","description":"Simuliert eine Entscheidungstabelle gegen einen Test-Input (rein lesend, keine Änderung)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"input":{"type":"object","additionalProperties":{},"default":{}}}},"example":{"input":{}}}}}}},"/api/v1/decision-tables/{id}/activate":{"post":{"responses":{"200":{"description":"Die Tabelle mit active=true, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Entscheidungstabelle"},"name":{"type":"string","description":"Name der Tabelle; NICHT auf Eindeutigkeit geprueft"},"entity":{"type":["string","null"],"description":"Entitaet, an der die Tabelle haengt; null wenn keine"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst ist"},"inputs":{"type":"array","items":{},"description":"Die Eingangsgroessen (je `key` und `type`); leere Liste wenn keine definiert sind"},"rows":{"type":"array","items":{},"description":"Die Regelzeilen (je `when`, `then` und wahlweise `label`); leere Liste wenn keine"},"defaultResult":{"type":["object","null"],"additionalProperties":{},"description":"Ergebnis, wenn keine Zeile trifft; null wenn keins hinterlegt ist"},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"description":"FIRST nimmt den ersten Treffer, UNIQUE meldet mehrere Treffer als Konflikt. Nie null — Altbestand liest sich als FIRST"},"version":{"type":"integer","description":"Versionszaehler; jede Aenderung erhoeht ihn um eins"},"active":{"type":"boolean","description":"Ob die Tabelle bei Schreibvorgaengen wirklich greift"},"owner":{"type":["string","null"],"description":"Zustaendige Person; null wenn keine erfasst ist"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","name","entity","description","inputs","rows","defaultResult","hitPolicy","version","active","owner","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","entity":"string","description":"string","inputs":[],"rows":[],"defaultResult":{},"hitPolicy":"FIRST","version":0,"active":true,"owner":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Unbekannt ODER bereits stillgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["decision_table_not_found","not_found"]}},"required":["error"]}}}},"503":{"description":"Schalten fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Decision-tablesByIdActivate","tags":["decision-tables"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt `active` auf true — ab dann greift die Tabelle bei Schreibvorgaengen der verknuepften Entitaet. Die Regeln werden dabei NICHT geprueft: eine Tabelle ohne Zeilen oder mit widerspruechlichen Regeln laesst sich genauso scharfstellen. Vorher `POST /decision-tables/{id}/simulate` benutzen. Der Rumpf wird nicht gelesen, der Aufruf ist wiederholbar, die Version steigt dabei NICHT und es entsteht kein Schnappschuss. Eine stillgelegte Tabelle ergibt 404. Erfordert mindestens die Rolle `manager`.","summary":"Setzt `active` auf true","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/decision-tables/{id}/deactivate":{"post":{"responses":{"200":{"description":"Die Tabelle mit active=false, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Entscheidungstabelle"},"name":{"type":"string","description":"Name der Tabelle; NICHT auf Eindeutigkeit geprueft"},"entity":{"type":["string","null"],"description":"Entitaet, an der die Tabelle haengt; null wenn keine"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst ist"},"inputs":{"type":"array","items":{},"description":"Die Eingangsgroessen (je `key` und `type`); leere Liste wenn keine definiert sind"},"rows":{"type":"array","items":{},"description":"Die Regelzeilen (je `when`, `then` und wahlweise `label`); leere Liste wenn keine"},"defaultResult":{"type":["object","null"],"additionalProperties":{},"description":"Ergebnis, wenn keine Zeile trifft; null wenn keins hinterlegt ist"},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"description":"FIRST nimmt den ersten Treffer, UNIQUE meldet mehrere Treffer als Konflikt. Nie null — Altbestand liest sich als FIRST"},"version":{"type":"integer","description":"Versionszaehler; jede Aenderung erhoeht ihn um eins"},"active":{"type":"boolean","description":"Ob die Tabelle bei Schreibvorgaengen wirklich greift"},"owner":{"type":["string","null"],"description":"Zustaendige Person; null wenn keine erfasst ist"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","name","entity","description","inputs","rows","defaultResult","hitPolicy","version","active","owner","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","entity":"string","description":"string","inputs":[],"rows":[],"defaultResult":{},"hitPolicy":"FIRST","version":0,"active":true,"owner":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Unbekannt ODER bereits stillgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["decision_table_not_found","not_found"]}},"required":["error"]}}}},"503":{"description":"Schalten fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Decision-tablesByIdDeactivate","tags":["decision-tables"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt `active` auf false — die Tabelle greift nicht mehr, bleibt aber vollstaendig erhalten und in der Liste sichtbar. Das ist NICHT das Stilllegen; dafuer gibt es `DELETE /decision-tables/{id}`. Der Rumpf wird nicht gelesen, der Aufruf ist wiederholbar, die Version steigt dabei NICHT und es entsteht kein Schnappschuss. Eine stillgelegte Tabelle ergibt 404. Erfordert mindestens die Rolle `manager`.","summary":"Setzt `active` auf false","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/decision-tables/{id}/versions":{"get":{"responses":{"200":{"description":"Alle Schnappschuesse, neueste Version zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Schnappschusses"},"tableId":{"type":"string","description":"Die Tabelle, zu der er gehoert"},"version":{"type":"integer","description":"Versionsnummer des festgehaltenen Standes"},"snapshot":{"type":"object","additionalProperties":{},"description":"Der vollstaendige Stand VOR der Aenderung; leeres Objekt wenn nicht lesbar"},"createdAt":{"type":"string","description":"Zeitpunkt des Schnappschusses"}},"required":["id","tableId","version","snapshot","createdAt"]},"description":"Die Schnappschuesse, neueste Version zuerst"},"total":{"type":"integer","minimum":0,"description":"Anzahl der Schnappschuesse — es wird nicht geblaettert"}},"required":["data","total"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","tableId":"string","version":0,"snapshot":{},"createdAt":"string"}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Unbekannt ODER bereits stillgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["decision_table_not_found","not_found"]}},"required":["error"]}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Decision-tablesByIdVersions","tags":["decision-tables"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liefert alle Schnappschuesse der Tabelle, neueste Version zuerst. Jeder Schnappschuss haelt den Stand VOR einer Aenderung fest und wird nur von `PATCH /decision-tables/{id}` geschrieben — Aktivieren und Deaktivieren erzeugen KEINEN. Eine nie geaenderte Tabelle hat deshalb eine leere Historie. Die Eintraege werden nie geaendert oder geloescht, auch nicht beim Stilllegen der Tabelle. Es wird NICHT geblaettert. Eine stillgelegte Tabelle ergibt hier allerdings 404 — an ihre Historie kommt man ueber diese Route dann nicht mehr.","summary":"Liefert alle Schnappschuesse der Tabelle, neueste Version zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/decision-tables/import-dmn":{"post":{"responses":{"201":{"description":"Alle Tabellen angelegt — INAKTIV, mit Anzahl der Tabellen und Regeln","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Entscheidungstabelle"},"name":{"type":"string","description":"Name der Tabelle; NICHT auf Eindeutigkeit geprueft"},"entity":{"type":["string","null"],"description":"Entitaet, an der die Tabelle haengt; null wenn keine"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine erfasst ist"},"inputs":{"type":"array","items":{},"description":"Die Eingangsgroessen (je `key` und `type`); leere Liste wenn keine definiert sind"},"rows":{"type":"array","items":{},"description":"Die Regelzeilen (je `when`, `then` und wahlweise `label`); leere Liste wenn keine"},"defaultResult":{"type":["object","null"],"additionalProperties":{},"description":"Ergebnis, wenn keine Zeile trifft; null wenn keins hinterlegt ist"},"hitPolicy":{"type":"string","enum":["FIRST","UNIQUE"],"description":"FIRST nimmt den ersten Treffer, UNIQUE meldet mehrere Treffer als Konflikt. Nie null — Altbestand liest sich als FIRST"},"version":{"type":"integer","description":"Versionszaehler; jede Aenderung erhoeht ihn um eins"},"active":{"type":"boolean","description":"Ob die Tabelle bei Schreibvorgaengen wirklich greift"},"owner":{"type":["string","null"],"description":"Zustaendige Person; null wenn keine erfasst ist"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","name","entity","description","inputs","rows","defaultResult","hitPolicy","version","active","owner","createdAt","updatedAt"]},"description":"Die angelegten Tabellen — alle INAKTIV"},"tabellen":{"type":"integer","minimum":1,"description":"Anzahl der angelegten Tabellen"},"regeln":{"type":"integer","minimum":0,"description":"Summe der Regelzeilen ueber alle angelegten Tabellen"}},"required":["data","tabellen","regeln"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","name":"string","entity":"string","description":"string","inputs":[],"rows":[],"defaultResult":{},"hitPolicy":"FIRST","version":0,"active":true,"owner":"string","createdAt":"string","updatedAt":"string"}],"tabellen":1,"regeln":0}}}},"400":{"description":"Die Datei ließ sich nicht übersetzen (`dmn_not_readable`, die Meldung nennt die Stelle) ODER sie enthielt keine Entscheidungstabelle (`dmn_empty`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["dmn_not_readable","dmn_empty"]},"message":{"type":"string","description":"Bei `dmn_not_readable`: die Stelle mit Regel, Spalte und Wortlaut"}},"required":["error","message"]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"409":{"description":"Mindestens ein Tabellenname ist schon vergeben — es wurde NICHTS eingelesen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"name_conflict"},"message":{"type":"string","description":"Klartext-Hinweis, dass NICHTS eingelesen wurde"},"namen":{"type":"array","items":{"type":"string"},"description":"Alle schon vergebenen Namen"}},"required":["error","message","namen"]}}}},"503":{"description":"Datenbank nicht erreichbar; `sqlState` nennt den Grund, sofern vorhanden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"},"sqlState":{"type":"string","description":"SQLSTATE der Datenbank, sofern einer vorlag"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Decision-tablesImport-dmn","tags":["decision-tables"],"parameters":[],"summary":"Liest eine DMN-1.3-Datei und legt die enthaltenen Tabellen an","description":"Liest eine DMN-1.3-Datei und legt jede enthaltene Entscheidungstabelle an — alles oder nichts. Ausdrücke, die sich nicht exakt übersetzen lassen, werden namentlich abgelehnt (mit Regel, Spalte und Wortlaut), ebenso Trefferregeln außer FIRST und UNIQUE. Die Tabellen entstehen INAKTIV: erst simulieren, dann schalten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"xml":{"type":"string","minLength":1,"maxLength":4000000},"entity":{"type":"string","minLength":1,"maxLength":64}},"required":["xml"]},"example":{"xml":"string","entity":"string"}}}}}},"/api/v1/customizing-introspect/overview":{"get":{"responses":{"200":{"description":"Überblick — leere Abschnitte heiszen auch „Quelle fehlt\"","content":{"application/json":{"schema":{"type":"object","properties":{"fields":{"type":"array","items":{"type":"object","properties":{"entity":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"fieldId":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"showInList":{"type":"boolean"}},"required":["fieldId","label","type","showInList"]}}},"required":["entity","fields"]}},"customEntities":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"fieldCount":{"type":"integer"}},"required":["slug","name","fieldCount"]}},"workflows":{"type":"array","items":{"type":"object","properties":{"id":{},"name":{"type":"string"},"triggerType":{"type":["string","null"]},"entity":{"type":["string","null"]},"active":{"type":"boolean"}},"required":["name","triggerType","entity","active"]}},"rules":{"type":"array","items":{"type":"object","properties":{"id":{},"entity":{"type":["string","null"]},"ruleType":{"type":["string","null"]},"targetField":{"type":["string","null"]},"messageDe":{"type":["string","null"]},"active":{"type":"boolean"},"label":{"type":"string"}},"required":["entity","ruleType","targetField","messageDe","active","label"]}},"recentCustomizations":{"type":"array","items":{"type":"object","properties":{"id":{},"kind":{},"entity":{},"artifactId":{},"version":{},"status":{},"createdBy":{},"createdAt":{}}}}},"required":["fields","customEntities","workflows","rules","recentCustomizations"]},"example":{"fields":[{"entity":"string","fields":[{"fieldId":"string","label":"string","type":"string","showInList":true}]}],"customEntities":[{"slug":"string","name":"string","fieldCount":0}],"workflows":[{"name":"string","triggerType":"string","entity":"string","active":true}],"rules":[{"entity":"string","ruleType":"string","targetField":"string","messageDe":"string","active":true,"label":"string"}],"recentCustomizations":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unterhalb von „manager\""},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Customizing-introspectOverview","tags":["customizing-introspect"],"parameters":[],"summary":"Ueberblick ueber alle Anpassungen dieses Mandanten","description":"Customizing-Überblick des Mandanten: Eigene Felder, Custom-Entities, Workflows, Regeln und die letzten Manifest-Einträge. Ab Rolle „manager\". Fuenf getrennte Abfragen in EINER Antwort — jede fuer sich fehlertolerant: fehlt eine Tabelle, bleibt ihr Abschnitt leer, statt den ganzen Aufruf scheitern zu lassen. Eine leere Liste heiszt darum „nichts angelegt ODER Quelle fehlt\". Zwei Abschnitte sind gedeckelt: hoechstens 100 Regeln und die 20 juengsten Manifest-Eintraege — die uebrigen kommen vollstaendig. Soft-geloeschte Regeln und nicht aktive eigene Entitaeten bleiben auszen vor. Die Regeln nennen `label`, `ruleType` und `targetField`, aber NICHT Bedingung und Ausdruck: die Auskunft beantwortet „was gibt es\", nicht „wie rechnet es\". Rein lesend."}},"/api/v1/compliance-advisor/checks":{"get":{"responses":{"200":{"description":"Befundliste","content":{"application/json":{"schema":{"type":"object","properties":{"scannedInvoices":{"type":"integer"},"summary":{"type":"object","properties":{"rot":{"type":"integer"},"gelb":{"type":"integer"},"gruen":{"type":"integer"},"total":{"type":"integer"}},"required":["rot","gelb","gruen","total"]},"findings":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"severity":{"type":"string","enum":["rot","gelb","gruen"]},"rule":{"type":"string"},"legalRef":{"type":"string"},"entity":{"type":"object","properties":{"type":{"type":"string","enum":["invoice","series","tenant"]},"id":{"type":["string","null"]},"label":{"type":"string"},"href":{"type":["string","null"]}},"required":["type","id","label","href"]},"message":{"type":"string"},"explanation":{"type":"string"},"suggestedFix":{"type":"string"},"applicable":{"type":"boolean"}},"required":["id","severity","rule","legalRef","entity","message","explanation","suggestedFix","applicable"]}},"checkedAt":{"type":"string"}},"required":["scannedInvoices","summary","findings","checkedAt"]},"example":{"scannedInvoices":0,"summary":{"rot":0,"gelb":0,"gruen":0,"total":0},"findings":[{"id":"string","severity":"rot","rule":"string","legalRef":"string","entity":{"type":"invoice","id":"string","label":"string","href":"string"},"message":"string","explanation":"string","suggestedFix":"string","applicable":true}],"checkedAt":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Unzureichende Rolle"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"getApiV1Compliance-advisorChecks","tags":["compliance"],"parameters":[{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}}],"summary":"Prueft die letzten Belege auf GoBD-, UStG- und XRechnung-Verstoesse","description":"Scannt die letzten Rechnungen/Belege auf GoBD-/UStG-/XRechnung-Verstöße und liefert bestätigbare Befunde gruppiert nach Severity (rot/gelb/grün). Betrachtet werden nur die juengsten Belege — `limit` (1…1000, Vorgabe 200) sagt wie viele; `scannedInvoices` nennt die tatsaechlich geprueften, NICHT den Gesamtbestand. Ein leerer Befund heiszt also „in diesem Ausschnitt nichts gefunden\". Geprueft werden Luecken in der Rechnungsnummernfolge, unzulaessige Steuersaetze und Summenabweichungen, die Pflichtfelder der E-Rechnung sowie die Empfaengerangaben. Die Erklaerung zu jeder Regel kommt aus der hinterlegten Wissensbasis, sonst aus einem festen Text. Rein lesend: es wird nichts geaendert und nichts gespeichert. Nur `applicable: true` laesst sich anschlieszend ueber /checks/{id}/apply korrigieren."}},"/api/v1/compliance-advisor/checks/{id}/apply":{"post":{"responses":{"200":{"description":"Fix angewendet","content":{"application/json":{"schema":{"type":"object","properties":{"applied":{"type":"boolean","const":true},"invoiceId":{"type":"string"},"recomputed":{"type":"object","properties":{"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"}},"required":["subtotal","tax","total"]},"message":{"type":"string"}},"required":["applied","invoiceId","recomputed","message"]},"example":{"applied":true,"invoiceId":"string","recomputed":{"subtotal":0,"tax":0,"total":0},"message":"string"}}}},"400":{"description":"Befund-Kennung nicht deutbar"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Unzureichende Rolle"},"404":{"description":"Beleg nicht gefunden"},"409":{"description":"Beleg festgeschrieben — Storno erforderlich"},"422":{"description":"Kein sicherer Auto-Fix für diesen Befund"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"postApiV1Compliance-advisorChecksByIdApply","tags":["compliance"],"parameters":[{"in":"path","name":"id","schema":{"type":"string","minLength":1,"maxLength":120},"required":true}],"description":"Wendet — sofern eindeutig sicher — den vorgeschlagenen Fix eines Befunds an. Nur für Draft-Belege; sonst informativ. Nur ab Rolle „admin\", und derzeit ist genau EINE Befundart automatisch anwendbar: die Neuberechnung der Rechnungssumme aus den Positionen (Kennungen mit dem Praefix `sum-mismatch-`). Jeder andere Befund ergibt 422, ohne dass etwas geschieht. Gerechnet wird ueber die abrechenbaren Positionen — optionale, alternative sowie Abschnitts- und Hinweiszeilen bleiben auszen vor; Netto, Steuer und Brutto der Rechnung werden UEBERSCHRIEBEN, die Positionen selbst nicht angefasst. Festgeschriebene oder nicht mehr im Entwurf befindliche Belege werden mit 409 abgewiesen (GoBD: Korrektur nur per Storno). Es entsteht keine Journalbuchung und kein Rueckgaengig.","summary":"Wendet — sofern eindeutig sicher — den vorgeschlagenen Fix eines Befunds an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/data-quality/issues":{"get":{"responses":{"200":{"description":"Befundliste; leer heisst wirklich „nichts gefunden\".","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Selbstbeschreibende Kennung; sie traegt alles, was die Reparatur braucht."},"type":{"type":"string","enum":["Duplikat","Verwaiste Referenz","Fehlender Pflichtwert","GoBD-Sequenzluecke"]},"entity":{"type":"string","description":"Betroffene Tabelle oder Belegart."},"count":{"type":"integer","description":"Zahl der betroffenen Zeilen."},"detail":{"type":"string","description":"Klartext fuer die Anzeige."},"repairable":{"type":"boolean","description":"false: es gibt keinen Reparaturweg — die Reparatur antwortet dann 400."}},"required":["id","type","entity","count","detail","repairable"]}},"total":{"type":"integer"},"repairableCount":{"type":"integer","description":"Teilmenge von `total` mit `repairable: true`."}},"required":["data","total","repairableCount"]},"example":{"data":[{"id":"string","type":"Duplikat","entity":"string","count":0,"detail":"string","repairable":true}],"total":0,"repairableCount":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext; bei 500 bewusst ohne technische Einzelheiten."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}}},"operationId":"getApiV1Data-qualityIssues","tags":["Datenqualitaet"],"parameters":[],"summary":"Datenqualitaets-Befunde erheben","description":"Durchsucht den Mandanten nach Dubletten, verwaisten Verweisen und\nLuecken in den Belegnummern und gibt alle Befunde in EINER Liste\nzurueck. Es wird nichts geaendert.\n\nDer Aufruf rechnet bei jedem Mal neu — es gibt keinen Zwischenspeicher.\nBei grossen Mandanten ist das entsprechend teuer; die drei Suchen laufen\nimmerhin nebenlaeufig.\n\nDie Kennung eines Befunds ist kein Datenbankschluessel, sondern traegt\nihren Reparaturauftrag in sich. Sie wird nirgends abgelegt: eine Kennung\naus einem alten Aufruf kann auf einen Zustand zeigen, den es nicht mehr\ngibt. Die Reparatur faengt das ab, indem sie erneut nach dem Ist-Zustand\nsucht und dann `affected: 0` meldet.\n\n`repairable: false` heisst: fuer diese Art gibt es hier keinen Weg. Bei\nGoBD-Luecken ist das Absicht — eine Belegnummer nachtraeglich zu fuellen\nwaere kein Datenqualitaets-, sondern ein Buchhaltungsvorgang.\n\nAb Rolle `manager` lesbar; die Reparatur daneben verlangt `admin`."}},"/api/v1/data-quality/issues/{id}/repair":{"post":{"responses":{"200":{"description":"Repariert. Form je nach Befundart; `message` ist deutscher Klartext.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"type":{"type":"string","const":"Duplikat"},"merged":{"type":"integer","description":"Zusammengefuehrte Dubletten."},"repointed":{"type":"integer","description":"Umgehaengte Fremdverweise."},"message":{"type":"string"}},"required":["ok","type","merged","repointed","message"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"type":{"type":"string","const":"Verwaiste Referenz"},"action":{"type":"string","enum":["set_null","delete"],"description":"Was mit der verwaisten Zeile geschah."},"affected":{"type":"integer","description":"Betroffene Zeilen. 0 heisst: nichts mehr zu tun."},"message":{"type":"string"}},"required":["ok","type","action","affected","message"]}]},"example":{"ok":true,"type":"Duplikat","merged":0,"repointed":0,"message":"string"}}}},"400":{"description":"Kennung unbekannt oder nicht reparierbar (`unknown_or_unrepairable_issue`, `issue_not_repairable`, `fk_not_whitelisted`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext; bei 500 bewusst ohne technische Einzelheiten."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Die Reparatur schlug fehl. `message` ist bewusst allgemein — der technische Grund steht nur im Serverprotokoll.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext; bei 500 bewusst ohne technische Einzelheiten."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung, oder das Zusammenfuehren ist in dieser Fassung nicht verfuegbar (`repair_unavailable`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext; bei 500 bewusst ohne technische Einzelheiten."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}}},"operationId":"postApiV1Data-qualityIssuesByIdRepair","tags":["Datenqualitaet"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen Befund reparieren","description":"Fuehrt die Reparatur aus, die in der Befund-Kennung steckt. Die Kennung\nstammt aus `GET /issues`; ein anderer Wert ergibt 400, ohne dass etwas\ngeschieht.\n\nDIE AENDERUNG IST ENDGUELTIG — es gibt kein Zuruecknehmen:\n· Dublette: Zeilen werden zusammengefuehrt, die unterlegenen weich\n  geloescht, die Fremdverweise auf die fuehrende Zeile umgehaengt. Das\n  laeuft in EINER Transaktion.\n· Verwaister Verweis: das Feld wird auf NULL gesetzt ODER die Zeile\n  geloescht — was von beidem, sagt `action`, und es steht in einer\nfesten Liste im Quelltext, nicht im Aufruf. Ein Feldname, der nicht in\n  dieser Liste steht, ergibt 400 `fk_not_whitelisted`.\n\n`affected: 0` bzw. `merged: 0` ist ein ERFOLG, kein Fehler: die\nReparatur sucht selbst noch einmal nach dem Ist-Zustand. Wurde der\nBefund zwischenzeitlich anderweitig behoben, bleibt schlicht nichts zu\ntun. Genau deshalb schadet ein doppelter Aufruf nicht.\n\nDie Antwort hat ZWEI Formen unter 200, je nach Befundart — beide stehen\nim Schema. Der Eintrag ins Aktivitaetenprotokoll ist bestmoeglich: geht\ner schief, gilt die Reparatur trotzdem als erfolgt.\n\nVerlangt Rolle `admin` — die Leseroute daneben nur `manager`."}},"/api/v1/ai/process-builder/draft":{"post":{"responses":{"200":{"description":"Workflow-Entwurf + Warnungen. ACHTUNG: 200 heisst NICHT, dass ein Entwurf entstanden ist. Lieferte das Modell kein verwertbares JSON, kommt ebenfalls 200 — dann aber mit `draft: null`, einer Warnung und OHNE `meta`. Wich der Entwurf von der strengen Form ab und wurde repariert, steht `needsReview: true` und `warnings` nennt, was angepasst wurde.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"draft":{"type":"null"},"warnings":{"type":"array","items":{"type":"string"}},"needsReview":{"type":"boolean","const":true}},"required":["draft","warnings","needsReview"],"additionalProperties":false},{"type":"object","properties":{"draft":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":2000,"default":""},"trigger":{"type":"object","properties":{"type":{"type":"string","enum":["event","schedule","webhook_in","manual","record_change"]}},"required":["type"],"additionalProperties":true},"conditions":{"type":"array","items":{"type":"object","properties":{"logic":{"type":"string","enum":["AND","OR"],"default":"AND"},"rules":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","in","not_in","exists"]},"value":{}},"required":["field","operator"]},"default":[]}},"required":["logic","rules"]},"default":[]},"actions":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["email","webhook_out","set_field","create_record","notify","ai_action","wait","approval"]}},"required":["type"],"additionalProperties":true},"default":[]}},"required":["name","description","trigger","conditions","actions"]},"warnings":{"type":"array","items":{"type":"string"}},"needsReview":{"type":"boolean"},"meta":{"type":"object","properties":{"source":{"type":"string","const":"ai"},"schema":{"type":"string"},"model":{"type":"string"}},"required":["source","schema","model"],"additionalProperties":false}},"required":["draft","warnings","needsReview","meta"],"additionalProperties":false}]},"example":{"draft":null,"warnings":["string"],"needsReview":true}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden (admin only)"},"502":{"description":"LLM nicht verfügbar / Fehler"},"503":{"description":"DB nicht verfügbar"}},"operationId":"postApiV1AiProcess-builderDraft","tags":["ai"],"parameters":[],"summary":"Erzeugt aus einer Beschreibung einen Workflow-Entwurf, speichert nichts","description":"Erzeugt aus einer deutschen Prozess-Beschreibung einen Workflow-Entwurf (Trigger, Bedingungen, Aktionen). Speichert oder führt NICHTS aus — reines Drafting.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","minLength":8,"maxLength":800}},"required":["prompt"]},"example":{"prompt":"stringxx"}}}}}},"/api/v1/ai/assistants/templates":{"get":{"responses":{"200":{"description":"Vorlagen-Katalog","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"systemPrompt":{"type":"string"},"task":{"type":"string","enum":["standard","fast","planning","analytics"]},"icon":{"type":"string"}},"required":["key","name","description","systemPrompt","task","icon"]}}},"required":["data"]},"example":{"data":[{"key":"string","name":"string","description":"string","systemPrompt":"string","task":"standard","icon":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AiAssistantsTemplates","tags":["ai"],"parameters":[],"summary":"Liefert den Katalog fertiger Assistenten-Startvorlagen","description":"Fester, im Programm hinterlegter Katalog — fuer alle Mandanten identisch, ohne Datenbankzugriff. Die Eintraege sind KEINE angelegten Assistenten: der `key` dient nur der Zuordnung in der Oberflaeche, und aus einer Vorlage wird erst durch POST / ein echter Assistent. Die Antwort enthaelt den vollen System-Prompt jeder Vorlage, sodass er vor dem Uebernehmen angepasst werden kann."}},"/api/v1/ai/assistants":{"get":{"responses":{"200":{"description":"Liste der Assistenten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"systemPrompt":{"type":"string"},"task":{"type":"string"},"icon":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","name","description","systemPrompt","task","icon","createdBy","createdAt","updatedAt"]}}},"required":["data"]},"example":{"data":[{"id":"string","name":"string","description":"string","systemPrompt":"string","task":"string","icon":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"Unauthorized"},"503":{"description":"DB nicht verfügbar"}},"operationId":"getApiV1AiAssistants","tags":["ai"],"parameters":[],"summary":"Listet alle aktiven KI-Assistenten des Tenants","description":"Gibt ALLE nicht geloeschten Assistenten in EINER Antwort zurueck — ohne Blaetterung und ohne Filter —, zuletzt angelegte zuerst. Jeder Eintrag enthaelt den vollstaendigen System-Prompt; ab Rolle „user\" lesbar, obwohl nur „manager\" ihn aendern darf. Fehlt die Tabelle im Mandanten-Schema, legt der Aufruf sie an und antwortet mit einer leeren Liste."},"post":{"responses":{"201":{"description":"Assistent angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"systemPrompt":{"type":"string"},"task":{"type":"string"},"icon":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","name","description","systemPrompt","task","icon","createdBy","createdAt","updatedAt"]},"example":{"id":"string","name":"string","description":"string","systemPrompt":"string","task":"string","icon":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden (manager only)"},"409":{"description":"Name bereits vergeben"},"503":{"description":"DB nicht verfügbar"}},"operationId":"postApiV1AiAssistants","tags":["ai"],"parameters":[],"summary":"Legt einen neuen KI-Assistenten an","description":"Nur ab Rolle „manager\". Pflicht sind `name` (hoechstens 80 Zeichen) und `systemPrompt` (hoechstens 8000); `task` steuert die Modellwahl (standard/fast/planning/analytics, Vorgabe standard), `icon` und `description` sind rein darstellend. Der Name ist je Mandant eindeutig — ein bereits vergebener ergibt 409, auch wenn der bisherige Traeger soft-geloescht ist. Als Anleger wird die aufrufende Nutzer-Kennung vermerkt, nicht ein Wert aus dem Rumpf. Der Prompt wird nicht geprueft und nicht ausgefuehrt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","maxLength":300,"default":""},"systemPrompt":{"type":"string","minLength":1,"maxLength":8000},"task":{"type":"string","enum":["standard","fast","planning","analytics"],"default":"standard"},"icon":{"type":"string","maxLength":40,"default":"sparkles"}},"required":["name","systemPrompt"]},"example":{"name":"string","description":"string","systemPrompt":"string","task":"standard","icon":"string"}}}}}},"/api/v1/ai/assistants/{id}":{"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"systemPrompt":{"type":"string"},"task":{"type":"string"},"icon":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","name","description","systemPrompt","task","icon","createdBy","createdAt","updatedAt"]},"example":{"id":"string","name":"string","description":"string","systemPrompt":"string","task":"string","icon":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden (manager only)"},"404":{"description":"Nicht gefunden"},"409":{"description":"Name bereits vergeben"},"503":{"description":"DB nicht verfügbar"}},"operationId":"putApiV1AiAssistantsById","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aktualisiert einen KI-Assistenten (Teil-Update)","description":"Nur ab Rolle „manager\". Geschrieben werden ausschlieszlich die gesendeten Felder — ein leerer Rumpf aendert nichts und gibt den unveraenderten Datensatz zurueck. Ein neuer Name muss im Mandanten eindeutig bleiben (409). Soft-geloeschte Assistenten sind nicht aenderbar und ergeben 404; ein geloeschter laesst sich hierueber auch nicht zurueckholen. Bereits gestartete Laeufe sind nicht betroffen: der geaenderte System-Prompt gilt ab dem naechsten Lauf.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","maxLength":300},"systemPrompt":{"type":"string","minLength":1,"maxLength":8000},"task":{"type":"string","enum":["standard","fast","planning","analytics"]},"icon":{"type":"string","maxLength":40}}},"example":{"name":"string","description":"string","systemPrompt":"string","task":"standard","icon":"string"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden (manager only)"},"404":{"description":"Nicht gefunden"},"503":{"description":"DB nicht verfügbar"}},"operationId":"deleteApiV1AiAssistantsById","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Löscht einen KI-Assistenten (Soft-Delete)","description":"Nur ab Rolle „manager\". Setzt `deleted_at` — die Zeile bleibt bestehen und verschwindet nur aus Liste und Ausfuehrung. Der Name bleibt dadurch belegt: ein neuer Assistent mit demselben Namen wird mit 409 abgewiesen. Ein zweiter Aufruf auf dieselbe Kennung ergibt 404, weil bereits geloeschte Zeilen nicht mehr getroffen werden. Ueber diese Schnittstelle gibt es kein Zurueckholen."}},"/api/v1/ai/assistants/{id}/run":{"post":{"responses":{"200":{"description":"KI-Antwort + Token-Verbrauch","content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"usage":{"type":"object","properties":{"input_tokens":{"type":"integer"},"output_tokens":{"type":"integer"}},"required":["input_tokens","output_tokens"]}},"required":["text","usage"]},"example":{"text":"string","usage":{"input_tokens":0,"output_tokens":0}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Assistent nicht gefunden"},"502":{"description":"LLM-Fehler"},"503":{"description":"DB / KI nicht verfügbar"}},"operationId":"postApiV1AiAssistantsByIdRun","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Fuehrt einen KI-Assistenten einmalig aus, ohne etwas zu speichern","description":"Führt einen KI-Assistenten single-shot aus: Eingabetext (hoechstens 8000 Zeichen) → Antwort über den System-Prompt des Assistenten. Persistiert nichts: weder Eingabe noch Antwort werden gespeichert, es gibt keinen Verlauf und keinen Bezug auf vorherige Laeufe. Der Assistent hat KEINE Werkzeuge und keinen Zugriff auf Mandantendaten — er sieht nur den eingesendeten Text. Die Antwort ist auf 1500 Token begrenzt und kann daher abgeschnitten sein. Ab Rolle „user\" ausfuehrbar. Ist keine KI konfiguriert, kommt 503; ein Fehler des Modells wird zu 502 mit allgemeiner Meldung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"input":{"type":"string","minLength":1,"maxLength":8000}},"required":["input"]},"example":{"input":"string"}}}}}},"/api/v1/ai/dashboard-builder/draft":{"post":{"responses":{"200":{"description":"Widget-Entwurf + Warnungen","content":{"application/json":{"schema":{"type":"object","properties":{"widget":{"type":["object","null"],"properties":{"i":{"type":"string","description":"Kennung der Widget-Instanz"},"type":{"type":"string","enum":["kpi-card","bar-chart","line-chart","pie-chart","list","table"],"description":"Widget-Typ; ein unbekannter Vorschlag der KI wird auf einen dieser Werte zurechtgebogen"},"x":{"type":"number","description":"Spaltenposition im Raster"},"y":{"type":"number","description":"Zeilenposition im Raster"},"w":{"type":"number","description":"Breite in Rasterspalten"},"h":{"type":"number","description":"Hoehe in Rasterzeilen"},"minW":{"type":"number","description":"Kleinstmoegliche Breite"},"minH":{"type":"number","description":"Kleinstmoegliche Hoehe"},"config":{"type":"object","additionalProperties":{},"description":"Widget-Einstellungen (Titel, Endpunkt, Metrik, …), je nach Typ verschieden"}},"required":["i","type","x","y","w","h","minW","minH","config"],"description":"Der Entwurf; null, wenn die KI-Antwort kein brauchbares JSON enthielt"},"warnings":{"type":"array","items":{"type":"string"},"description":"Hinweise auf zurechtgebogene oder verworfene Angaben; bei widget=null steht hier der Grund"},"meta":{"type":"object","properties":{"source":{"type":"string","const":"ai","description":"Herkunft des Entwurfs"},"schema":{"type":"string","description":"Mandanten-Schema; fehlt, wenn kein Entwurf zustande kam"}},"required":["source"],"description":"Angaben zur Erzeugung"}},"required":["widget","warnings","meta"]},"example":{"widget":{"i":"string","type":"kpi-card","x":0,"y":0,"w":0,"h":0,"minW":0,"minH":0,"config":{}},"warnings":["string"],"meta":{"source":"ai","schema":"string"}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"502":{"description":"Die KI konnte keinen Entwurf erzeugen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"ai_error"},"message":{"type":"string"}},"required":["error","message"]}}}},"503":{"description":"KI-Anbieter nicht konfiguriert oder DB nicht verfügbar","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"ai_unavailable"},"message":{"type":"string"}},"required":["error","message"]},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}]}}}}},"operationId":"postApiV1AiDashboard-builderDraft","tags":["ai"],"parameters":[],"summary":"Erzeugt aus einer Beschreibung einen Widget-Entwurf, speichert nichts","description":"Erzeugt aus einer deutschen Kennzahl-/Widget-Beschreibung einen einzelnen Dashboard-Widget-Entwurf (WidgetInstance). Speichert NICHTS — reines Drafting; das Persistieren übernimmt das Frontend über die Dashboard-Layout-Endpunkte.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":600}},"required":["prompt"]},"example":{"prompt":"string"}}}}}},"/api/v1/ai/build-versions":{"get":{"responses":{"200":{"description":"Der Verlauf, neueste Anpassung zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"toolName":{"type":"string"},"label":{"type":"string"},"entity":{"type":["string","null"]},"fieldId":{"type":["string","null"]},"payload":{},"status":{"type":"string","enum":["active","reverted"]},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"revertedAt":{"type":["string","null"]}},"required":["id","toolName","label","entity","fieldId","status","createdBy","createdAt","revertedAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","toolName":"string","label":"string","entity":"string","fieldId":"string","status":"active","createdBy":"string","createdAt":"string","revertedAt":"string"}]}}}},"400":{"description":"Kein Mandantenkontext (`tenant context missing`). Der Koerper kommt aus `app.onError`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Kein Datenbank-Client verfuegbar oder die Abfrage ist gescheitert.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1AiBuild-versions","tags":["ai"],"parameters":[],"summary":"Verlauf der KI-Bau-Anpassungen des Mandanten","description":"Liefert die aufgezeichneten Bau-Anpassungen aus\n`\"<mandantenschema>\".ai_build_versions`, neueste zuerst. Die Tabelle\nwird beim ersten Aufruf angelegt — ein frischer Mandant bekommt eine\nleere Liste, keinen Fehler.\n\nDer Parameter `limit` wird NICHT geprueft und kann deshalb auch nicht\nmit 400 abgelehnt werden. Er wird durch `Number()` geschickt; ist das\nErgebnis keine Zahl, gilt 50. Danach klemmt der Speicher den Wert auf\n1 bis 200 fest. `?limit=abc`, `?limit=-5` und `?limit=99999` liefern\nalso alle eine gueltige Antwort — nur nicht die angeforderte Menge.\n\nEs gibt kein Blaettern und keine Gesamtzahl. Wer mehr als 200\nAnpassungen hat, sieht die aelteren ueber diesen Endpunkt nicht.\n\n`payload` ist eine JSONB-Spalte. Der Inhalt kommt unveraendert aus einer JSONB-Spalte. Es werden KEINE Feldnamen zugesagt — was heute darin steht, hat der Schreibpfad hineingelegt, nicht dieser Vertrag."}},"/api/v1/ai/build-versions/{id}/revert":{"post":{"responses":{"200":{"description":"Zurueckgerollt — oder war es schon. Zwei unterscheidbare Formen.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"alreadyReverted":{"type":"boolean","const":true}},"required":["ok","alreadyReverted"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"version":{"type":["object","null"],"properties":{"id":{"type":"string"},"toolName":{"type":"string"},"label":{"type":"string"},"entity":{"type":["string","null"]},"fieldId":{"type":["string","null"]},"payload":{},"status":{"type":"string","enum":["active","reverted"]},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"revertedAt":{"type":["string","null"]}},"required":["id","toolName","label","entity","fieldId","status","createdBy","createdAt","revertedAt"],"additionalProperties":false}},"required":["ok","version"],"additionalProperties":false}]},"example":{"ok":true,"alreadyReverted":true}}}},"400":{"description":"Kein Mandantenkontext (`tenant context missing`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Version nicht gefunden.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"409":{"description":"Das Feld liess sich im Anpassungs-Manifest nicht stilllegen. Die Version bleibt offen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"422":{"description":"Diese Art Anpassung kann nicht automatisch zurueckgerollt werden (alles ausser `create_custom_field`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar oder die Abfrage ist gescheitert.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1AiBuild-versionsByIdRevert","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine KI-Bau-Anpassung zurueckrollen (nur eigene Felder, teils nur bestes Bemuehen)","description":"Rollt eine aufgezeichnete Anpassung zurueck. Heute ist genau EINE Art\numkehrbar: `create_custom_field`. Alles andere wird mit 422 abgelehnt —\nausdruecklich, nicht stillschweigend.\n\nDer Vorgang hat drei Teile, und sie sind unterschiedlich verbindlich:\n\n  1. Die echte Spalte wird per `ALTER TABLE … DROP COLUMN IF EXISTS`\n     entfernt. DIESER TEIL IST BESTES BEMUEHEN: schlaegt er fehl, wird\n     der Fehler nur ins Protokoll geschrieben, und die Antwort bleibt\n     `ok: true`. Auch wenn Feld- oder Entitaetsname nicht der erlaubten\n     Schreibweise entsprechen, wird der Schritt still uebersprungen.\n  2. Registry-Zeile und Anpassungs-Manifest werden abgeraeumt. Das\n     passiert NUR, wenn der Anfragekontext eine `tenantId` traegt.\n     Fehlt sie, entfaellt der ganze Schritt — ohne Hinweis in der\n     Antwort — und die Version gilt trotzdem als zurueckgerollt.\n  3. Die Version wird auf `reverted` gesetzt. Nur dieser Teil ist\n     zugesichert.\n\nBleibt das Feld nach Schritt 2 im Manifest aktiv, bricht der Aufruf mit\n409 ab und die Version bleibt offen — dieser eine Fall wird also wirklich\ngegengeprueft, statt geglaubt zu werden.\n\nZWEI ANTWORTFORMEN unter 200: war die Version schon zurueckgerollt,\nkommt `{ ok: true, alreadyReverted: true }` und es passiert NICHTS.\nSonst `{ ok: true, version: … }`. `version` kann `null` sein, wenn die\nZeile zwischen Lesen und Schreiben verschwunden ist."}},"/api/v1/briefing":{"get":{"responses":{"200":{"description":"Tagesbriefing mit Kennzahlen, Listen und KI-Erzählung. `narrative` und `actionItems` sind IMMER gefüllt: ohne konfigurierten KI-Provider — und auch bei jedem Fehler des Modells — greift ein deterministisch aus den Zahlen gebauter deutscher Text. Woher der Text stammt, sagt die Antwort NICHT. Die drei Abfragen sind einzeln abgesichert: fällt eine aus, kommt sie leer und der Rest trotzdem — eine 0 kann deshalb „nichts offen\" oder „nicht lesbar\" heißen.","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"narrative":{"type":"string"},"actionItems":{"type":"array","items":{"type":"string"}},"counts":{"type":"object","properties":{"overdueCount":{"type":"number"},"overdueAmount":{"type":"number"},"dueTodayInvoicesCount":{"type":"number"},"openInvoicesCount":{"type":"number"},"weekTasksCount":{"type":"number"},"expiringQuotesCount":{"type":"number"}},"required":["overdueCount","overdueAmount","dueTodayInvoicesCount","openInvoicesCount","weekTasksCount","expiringQuotesCount"],"additionalProperties":false},"lists":{"type":"object","properties":{"overdueInvoices":{"type":"array","items":{"type":"object","properties":{"number":{"type":["string","null"]},"customerId":{"type":["string","null"]},"total":{"type":"number"},"paidAmount":{"type":"number"},"dueDate":{"type":["string","null"]}},"required":["number","customerId","total","paidAmount","dueDate"],"additionalProperties":false}},"weekTasks":{"type":"array","items":{"type":"object","properties":{"title":{"type":["string","null"]},"dueDate":{"type":["string","null"]},"status":{"type":["string","null"]}},"required":["title","dueDate","status"],"additionalProperties":false}},"expiringQuotes":{"type":"array","items":{"type":"object","properties":{"quoteNumber":{"type":["string","null"]},"customerName":{"type":["string","null"]},"total":{"type":"number"},"validUntil":{"type":["string","null"]}},"required":["quoteNumber","customerName","total","validUntil"],"additionalProperties":false}}},"required":["overdueInvoices","weekTasks","expiringQuotes"],"additionalProperties":false}},"required":["date","narrative","actionItems","counts","lists"],"additionalProperties":false},"example":{"date":"string","narrative":"string","actionItems":["string"],"counts":{"overdueCount":0,"overdueAmount":0,"dueTodayInvoicesCount":0,"openInvoicesCount":0,"weekTasksCount":0,"expiringQuotesCount":0},"lists":{"overdueInvoices":[{"number":"string","customerId":"string","total":0,"paidAmount":0,"dueDate":"string"}],"weekTasks":[{"title":"string","dueDate":"string","status":"string"}],"expiringQuotes":[{"quoteNumber":"string","customerName":"string","total":0,"validUntil":"string"}]}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"503":{"description":"DB nicht verfügbar"}},"operationId":"getApiV1Briefing","tags":["ai"],"parameters":[],"summary":"KI-Tagesbriefing: faellige Rechnungen, Aufgaben und ablaufende Angebote","description":"KI-Tagesbriefing: aggregiert überfällige Rechnungen, fällige Aufgaben dieser Woche und ablaufende Angebote und ergänzt eine KI-Erzählung + priorisierte Handlungsempfehlungen."}},"/api/v1/anomaly-detection":{"get":{"responses":{"200":{"description":"Anomalie-Befunde gruppiert nach Severity","content":{"application/json":{"schema":{"type":"object","properties":{"scannedInvoices":{"type":"integer","minimum":0,"description":"Anzahl der geprueften, nicht geloeschten Rechnungen; 0 wenn die Tabelle fehlt"},"summary":{"type":"object","properties":{"critical":{"type":"integer","minimum":0},"warning":{"type":"integer","minimum":0},"info":{"type":"integer","minimum":0}},"required":["critical","warning","info"],"description":"Anzahl der Befunde je Gewicht, gezaehlt aus findings"},"findings":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Befunds, aus Detektor und Beleg gebildet"},"type":{"type":"string","enum":["outlier_invoice_amount","duplicate_amount_same_customer","round_number_spike","journal_night_booking"],"description":"Welcher Detektor angeschlagen hat"},"severity":{"type":"string","enum":["critical","warning","info"],"description":"Gewicht des Verdachts"},"title":{"type":"string","description":"Kurzfassung des Befunds"},"detail":{"type":"string","description":"Erlaeuterung, warum der Detektor angeschlagen hat"},"reference":{"type":["string","null"],"description":"Belegnummer oder vergleichbarer Verweis; null wenn keiner vorliegt"},"amount":{"type":["number","null"],"description":"Betroffener Betrag in EUR; null wenn der Befund keinen Betrag hat"}},"required":["id","type","severity","title","detail","reference","amount"]},"description":"Hoechstens 40 Befunde, sortiert critical vor warning vor info"},"checkedAt":{"type":"string","format":"date-time","description":"Zeitpunkt des Laufs"}},"required":["scannedInvoices","summary","findings","checkedAt"]},"example":{"scannedInvoices":0,"summary":{"critical":0,"warning":0,"info":0},"findings":[{"id":"string","type":"outlier_invoice_amount","severity":"critical","title":"string","detail":"string","reference":"string","amount":0}],"checkedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"503":{"description":"DB nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Anomaly-detection","tags":["ai"],"parameters":[],"summary":"Erkennt Auffaelligkeiten in den Belegen des Mandanten, nur lesend","description":"KI-Anomalie-/Betrugserkennung: read-only Detektoren über die Belege des Tenants (Betrags-Ausreißer, mögliche Doppel-Abrechnungen, runde Großbeträge, Nacht-Buchungen). Mutiert nichts; worst case leere Befunde."}},"/api/v1/ai/integration-builder/draft":{"post":{"responses":{"200":{"description":"Der Entwurf und die Warnungen des Normalisierers. `draft: null` heisst: die Antwort des Modells liess sich nicht als Entwurf lesen — auch das ist 200, und `warnings` nennt den Grund. Welche Felder gefuellt sind, haengt an `kind`.","content":{"application/json":{"schema":{"type":"object","properties":{"draft":{"type":["object","null"],"properties":{"kind":{"type":"string","description":"webhook (ausgehend) | api_endpoint (eingehend)"},"name":{"type":"string"},"description":{"type":"string"},"event":{"type":"string","description":"Nur bei `webhook`: das ausloesende ERP-Ereignis"},"method":{"type":"string","description":"Nur bei `webhook`: POST | PUT | GET"},"targetUrlPlaceholder":{"type":"string","description":"Nur bei `webhook`: PLATZHALTER, keine echte Zieladresse"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"payloadExample":{"type":"object","additionalProperties":{},"description":"Beispiel, kein Vertrag"},"path":{"type":"string","description":"Nur bei `api_endpoint`"},"httpMethod":{"type":"string","description":"Nur bei `api_endpoint`: GET | POST | PUT | DELETE"},"requestSchema":{"type":"object","additionalProperties":{}},"responseExample":{"type":"object","additionalProperties":{},"description":"Beispiel, kein Vertrag"},"notes":{"type":"string"}},"required":["kind","name","description"],"description":"null, wenn aus der Antwort des Modells kein Entwurf zu lesen war"},"warnings":{"type":"array","items":{"type":"string"},"description":"Deutsche Hinweise auf das, was der Normalisierer geradegezogen hat"},"meta":{"type":"object","properties":{"source":{"type":"string","const":"ai"},"schema":{"type":"string","description":"Der Mandantenschema-Name; fehlt im Fall ohne Entwurf"}},"required":["source"]}},"required":["draft","warnings","meta"]},"example":{"draft":{"kind":"string","name":"string","description":"string","event":"string","method":"string","targetUrlPlaceholder":"string","headers":{"beispiel":"string"},"payloadExample":{},"path":"string","httpMethod":"string","requestSchema":{},"responseExample":{},"notes":"string"},"warnings":["string"],"meta":{"source":"ai","schema":"string"}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden (admin only)"},"502":{"description":"LLM-Fehler"},"503":{"description":"DB oder KI nicht verfügbar"}},"operationId":"postApiV1AiIntegration-builderDraft","tags":["ai"],"parameters":[],"summary":"Erzeugt aus einer Beschreibung einen Integrations-Entwurf, speichert nichts","description":"Erzeugt aus einer deutschen Schnittstellen-Beschreibung einen einzelnen Integrations-Entwurf (Webhook oder API-Endpunkt). Speichert oder führt NICHTS aus — rein beratendes Drafting; die Umsetzung übernimmt später ein Mensch.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":700}},"required":["prompt"]},"example":{"prompt":"string"}}}}}},"/api/v1/workflow-runs/{id}/runs":{"get":{"responses":{"200":{"description":"Laeufe der Seite plus Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Laufs"},"workflowId":{"type":"string","description":"Der ausgeloeste Workflow"},"tenantId":{"type":"string","description":"Mandant; `unknown` wenn beim Ausloesen kein Kontext gesetzt war"},"triggerType":{"type":"string","description":"Ausloeseart; ueber diese Route immer `manual`"},"status":{"type":"string","description":"Zustand des Laufs; beim Anlegen `pending`"},"startedAt":{"type":"string","format":"date-time","description":"Zeitpunkt des Ausloesens"},"finishedAt":{"type":"string","format":"date-time","description":"Ende des Laufs; fehlt solange er nicht fertig ist"},"stepResults":{"type":"array","items":{"type":"object","properties":{"actionId":{"type":"string","description":"Kennung des Schritts"},"actionType":{"type":"string","description":"Art des Schritts"},"ok":{"type":"boolean","description":"true, wenn der Schritt durchlief"},"durationMs":{"type":"number","description":"Dauer des Schritts in Millisekunden"},"error":{"type":"string","description":"Fehlergrund; fehlt bei ok=true"}},"required":["actionId","actionType","ok","durationMs"]},"description":"Ergebnisse der einzelnen Schritte; beim Anlegen leer"}},"required":["id","workflowId","tenantId","triggerType","status","startedAt","stepResults"]},"description":"Die Laeufe der Seite, neueste zuerst"},"total":{"type":"integer","minimum":0,"description":"Anzahl der vorgehaltenen Laeufe dieses Workflows"},"page":{"type":"integer","minimum":1,"description":"Angeforderte Seite"},"limit":{"type":"integer","maximum":100,"description":"Angeforderte Seitengroesse"},"meta":{"type":"object","properties":{"source":{"type":"string","const":"memory","description":"Herkunft der Daten"}},"required":["source"],"description":"Immer `memory` — die Historie liegt im Prozessspeicher"}},"required":["data","total","page","limit","meta"]},"example":{"data":[{"id":"string","workflowId":"string","tenantId":"string","triggerType":"string","status":"string","startedAt":"2026-01-01T12:00:00.000Z","finishedAt":"2026-01-01T12:00:00.000Z","stepResults":[{"actionId":"string","actionType":"string","ok":true,"durationMs":0,"error":"string"}]}],"total":0,"page":1,"limit":0,"meta":{"source":"memory"}}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Workflow-runsByIdRuns","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Blaettert durch die Laufhistorie EINES Workflows, neueste zuerst. Die Historie liegt im SPEICHER DES PROZESSES, nicht in der Datenbank: nach einem Neustart ist sie leer, sie wird nicht zwischen Containern geteilt, und je Workflow werden hoechstens 500 Laeufe vorgehalten — `total` ist deshalb die Zahl der VORGEHALTENEN, nicht der jemals gelaufenen. `limit` (Vorgabe 25, Obergrenze 100) und `page` (ab 1) blaettern. Ein unbekannter Workflow ergibt KEIN 404, sondern eine leere Liste — von einem leeren Puffer ist er nicht zu unterscheiden.","summary":"Blaettert durch die Laufhistorie EINES Workflows, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/workflow-runs/{id}/trigger":{"post":{"responses":{"202":{"description":"Laufsatz angelegt; `queued` sagt, ob er auch ausgefuehrt wird","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Klartext-Bestaetigung"},"runId":{"type":"string","description":"Kennung des angelegten Laufsatzes"},"queued":{"type":"boolean","description":"true nur, wenn ein Ausfuehrer vorhanden war; bei false ist der Satz angelegt, aber NICHTS laeuft"},"payload":{"type":"object","additionalProperties":{},"description":"Die uebergebenen Nutzdaten, unveraendert zurueckgegeben"}},"required":["message","runId","queued","payload"]},"example":{"message":"string","runId":"string","queued":true,"payload":{}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Manager-Rolle erforderlich"}},"operationId":"postApiV1Workflow-runsByIdTrigger","tags":["workflows"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Workflow-Lauf von Hand anstossen (202, Laufsatz angelegt)","description":"Legt einen Laufsatz mit Status `pending` in der Historie an und uebergibt ihn an den Ausfuehrer. Ob ueberhaupt etwas laeuft, sagt `queued`: ist kein Ausfuehrer verdrahtet, bleibt der Satz stehen und es passiert NICHTS — die Antwort ist trotzdem erfolgreich. Antwortet mit 202, nicht mit 201. Der Rumpf ist freiwillig: `eventType` (Vorgabe `manual.trigger`) und `payload`; ein unlesbarer Rumpf wird stillschweigend als leer behandelt. Ob es den Workflow gibt, wird NICHT geprueft — ein unbekannter ergibt kein 404. Erfordert mindestens die Rolle `manager`."}},"/api/v1/ai/dashboard":{"post":{"responses":{"200":{"description":"Erzeugte Dashboard-Konfiguration samt Modell- und Kostenangaben.","content":{"application/json":{"schema":{"type":"object","properties":{"config":{"type":"object","properties":{"layout":{"type":"string","enum":["grid","masonry"],"default":"grid"},"columns":{"type":"number","minimum":1,"maximum":4,"default":4},"widgets":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"kpi"},"id":{"type":"string"},"title":{"type":"string"},"metric":{"type":"string","enum":["revenue","orders_count","customers_new","invoices_open","inventory_low","projects_active"]},"period":{"type":"string","enum":["today","week","month","quarter","year"]},"comparison":{"type":"boolean","default":true},"position":{"type":"object","properties":{"col":{"type":"number","minimum":0,"maximum":3},"row":{"type":"number","minimum":0}},"required":["col","row"]},"size":{"type":"string","enum":["sm","md","lg"],"default":"md"}},"required":["type","id","title","metric","period","comparison","position","size"]},{"type":"object","properties":{"type":{"type":"string","const":"table"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string","enum":["orders","customers","invoices","inventory","projects","leads"]},"columns":{"type":"array","items":{"type":"string"},"maxItems":8},"sort":{"type":"string"},"filter":{"type":"string"},"limit":{"type":"number","minimum":5,"maximum":50,"default":10},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","source","columns","limit","position"]},{"type":"object","properties":{"type":{"type":"string","const":"chart"},"id":{"type":"string"},"title":{"type":"string"},"chartType":{"type":"string","enum":["bar","line","pie","area","funnel"]},"metric":{"type":"string"},"groupBy":{"type":"string"},"period":{"type":"string","enum":["week","month","quarter","year"]},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","chartType","metric","period","position"]},{"type":"object","properties":{"type":{"type":"string","const":"alert"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string"},"condition":{"type":"string"},"severity":{"type":"string","enum":["info","warning","critical"]},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","source","condition","severity","position"]}]},"minItems":1,"maxItems":12},"name":{"type":"string"}},"required":["layout","columns","widgets"]},"id":{"type":["string","null"]},"persisted":{"type":"boolean"},"message":{"type":"string"},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"},"tokensUsed":{"type":"object","properties":{"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"cachedInputTokens":{"type":"number"},"eurCents":{"type":"number"}},"required":["inputTokens","outputTokens","cachedInputTokens","eurCents"]}},"required":["config","id","persisted","message","modelUsed","fellBack","tokensUsed"]},"example":{"config":{"layout":"grid","columns":1,"widgets":[{"type":"kpi","id":"string","title":"string","metric":"revenue","period":"today","comparison":true,"position":{"col":0,"row":0},"size":"sm"}],"name":"string"},"id":"string","persisted":true,"message":"string","modelUsed":"string","fellBack":true,"tokensUsed":{"inputTokens":0,"outputTokens":0,"cachedInputTokens":0,"eurCents":0}}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1AiDashboard","tags":["ai"],"parameters":[],"summary":"Dashboard-Konfiguration aus Freitext erzeugen und speichern","description":"Laesst ein Sprachmodell aus dem Freitext in `intent` eine Widget-Konfiguration (KPI, Tabelle, Diagramm, Alarm) bauen; `existingConfig` wird als Ausgangsstand mitgegeben und geaendert statt ersetzt. Das Ergebnis wird als UI-Konfiguration vom Typ `dashboard` im Scope `global` gespeichert — schlaegt das fehl, kommt die Konfiguration trotzdem zurueck, dann mit `persisted: false`. Kostet Kontingent: ueberschrittenes Monatsbudget antwortet 402, ein gescheiterter Modellaufruf wird dem Mandanten wieder gutgeschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","minLength":1,"maxLength":1000},"existingConfig":{"type":"object","properties":{"layout":{"type":"string","enum":["grid","masonry"],"default":"grid"},"columns":{"type":"number","minimum":1,"maximum":4,"default":4},"widgets":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"kpi"},"id":{"type":"string"},"title":{"type":"string"},"metric":{"type":"string","enum":["revenue","orders_count","customers_new","invoices_open","inventory_low","projects_active"]},"period":{"type":"string","enum":["today","week","month","quarter","year"]},"comparison":{"type":"boolean","default":true},"position":{"type":"object","properties":{"col":{"type":"number","minimum":0,"maximum":3},"row":{"type":"number","minimum":0}},"required":["col","row"]},"size":{"type":"string","enum":["sm","md","lg"],"default":"md"}},"required":["type","id","title","metric","period","position"]},{"type":"object","properties":{"type":{"type":"string","const":"table"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string","enum":["orders","customers","invoices","inventory","projects","leads"]},"columns":{"type":"array","items":{"type":"string"},"maxItems":8},"sort":{"type":"string"},"filter":{"type":"string"},"limit":{"type":"number","minimum":5,"maximum":50,"default":10},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","source","columns","position"]},{"type":"object","properties":{"type":{"type":"string","const":"chart"},"id":{"type":"string"},"title":{"type":"string"},"chartType":{"type":"string","enum":["bar","line","pie","area","funnel"]},"metric":{"type":"string"},"groupBy":{"type":"string"},"period":{"type":"string","enum":["week","month","quarter","year"]},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","chartType","metric","period","position"]},{"type":"object","properties":{"type":{"type":"string","const":"alert"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string"},"condition":{"type":"string"},"severity":{"type":"string","enum":["info","warning","critical"]},"position":{"type":"object","properties":{"col":{"type":"number"},"row":{"type":"number"}},"required":["col","row"]}},"required":["type","id","title","source","condition","severity","position"]}]},"minItems":1,"maxItems":12},"name":{"type":"string"}},"required":["widgets"]}},"required":["intent"]},"example":{"intent":"string","existingConfig":{"layout":"grid","columns":1,"widgets":[{"type":"kpi","id":"string","title":"string","metric":"revenue","period":"today","comparison":true,"position":{"col":0,"row":0},"size":"sm"}],"name":"string"}}}}}},"get":{"responses":{"200":{"description":"Gespeicherte Konfiguration, oder `config: null` wenn keine hinterlegt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":["string","null"]},"config":{},"name":{"type":["string","null"]},"scope":{"type":["string","null"]},"updatedAt":{"type":["string","null"]},"warning":{"type":"string"}},"required":["id"]},"example":{"id":"string","name":"string","scope":"string","updatedAt":"string","warning":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1AiDashboard","tags":["ai"],"parameters":[],"description":"Liest die zuletzt gespeicherte Dashboard-Konfiguration des Mandanten. Der Abfrageparameter `scope` waehlt die Variante (Standard `global`). Gibt es keine, sind `config` und `id` `null` — das ist kein Fehler. Ist die Datenbank nicht erreichbar, antwortet der Endpunkt ebenfalls mit 200 und setzt zusaetzlich `warning`.","summary":"Liest die zuletzt gespeicherte Dashboard-Konfiguration des Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/workflow":{"post":{"responses":{"200":{"description":"Workflow-Entwurf samt Modell- und Kostenangaben.","content":{"application/json":{"schema":{"type":"object","properties":{"workflow":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"trigger":{"type":"object","properties":{"type":{"type":"string","enum":["event","schedule","webhook_in","manual","record_change"]},"event":{"type":"string"},"cron":{"type":"string"},"entity":{"type":"string"}},"required":["type"]},"conditions":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains"]},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"required":["field","operator","value"]}},"actions":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["email","webhook_out","set_field","notify","create_record","approval"]},"description":{"type":"string"},"config":{"type":"object","additionalProperties":{}}},"required":["type","description","config"]}},"explanation":{"type":"string"}},"required":["name","description","trigger","actions","explanation"]},"message":{"type":"string"},"explanation":{"type":"string"},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"},"tokensUsed":{"type":"object","properties":{"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"cachedInputTokens":{"type":"number"},"eurCents":{"type":"number"}},"required":["inputTokens","outputTokens","cachedInputTokens","eurCents"]}},"required":["workflow","message","explanation","modelUsed","fellBack","tokensUsed"]},"example":{"workflow":{"name":"string","description":"string","trigger":{"type":"event","event":"string","cron":"string","entity":"string"},"conditions":[{"field":"string","operator":"eq","value":"string"}],"actions":[{"type":"email","description":"string","config":{}}],"explanation":"string"},"message":"string","explanation":"string","modelUsed":"string","fellBack":true,"tokensUsed":{"inputTokens":0,"outputTokens":0,"cachedInputTokens":0,"eurCents":0}}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1AiWorkflow","tags":["ai"],"parameters":[],"description":"Uebersetzt den Freitext in `intent` in einen Workflow-Entwurf: Ausloeser (Ereignis, Zeitplan, Webhook, manuell, Datensatzaenderung), Bedingungen und Aktionen, dazu eine Begruendung in `explanation`. Der Entwurf wird NICHT gespeichert — er kommt nur zurueck. Kostet Kontingent: ueberschrittenes Monatsbudget antwortet 402, ein gescheiterter Modellaufruf wird gutgeschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","minLength":1,"maxLength":1000}},"required":["intent"]},"example":{"intent":"string"}}}},"summary":"Uebersetzt den Freitext in `intent` in einen Workflow-Entwurf","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/search":{"post":{"responses":{"200":{"description":"Treffer nach absteigender Aehnlichkeit. Leere Liste auch im Ausfall (siehe `warning`/`error`).","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"results":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"snippet":{"type":"string"},"score":{"type":"number"}},"required":["source","id","title","snippet","score"]}},"total":{"type":"number"},"warning":{"type":"string"},"error":{"type":"string"}},"required":["query","results","total"]},"example":{"query":"string","results":[{"source":"string","id":"string","title":"string","snippet":"string","score":0}],"total":0,"warning":"string","error":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1AiSearch","tags":["ai"],"parameters":[],"description":"Semantische Suche: `query` wird in einen 1536-dimensionalen Vektor uebersetzt und per Kosinus-Abstand gegen `public.ai_embeddings` gesucht, eingegrenzt auf den eigenen Mandanten und optional auf die in `sources` genannten Entitaetstypen. `limit` steuert die Trefferzahl (1..20, Standard 5). Faellt die Datenbank oder die Vektorsuche aus, antwortet der Endpunkt trotzdem mit 200 und leerer Trefferliste — der Grund steht dann in `warning` bzw. `error`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","minLength":1},"sources":{"type":"array","items":{"type":"string"}},"limit":{"type":"number","minimum":1,"maximum":20,"default":5}},"required":["query"]},"example":{"query":"string","sources":["string"],"limit":1}}}},"summary":"Semantische Suche","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/chat":{"post":{"responses":{"200":{"description":"Ohne `noStream` ein SSE-Strom (`data:`-Zeilen mit `{ type, value }`). Mit `noStream: true` das hier beschriebene JSON-Objekt.","content":{"text/event-stream":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"ragEnabled":{"type":"boolean"},"snippetCount":{"type":"number"},"cacheEnabled":{"type":"boolean"},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"},"message":{"type":"string"},"toolCalls":{"type":"array","items":{}},"toolResults":{"type":"array","items":{}},"citations":{"type":"array","items":{}}},"required":["tenantId","ragEnabled","snippetCount","cacheEnabled","modelUsed","fellBack","message","toolCalls","toolResults","citations"]},"example":{"tenantId":"string","ragEnabled":true,"snippetCount":0,"cacheEnabled":true,"modelUsed":"string","fellBack":true,"message":"string","toolCalls":[],"toolResults":[],"citations":[]}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1AiChat","tags":["ai"],"parameters":[],"description":"Der Chat des ERP-Assistenten mit Werkzeugaufrufen. `message` (bis 4000 Zeichen) ist die Frage, `history` (bis 20 Zuege) der Gespraechsverlauf. Standardfall ist ein Server-Sent-Events-Strom; `noStream: true` liefert stattdessen ein einzelnes JSON-Objekt. Der Text des Nutzers laeuft vorher durch die Injection-Pruefung und wird bereinigt weitergereicht, nicht abgelehnt. Destruktive Werkzeuge bleiben hier fail-closed: eine Bestaetigung aus dem Anfragerumpf oeffnet die Wand NICHT. Kostet Kontingent; ueberschrittenes Monatsbudget antwortet 402.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","minLength":1,"maxLength":4000},"history":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string"}},"required":["role","content"]},"maxItems":20},"useRag":{"type":"boolean"},"ragOptions":{"type":"object","properties":{"maxContext":{"type":"number","minimum":1,"maximum":50},"entityTypes":{"type":"array","items":{"type":"string"},"maxItems":10}}},"noStream":{"type":"boolean"}},"required":["message"]},"example":{"message":"string","history":[{"role":"user","content":"string"}],"useRag":true,"ragOptions":{"maxContext":1,"entityTypes":["string"]},"noStream":true}}}},"summary":"Der Chat des ERP-Assistenten mit Werkzeugaufrufen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/report":{"post":{"responses":{"200":{"description":"Die erzeugte Report-Definition und das benutzte Modell.","content":{"application/json":{"schema":{"type":"object","properties":{"report":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"source":{"type":"string","enum":["orders","customers","invoices","inventory","projects","leads"]},"metrics":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":8},"groupBy":{"type":"string"},"period":{"type":"string","enum":["week","month","quarter","year"],"default":"month"},"format":{"type":"string","enum":["table","chart","kpi"],"default":"table"},"explanation":{"type":"string"}},"required":["title","description","source","metrics","period","format","explanation"]},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"}},"required":["report","modelUsed","fellBack"]},"example":{"report":{"title":"string","description":"string","source":"orders","metrics":["string"],"groupBy":"string","period":"week","format":"table","explanation":"string"},"modelUsed":"string","fellBack":true}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1AiReport","tags":["ai"],"parameters":[],"description":"Baut aus dem Freitext in `intent` eine Report-Definition: Datenquelle, Kennzahlen, Gruppierung, Zeitraum und Darstellungsform (Tabelle, Diagramm oder Kennzahl). Der Report wird NICHT gespeichert und NICHT ausgefuehrt — es kommt nur die Definition zurueck. Kostet Kontingent; ueberschrittenes Monatsbudget antwortet 402.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","minLength":1,"maxLength":1000}},"required":["intent"]},"example":{"intent":"string"}}}},"summary":"Baut aus dem Freitext in `intent` eine Report-Definition","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/custom-field":{"post":{"responses":{"200":{"description":"Der Feldvorschlag samt Anzeige-SQL. Bei `apply: true` zusaetzlich `execution` mit dem Ergebnis der Pipeline.","content":{"application/json":{"schema":{"type":"object","properties":{"proposal":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","invoices","products","projects"]},"fieldKey":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,40}$"},"label":{"type":"string"},"type":{"type":"string","enum":["text","number","date","boolean","json"]},"required":{"type":"boolean","default":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"explanation":{"type":"string"}},"required":["entity","fieldKey","label","type","required","explanation"]},"proposedSql":{"type":"string"},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"},"applied":{"type":"boolean"},"message":{"type":"string"},"execution":{}},"required":["proposal","proposedSql","modelUsed","fellBack","applied"]}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1AiCustom-field","tags":["ai"],"parameters":[],"summary":"Zusatzfeld aus Freitext vorschlagen, auf Wunsch anlegen","description":"Leitet aus dem Freitext in `intent` eine Zusatzfeld-Definition ab (Entitaet, Feldschluessel in snake_case, Typ, Pflicht, Vorgabewert) und gibt daneben das zugehoerige `ALTER TABLE` als reinen Anzeigetext zurueck — dieser Text wird nie ausgefuehrt. Ohne `apply` bleibt es beim Vorschlag (`applied: false`). Mit `apply: true` laeuft die Aenderung durch die Werkzeug-Pipeline (RBAC, Vorschau, Transaktion, GoBD-Eintrag); vorher greifen eine Werkzeugwand (Dienstkonten 403, unbekanntes Werkzeug 404) und ein Tageslimit je Mandant (429).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","minLength":1,"maxLength":800},"apply":{"type":"boolean"}},"required":["intent"]},"example":{"intent":"string","apply":true}}}}}},"/api/v1/ai/integration-mapping":{"post":{"responses":{"200":{"description":"Die vorgeschlagene Feldzuordnung samt Sicherheiten und Restliste.","content":{"application/json":{"schema":{"type":"object","properties":{"mapping":{"type":"object","properties":{"provider":{"type":"string"},"fieldMap":{"type":"array","items":{"type":"object","properties":{"external":{"type":"string"},"internal":{"type":"string"},"transform":{"type":"string"},"confidence":{"type":"number","minimum":0,"maximum":1}},"required":["external","internal","confidence"]}},"unmapped":{"type":"array","items":{"type":"string"},"default":[]},"explanation":{"type":"string"}},"required":["provider","fieldMap","unmapped","explanation"]},"modelUsed":{"type":"string"},"fellBack":{"type":"boolean"}},"required":["mapping","modelUsed","fellBack"]},"example":{"mapping":{"provider":"string","fieldMap":[{"external":"string","internal":"string","transform":"string","confidence":0}],"unmapped":["string"],"explanation":"string"},"modelUsed":"string","fellBack":true}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1AiIntegration-mapping","tags":["ai"],"parameters":[],"summary":"Fremdsystem-Felder einer Nemix-Entitaet zuordnen","description":"Ordnet die in `externalFields` genannten Feldnamen eines Fremdsystems (bis 200) den Feldern der Nemix-Entitaet `internalEntity` zu. Je Zuordnung kommt eine Sicherheit zwischen 0 und 1 zurueck; was nicht sicher zuzuordnen war, steht in `unmapped`. Es wird nichts gespeichert und keine Integration eingerichtet. Kostet Kontingent; ueberschrittenes Monatsbudget antwortet 402.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"externalFields":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":200},"internalEntity":{"type":"string","enum":["customers","orders","invoices","products","projects"]}},"required":["provider","externalFields","internalEntity"]},"example":{"provider":"string","externalFields":["string"],"internalEntity":"customers"}}}}}},"/api/v1/ai/tts":{"post":{"responses":{"200":{"description":"Die Sprachausgabe als MP3-Bytestrom (Cache-Control: no-cache), kein JSON.","content":{"audio/mpeg":{}}},"400":{"description":"Rumpf ungueltig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unterhalb von „user\""},"502":{"description":"Der Sprachdienst antwortete nicht oder mit einem Fehler"},"503":{"description":"Kein Zugang zum Sprachdienst hinterlegt"},"504":{"description":"Der Sprachdienst antwortete nicht binnen 15 Sekunden"}},"operationId":"postApiV1AiTts","tags":["ai"],"parameters":[],"summary":"Text-to-Speech: generate audio from text (returns MP3 stream)","description":"Wandelt `text` (1…4000 Zeichen) in eine MP3-Datei. `voice` waehlt die Stimme (Vorgabe „alloy\"), `speed` die Geschwindigkeit (0,25…4, Vorgabe 1). Ab Rolle „user\". Die Erzeugung laeuft ueber einen externen Dienst; ohne hinterlegten Schluessel antwortet der Endpunkt mit 503, bei Zeitueberschreitung nach 15 Sekunden mit 504 und bei einem Fehler der Gegenstelle mit 502. NEBENWIRKUNG: die erzeugte Spieldauer wird auf das Sprachkontingent des Mandanten angerechnet — GESCHAETZT aus der Dateigroesze, nicht gemessen; scheitert die Anrechnung, wird sie still uebergangen und die Datei trotzdem geliefert. Die Datei wird NICHT abgelegt: sie kommt nur in dieser Antwort.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":4000},"voice":{"type":"string","enum":["alloy","echo","fable","onyx","nova","shimmer"],"default":"alloy"},"speed":{"type":"number","minimum":0.25,"maximum":4,"default":1}},"required":["text"]},"example":{"text":"string","voice":"alloy","speed":0.25}}}}}},"/api/v1/ai/transcribe":{"post":{"responses":{"200":{"description":"Der erkannte Text.","content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"provider":{"type":"string","enum":["openai-whisper","gemini","mock"]},"language":{"type":"string"},"durationBytes":{"type":"number"}},"required":["text","provider","language","durationBytes"],"additionalProperties":false},"example":{"text":"string","provider":"openai-whisper","language":"string","durationBytes":0}}}},"400":{"description":"Kein lesbares Formular, kein Audio-Feld oder leere Datei.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"invalid_multipart"},"detail":{"type":"string"}},"required":["error","detail"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"audio_field_missing"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"audio_empty"}},"required":["error"],"additionalProperties":false}]}}}},"401":{"description":"Unauthorized"},"413":{"description":"Datei groesser als 25 MB.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"audio_too_large"},"maxBytes":{"type":"number"}},"required":["error","maxBytes"],"additionalProperties":false}}}},"415":{"description":"Angegebener Medientyp wird nicht unterstuetzt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unsupported_mime"},"mimeType":{"type":"string"}},"required":["error","mimeType"],"additionalProperties":false}}}},"500":{"description":"Der Transkriptionsdienst hat einen Fehler gemeldet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"transcription_failed"},"detail":{"type":"string"}},"required":["error","detail"],"additionalProperties":false}}}},"503":{"description":"Kein Transkriptionsdienst eingerichtet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"no_transcription_provider_configured"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiTranscribe","tags":["ai"],"parameters":[],"summary":"Erstellt/Verarbeitet Ressource /transcribe (ai).","description":"Nimmt eine Audiodatei als `multipart/form-data` entgegen und gibt den\nerkannten Text zurueck. Das Feld heisst `audio`; `file` wird ebenfalls\nakzeptiert. Ein optionales Feld `language` setzt den Sprachcode,\nohne Angabe `de`.\n\nDie Datei darf hoechstens 25 MB gross sein, sonst 413. Ein Medientyp\nausserhalb der erlaubten Audio- und WebM/MP4-Container fuehrt zu 415.\nFehlt der Typ oder ist er `application/octet-stream`, wird `audio/webm`\nangenommen statt abgelehnt.\n\nDer Aufruf geht an einen externen Dienst und KOSTET: zuerst OpenAI\nWhisper, wenn `OPENAI_API_KEY` gesetzt ist, sonst Google Gemini. Ist\nkeiner von beiden eingerichtet, antwortet der Endpunkt 503, ohne die\nDatei zu verarbeiten. Es wird nichts gespeichert — weder die Audiodatei\nnoch der Text noch ein Protokolleintrag."}},"/api/v1/ai/page-helper":{"post":{"responses":{"200":{"description":"Ohne `stream: false` ein SSE-Strom (`data:`-Zeilen). Mit `stream: false` das hier beschriebene JSON-Objekt.","content":{"text/event-stream":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object","properties":{"reply":{"type":"string","description":"Die vollstaendige Antwort des Modells als Text"},"usage":{"type":"object","properties":{"inputTokens":{"type":"integer"},"outputTokens":{"type":"integer"}},"required":["inputTokens","outputTokens"]},"toolsUsed":{"type":"integer","description":"Wie viele Werkzeuge ANGEBOTEN wurden — nicht, wie viele liefen"},"page":{"type":"string","description":"Die Seitenkennung aus der Anfrage, zurueckgespiegelt"},"toolResults":{"type":"array","items":{"type":"object","properties":{"toolName":{"type":"string"},"result":{"description":"Was das Werkzeug zurueckgab. Ueber 64 KB wird der Wert durch einen Hinweis ERSETZT, nicht abgeschnitten — ein halbes Objekt, das noch wie ein ganzes aussieht, waere schlimmer."}},"required":["toolName"]},"description":"Alle Werkzeug-Ergebnisse ueber ALLE Schritte hinweg, in Aufrufreihenfolge"}},"required":["reply","usage","toolsUsed","page","toolResults"]},"example":{"reply":"string","usage":{"inputTokens":0,"outputTokens":0},"toolsUsed":0,"page":"string","toolResults":[{"toolName":"string"}]}}}},"401":{"description":"Unauthorized"},"402":{"description":"Quota exceeded"},"503":{"description":"AI provider unavailable (circuit open)"}},"operationId":"postApiV1AiPage-helper","tags":["ai"],"parameters":[],"description":"Der Seiten-Assistent: beantwortet `message` im Kontext der Seite, auf der der Nutzer gerade steht. `context.page` entscheidet, WELCHE Werkzeuge das Modell ueberhaupt angeboten bekommt — dieselbe Frage auf einer anderen Seite kann anders beantwortet werden. `selectedIds`, `filters` und `visibleTab` geben den sichtbaren Ausschnitt mit. Standardfall ist ein Server-Sent-Events-Strom; `stream: false` liefert stattdessen ein JSON-Objekt, das zusaetzlich die Werkzeug-Ergebnisse einzeln nennt. Der Text des Nutzers laeuft vorher durch die Injection-Pruefung. Destruktive Werkzeuge bleiben fail-closed: eine Bestaetigung aus dem Anfragerumpf oeffnet die Wand NICHT.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","minLength":1,"maxLength":4000},"context":{"type":"object","properties":{"page":{"type":"string"},"selectedIds":{"type":"array","items":{"type":"string"}},"filters":{"type":"object","additionalProperties":{}},"visibleTab":{"type":"string"}},"required":["page"]},"stream":{"type":"boolean","default":true}},"required":["message","context"]},"example":{"message":"string","context":{"page":"string","selectedIds":["string"],"filters":{},"visibleTab":"string"},"stream":true}}}},"summary":"Der Seiten-Assistent","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/upload":{"post":{"responses":{"200":{"description":"Datei abgelegt und eingestuft. Der Vorschlag wartet auf Bestaetigung.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"requiresConfirmation":{"type":"boolean","const":true},"autoApplyEligible":{"type":"boolean"},"classification":{"type":"object","properties":{"type":{"type":"string"},"confidence":{"type":"number"},"candidates":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"confidence":{"type":"number"}},"required":["type","confidence"],"additionalProperties":false}},"fields":{"type":"object","additionalProperties":{"type":["string","null"]}}},"required":["type","confidence","candidates","fields"],"additionalProperties":false},"proposal":{"type":"object","properties":{"what":{"type":"string"},"how":{"type":"string"},"where":{"type":"string"}},"required":["what","how","where"],"additionalProperties":false},"suggestionId":{"type":"string"},"status":{"type":"string","const":"pending"},"classified":{"type":"object","properties":{"type":{"type":"string"},"confidence":{"type":"number"},"suggested_action":{"type":"string"}},"required":["type","confidence","suggested_action"],"additionalProperties":false},"key":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":"number"},"originalName":{"type":"string"},"extractedText":{"type":["string","null"]}},"required":["ok","requiresConfirmation","autoApplyEligible","classification","proposal","suggestionId","status","classified","key","mimeType","sizeBytes","originalName","extractedText"],"additionalProperties":false},"example":{"ok":true,"requiresConfirmation":true,"autoApplyEligible":true,"classification":{"type":"string","confidence":0,"candidates":[{"type":"string","confidence":0}],"fields":{"beispiel":"string"}},"proposal":{"what":"string","how":"string","where":"string"},"suggestionId":"string","status":"pending","classified":{"type":"string","confidence":0,"suggested_action":"string"},"key":"string","mimeType":"string","sizeBytes":0,"originalName":"string","extractedText":"string"}}}},"400":{"description":"Kein `multipart/form-data` oder kein Feld `file`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext (`tenant_context_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"413":{"description":"Datei zu gross.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"415":{"description":"Nicht erlaubter Dateityp, oder die Signaturpruefung hat die Datei abgelehnt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"500":{"description":"Der Vorschlag liess sich nicht sichern (kein Verbindungsfehler). Es wird bewusst KEINE Kennung ausgegeben, die niemand bestaetigen koennte.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Der Vorschlag liess sich wegen eines Verbindungsfehlers nicht sichern.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1AiUpload","tags":["ai"],"parameters":[],"summary":"Datei hochladen und einstufen lassen (schreibt noch nichts)","description":"Nimmt eine Datei als `multipart/form-data` entgegen (Feld `file`),\nlegt sie im Objektspeicher ab, stuft sie ein und schlaegt vor, was\ndamit geschehen soll.\n\nDIESER AUFRUF LEGT NOCH NICHTS AN. `requiresConfirmation` ist fest\n`true`; auch eine sehr sichere Einstufung fuehrt zu keinem Datensatz.\nErst `POST /api/v1/ai/upload/{suggestionId}/confirm` schreibt. Das ist\nAbsicht: ein KI-Upload soll die Freigabe- und Protokollkette nicht\numgehen koennen.\n\n`autoApplyEligible` ist deshalb NUR EIN HINWEIS fuer die Oberflaeche\n(„das koennte man durchwinken\"), keine Ankuendigung. Auch bei `true`\npassiert ohne den zweiten Aufruf nichts.\n\nDIE EINSTUFUNG KANN LEISE SCHEITERN. Sie laeuft mit 25 Sekunden\nZeitgrenze. Laeuft sie ab oder wirft das Werkzeug, antwortet dieser\nEndpunkt trotzdem mit 200 — dann mit `confidence: 0` und einem\nErsatztyp. Der ist je nach Ursache verschieden: `unknown` bei\nZeitueberschreitung, `sonstiges` bei einem Fehler im Werkzeug.\nEin `confidence: 0` heisst „nicht eingestuft\", nicht „sicher nichts\".\n\n`extractedText` gibt es nur fuer PDF mit Textebene sowie TXT und CSV,\ngekuerzt auf 12 000 Zeichen. Gescannte PDFs und Bilder liefern `null`;\ndie Auswertung bricht dann nicht ab.\n\nDer Vorschlag steht als Zuordnungstabelle fest im Quelltext (Belegart\n-> `what`/`how`/`where`), er wird nicht von einem Modell erzeugt.\n\nERLAUBTE TYPEN: PDF, JPEG, PNG, WebP, GIF, XLSX, XLS, DOCX, DOC, TXT,\nCSV. Alles andere 415. Ueber der Groessengrenze 413.\n\nDie Antwort traegt Status 200, nicht 201 — es entsteht ja noch keine\nRessource."}},"/api/v1/ai/upload/{suggestionId}/status":{"get":{"responses":{"200":{"description":"Der Vorschlag. `status` ist immer `done`.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"done"},"classification":{"type":"object","properties":{"type":{"type":"string"},"confidence":{"type":"number"},"candidates":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"confidence":{"type":"number"}},"required":["type","confidence"],"additionalProperties":false}},"fields":{"type":"object","additionalProperties":{"type":["string","null"]}}},"required":["type","confidence","candidates","fields"],"additionalProperties":false},"proposal":{"type":"object","properties":{"what":{"type":"string"},"how":{"type":"string"},"where":{"type":"string"}},"required":["what","how","where"],"additionalProperties":false},"suggestionId":{"type":"string"},"fileName":{"type":"string"}},"required":["status","classification","proposal","suggestionId","fileName"],"additionalProperties":false},"example":{"status":"done","classification":{"type":"string","confidence":0,"candidates":[{"type":"string","confidence":0}],"fields":{"beispiel":"string"}},"proposal":{"what":"string","how":"string","where":"string"},"suggestionId":"string","fileName":"string"}}}},"401":{"description":"Kein Mandantenkontext (`tenant_context_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"403":{"description":"Der Vorschlag gehoert einem anderen Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"forbidden"}},"required":["status"],"additionalProperties":false}}}},"404":{"description":"Unbekannt, abgelaufen, bereits verbraucht — ODER die Abfrage ist gescheitert.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"not_found"}},"required":["status"],"additionalProperties":false}}}}},"operationId":"getApiV1AiUploadBySuggestionIdStatus","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"suggestionId","required":true}],"summary":"Stand eines Upload-Vorschlags (kennt nur „fertig\" und „weg\")","description":"Liefert den gespeicherten Vorschlag zu einer Kennung aus\n`POST /api/v1/ai/upload`.\n\nES GIBT KEINEN LAUFENDEN ZUSTAND. Die Einstufung passiert synchron im\nUpload-Aufruf; wenn ein Vorschlag existiert, ist sie abgeschlossen.\nDiese Operation kann daher nur `done` antworten — oder 404 bzw. 403,\nwenn nichts (mehr) da ist. Wer sie zum Abfragen eines Fortschritts\neinsetzt, fragt etwas ab, das es nicht gibt.\n\nDER 404 IST MEHRDEUTIG. Er tritt auf, wenn die Kennung unbekannt ist,\nwenn der Vorschlag aelter als 24 Stunden ist (`expires_at`), wenn er\nueber `POST /{suggestionId}/confirm` bereits verbraucht wurde — UND\nwenn die Abfrage selbst scheitert: `dbLoadPending` faengt ihren Fehler\nab und liefert eine leere Liste. Ein Datenbankproblem sieht hier also\naus wie „gibt es nicht\".\n\nOhne Datenbank-Client greift ein Rueckfall auf eine Map im Prozess.\nDer traegt nur, solange dieselbe Instanz antwortet — bei mehreren\nInstanzen kann derselbe Vorschlag mal gefunden werden und mal nicht."}},"/api/v1/ai/upload/{suggestionId}/confirm":{"post":{"responses":{"200":{"description":"Verworfen ODER uebernommen. Zwei unterscheidbare Formen.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"cancelled":{"type":"boolean","const":true}},"required":["ok","cancelled"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"resourceId":{"type":"string"},"target":{"type":"object","properties":{"what":{"type":"string"},"how":{"type":"string"},"where":{"type":"string"}},"required":["what","how","where"],"additionalProperties":false}},"required":["ok","resourceId","target"],"additionalProperties":false}]},"example":{"ok":true,"cancelled":true}}}},"400":{"description":"Der Rumpf ist kein gueltiges JSON oder entspricht nicht `{ accepted: boolean, overrides?: { what?, where? } }`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext (`tenant_context_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}},"404":{"description":"Unbekannt, abgelaufen, bereits verbraucht, fremder Mandant — ODER die Abfrage ist gescheitert.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"number"}},"required":["error","status"],"additionalProperties":false}}}}},"operationId":"postApiV1AiUploadBySuggestionIdConfirm","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"suggestionId","required":true}],"summary":"Upload-Vorschlag uebernehmen oder verwerfen (Anlegen ist bestes Bemuehen)","description":"Schliesst den Vorgang aus `POST /api/v1/ai/upload` ab. Der Rumpf\nentscheidet, wie:\n\n  · `accepted: false` — der Vorschlag wird verworfen. Antwort\n    `{ ok: true, cancelled: true }`. Die Datei im Objektspeicher\n    bleibt liegen; entfernt wird nur die Vormerkung.\n  · `accepted: true`  — es entsteht ein Dokument im Mandanten.\n    Antwort `{ ok: true, resourceId, target }`.\n\nZWEI ANTWORTFORMEN, EIN STATUS. `resourceId` gibt es nur im zweiten\nFall, `cancelled` nur im ersten.\n\nDAS ANLEGEN IST BESTES BEMUEHEN — der wichtigste Vorbehalt hier.\nScheitert das Schreiben in die Dokumententabelle, wird der Fehler nur\nins Protokoll geschrieben; die Antwort bleibt `ok: true` und traegt\neine `resourceId`, die auf kein Dokument zeigt. Die Vormerkung wird\nunmittelbar danach geloescht — der Upload ist dann weg, und eine\nWiederholung ist nicht moeglich (die Kennung antwortet ab dann mit\n404). Wer sichergehen muss, prueft das Dokument anschliessend ueber\ndie Dokumenten-Endpunkte nach.\n\n`resourceId` wird VOR dem Schreiben erzeugt und ist deshalb kein\nBeleg dafuer, dass etwas entstanden ist.\n\nMit `overrides.what` und `overrides.where` laesst sich der Vorschlag\nanpassen. ACHTUNG: die Belegart der Einstufung bestimmt weiterhin die\nDokumentart in der Datenbank — `overrides` aendert nur den in `target`\nzurueckgegebenen Vorschlagstext, nicht die Einordnung.\n\nDer Aufruf ist NICHT wiederholbar: nach dem ersten Mal ist die\nVormerkung geloescht."}},"/api/v1/ai-actions/voice-invoice":{"post":{"responses":{"200":{"description":"Vier Gestalten: Rueckfrage, angelegte Rechnung oder Vorschau. needsConfirmation und preview sagen, was wirklich passiert ist.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"needsConfirmation":{"type":"boolean","const":true},"reason":{"type":"string","enum":["customer_ambiguous","missing_fields"]},"intent":{"type":"object","properties":{"customer":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"},"dueDate":{"type":["string","null"]},"description":{"type":["string","null"]},"lineItems":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"}},"required":["name","quantity","unitPrice"]}}},"required":["customer","amount","currency"]},"confidence":{"type":"number"},"candidates":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"company":{"type":["string","null"]},"score":{"type":"number","description":"0 bis 1, je hoeher desto besser die Namensuebereinstimmung"}},"required":["id","name","company","score"]},"description":"Hoechstens fuenf; leer, wenn nur Angaben fehlen"},"missingFields":{"type":"array","items":{"type":"string"},"description":"customer und/oder amount"}},"required":["needsConfirmation","reason","intent","confidence","candidates","missingFields"]},{"type":"object","properties":{"invoice":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"orderId":{"type":["string","null"]},"customerId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"status":{"type":["string","null"]},"positions":{},"subtotal":{"type":["string","null"]},"tax":{"type":["string","null"]},"total":{"type":["string","null"]},"paidAmount":{"type":["string","null"]},"dueDate":{"type":"string"},"paidAt":{"type":["string","null"]},"lockedAt":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["id","number","orderId","customerId","projectId","status","subtotal","tax","total","paidAmount","dueDate","paidAt","lockedAt","createdAt","updatedAt"]},"customer":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"company":{"type":["string","null"]},"type":{"type":["string","null"]},"status":{"type":["string","null"]},"address":{},"tags":{"type":["array","null"],"items":{"type":"string"}},"notes":{"type":["string","null"]},"customFields":{},"defaultCurrency":{"type":["string","null"]}},"required":["id","name","email","phone","company","type","status","tags","notes","defaultCurrency"]},"intent":{"type":"object","properties":{"customer":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"},"dueDate":{"type":["string","null"]},"description":{"type":["string","null"]},"lineItems":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"}},"required":["name","quantity","unitPrice"]}}},"required":["customer","amount","currency"]},"confidence":{"type":"number"},"needsConfirmation":{"type":"boolean","description":"true ab 1000 EUR netto — die Rechnung ist dann trotzdem schon angelegt"},"missingFields":{"type":"array","items":{"type":"string"}}},"required":["invoice","customer","intent","confidence","needsConfirmation","missingFields"]},{"type":"object","properties":{"invoice":{"type":"object","properties":{"id":{"type":"string","description":"Beginnt mit preview_ — keine echte Kennung"},"number":{"type":"string","description":"Endet auf -PREVIEW — keine vergebene Rechnungsnummer"},"customerId":{"type":"string"},"status":{"type":"string","const":"draft"},"subtotal":{"type":"string"},"tax":{"type":"string"},"total":{"type":"string"},"dueDate":{"type":"string"},"paidAmount":{"type":"string","const":"0"},"positions":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"}},"required":["name","quantity","unitPrice"]}}},"required":["id","number","customerId","status","subtotal","tax","total","dueDate","paidAmount","positions"]},"customer":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"company":{"type":["string","null"]},"type":{"type":["string","null"]},"status":{"type":["string","null"]},"address":{},"tags":{"type":["array","null"],"items":{"type":"string"}},"notes":{"type":["string","null"]},"customFields":{},"defaultCurrency":{"type":["string","null"]}},"required":["id","name","email","phone","company","type","status","tags","notes","defaultCurrency"]},"intent":{"type":"object","properties":{"customer":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"},"dueDate":{"type":["string","null"]},"description":{"type":["string","null"]},"lineItems":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"}},"required":["name","quantity","unitPrice"]}}},"required":["customer","amount","currency"]},"confidence":{"type":"number"},"needsConfirmation":{"type":"boolean"},"preview":{"type":"boolean","const":true,"description":"Es wurde nichts gespeichert"},"missingFields":{"type":"array","items":{"type":"string"}}},"required":["invoice","customer","intent","confidence","needsConfirmation","preview","missingFields"]}]},"example":{"needsConfirmation":true,"reason":"customer_ambiguous","intent":{"customer":"string","amount":0,"currency":"string","dueDate":"string","description":"string","lineItems":[{"name":"string","quantity":0,"unitPrice":0}]},"confidence":0,"candidates":[{"id":"string","name":"string","company":"string","score":0}],"missingFields":["string"]}}}},"401":{"description":"Unauthorized"},"402":{"description":"KI-Kontingent des Mandanten erschoepft"},"503":{"description":"Schutzschalter des Anbieters offen — spaeter erneut versuchen"}},"operationId":"postApiV1Ai-actionsVoice-invoice","tags":["ai"],"parameters":[],"description":"Erzeugt aus einem Diktat-Text eine Rechnung. Der Aufruf zaehlt auf das KI-Kontingent des Mandanten; ueber dem Monatslimit antwortet die Route mit 402. Aus dem Text zieht ein Sprachmodell Kunde, Betrag, Faelligkeit und Positionen; faellt es aus, greift eine Notloesung per Textmuster und das verbrauchte Kontingent wird zurueckgebucht, bei geoeffnetem Schutzschalter kommt 503. Der Kunde wird unscharf gesucht: ist die Zuordnung nicht eindeutig oder fehlt der Betrag, kommt 200 mit needsConfirmation=true und Kandidaten — es wird dann NICHTS angelegt, und ein zweiter Aufruf mit confirmCustomerId entscheidet. Sonst entsteht eine Rechnung im Status draft, faellig in 14 Tagen, wenn das Diktat kein Datum nennt. ACHTUNG: ist die Datenbank nicht erreichbar, kommt trotzdem 200 — dann mit preview=true und einer nur gerechneten Rechnung, die nirgends gespeichert ist.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"transcript":{"type":"string","minLength":3,"maxLength":2000},"confirmCustomerId":{"type":"string","format":"uuid"}},"required":["transcript"]},"example":{"transcript":"string","confirmCustomerId":"00000000-0000-4000-8000-000000000000"}}}},"summary":"Erzeugt aus einem Diktat-Text eine Rechnung","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai-usage/check":{"get":{"responses":{"200":{"description":"Ergebnis der Pruefung. Immer 200 — auch wenn nicht gemessen werden konnte.","content":{"application/json":{"schema":{"type":"object","properties":{"overBudget":{"type":"boolean","description":"true: die Monatsgrenze ist hart erreicht."},"reason":{"type":"string","enum":["non_production","no_tenant","unlimited","overage_allowed","meter_degraded","quota_exceeded","within_quota"],"description":"WARUM das Ergebnis so lautet — der eigentliche Inhalt der Antwort."},"limit":{"type":"number","description":"Nur bei `quota_exceeded` und `within_quota` gesetzt."},"current":{"type":"number","description":"Nur bei `quota_exceeded` und `within_quota` gesetzt."}},"required":["overBudget","reason"]},"example":{"overBudget":true,"reason":"non_production","limit":0,"current":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Ai-usageCheck","tags":["KI"],"parameters":[],"summary":"Vor einem KI-Aufruf das Monatsbudget pruefen","description":"Sagt, ob ein weiterer KI-Aufruf die Monatsgrenze des Mandanten\nueberschreiten wuerde. Vorabfrage, keine Sperre: sie zaehlt nichts hoch\nund aendert nichts.\n\n`reason` IST DIE ANTWORT, nicht `overBudget`. Sechs der sieben Gruende\nergeben `overBudget: false`, und sie bedeuten Verschiedenes:\n· `within_quota` — gemessen, es ist Luft. `limit` und `current` dabei.\n· `unlimited` — der Tarif kennt keine Grenze.\n· `overage_allowed` — bezahlter Tarif, Ueberschreitung zulaessig.\n· `non_production` — ausserhalb der Produktivumgebung wird nicht gecappt.\n· `no_tenant` — kein Mandantenkontext, also nichts zu pruefen.\n· `meter_degraded` — DIE ZAEHLUNG SELBST WAR NICHT LESBAR. Kein\n  Freibrief, sondern ein Nichtwissen; die Route sagt es ausdruecklich,\n  statt „alles in Ordnung\" zu behaupten.\n\nDarin liegt der Unterschied zu einer stillen Beruhigung: wer nur\n`overBudget` liest, behandelt eine kaputte Zaehlung wie freie Fahrt. Wer\n`reason` liest, sieht den Unterschied. Das Durchlassen im Zweifel ist\nAbsicht — eine Stoerung der Messung soll den Chat nicht lahmlegen.\n\nNur `quota_exceeded` und `within_quota` tragen Zahlen; sonst fehlen\n`limit` und `current`, weil nichts gemessen wurde.\n\nKeine Rollenpruefung."}},"/api/v1/ai-usage/record":{"post":{"responses":{"200":{"description":"Der Aufruf ist durchgelaufen. `recorded: true` belegt NICHT, dass geschrieben wurde.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"recorded":{"type":"boolean","const":true}},"required":["recorded"],"additionalProperties":false},{"type":"object","properties":{"recorded":{"type":"boolean","const":false},"reason":{"type":"string","const":"no_tenant"}},"required":["recorded","reason"],"additionalProperties":false}]},"example":{"recorded":true}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema. Der `zValidator` laeuft VOR dem Handler — dessen „immer 200\" gilt hier also nicht. Es ist der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Ai-usageRecord","tags":["KI"],"parameters":[],"summary":"Verbrauch nachtraeglich buchen (recorded: true ist kein Schreibbeleg)","description":"Bucht den Token-Verbrauch EINES KI-Aufrufs, den der Web-Teil der\nAnwendung selbst ausgefuehrt hat. Der eigentliche Modellaufruf ist zu\ndiesem Zeitpunkt schon gelaufen — dieser Endpunkt ruft KEIN Modell auf\nund verbraucht selbst kein Kontingent; er schreibt nur die Abrechnung\nnach.\n\nER EXISTIERT, WEIL DER WEB-PFAD SONST UNGEZAEHLT BLIEBE: die KI-Routen im\nNext.js-Prozess kommen weder an die Datenbank noch an die Zaehler heran.\nOhne diesen Rueckruf waere der teuerste Teil des Verbrauchs weder erfasst\nnoch begrenzt.\n\nMANDANT UND BENUTZER STAMMEN AUS DER SITZUNG, nie aus dem Rumpf. Wer\nseine Nutzung einem anderen Mandanten anhaengen will, kommt hier nicht\ndurch.\n\nWAS DAUERHAFT GESCHRIEBEN WIRD — bis zu vier Dinge, alle nicht\nrueckgaengig zu machen:\n\n  · `public.cost_events` und `tenant_ai_token_usage` (Altbestand),\n  · `public.ai_cost_events` — der EINE Ledger, den alle Auswertungen\n    lesen (`/tenant/ai/costs`, `/tenant/ai/audit`); nur, wenn ueberhaupt\n    Token gemeldet wurden,\n  · der Abrechnungspfad von `@nemix/billing-ai`,\n  · bei `countAction: true` der monatliche Aktionszaehler in Redis — der\n    Zaehler, gegen den `GET /api/v1/ai-usage/check` und die Sperre des\n    Chats pruefen.\n\n`countAction` GEHOERT NUR AN DEN ABRECHENBAREN HAUPTZUG. Nebenlaeufige\nKleinaufrufe (Zusammenfassung, Wiederherstellung, Verdichtung) melden\nihre Token, setzen das Kennzeichen aber auf `false`, damit sie nicht je\neine eigene Aktion verbrauchen.\n\n`recorded: true` IST KEIN SCHREIBBELEG. Jeder der vier Wege ist\nbestmoeglich ausgefuehrt und faengt seine Fehler selbst ab: der\nKosten-Ledger protokolliert und macht weiter, der kanonische Ledger laeuft\nnebenlaeufig ohne Rueckmeldung, der Aktionszaehler protokolliert und macht\nweiter. Faellt Redis oder die Datenbank aus, antwortet die Operation\ntrotzdem mit 200 und `recorded: true`, obwohl nichts gebucht wurde. Das\nist Absicht — eine Stoerung der Messung soll den Chat nicht lahmlegen —,\naber die Antwort taugt nicht als Nachweis. Ob wirklich gebucht wurde,\nsagt `GET /api/v1/tenant/ai/audit`.\n\n`recorded: false` mit `reason: \"no_tenant\"` heisst: es gab keinen Mandanten\nzum Zuordnen, es wurde bewusst nichts geschrieben. Auch das ist ein 200.\n\nES GIBT KEINEN SCHUTZ GEGEN DOPPELTES BUCHEN. Derselbe Aufruf zweimal\ngeschickt bucht zweimal — es gibt keine Vorgangskennung und keinen\nAbgleich.\n\nDIESE ROUTE HAENGT ABSICHTLICH NICHT UNTER `/ai/*`: dort liefe die\nKontingent-Middleware mit und wuerde den Zaehler ein zweites Mal\nhochsetzen. Eine Buchung ist kein Modellaufruf.\n\nKeine Rollenpruefung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"toolId":{"type":"string","minLength":1,"maxLength":120},"model":{"type":"string","minLength":1,"maxLength":120},"usage":{"type":["object","null"],"properties":{"inputTokens":{"type":"number","minimum":0},"outputTokens":{"type":"number","minimum":0},"cachedInputTokens":{"type":"number","minimum":0},"cacheCreationInputTokens":{"type":"number","minimum":0},"promptTokens":{"type":"number","minimum":0},"completionTokens":{"type":"number","minimum":0},"input_tokens":{"type":"number","minimum":0},"output_tokens":{"type":"number","minimum":0},"cache_read_input_tokens":{"type":"number","minimum":0},"cache_creation_input_tokens":{"type":"number","minimum":0}},"additionalProperties":true},"providerMetadata":{},"countAction":{"type":"boolean"},"metadata":{"type":"object","additionalProperties":{}}},"required":["toolId","model"]},"example":{"toolId":"string","model":"string","usage":{"inputTokens":0,"outputTokens":0,"cachedInputTokens":0,"cacheCreationInputTokens":0,"promptTokens":0,"completionTokens":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0},"countAction":true,"metadata":{}}}}}}},"/api/v1/ai-brain/event":{"post":{"responses":{"200":{"description":"Der Aufruf ist durchgelaufen. Ob geschrieben wurde, steht in `recorded`, nicht im Status.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"recorded":{"type":"boolean","const":true}},"required":["recorded"],"additionalProperties":false},{"type":"object","properties":{"recorded":{"type":"boolean","const":false},"reason":{"type":"string","enum":["learning_disabled","confirmed_only"]}},"required":["recorded"],"additionalProperties":false}]},"example":{"recorded":true}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — etwa ein `outcome` ausserhalb der fuenf erlaubten Werte. Der `zValidator` laeuft VOR dem Handler, dessen „immer 200\" gilt hier also nicht. Es ist der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Ai-brainEvent","tags":["KI-Gedaechtnis"],"parameters":[],"summary":"Eine KI-Interaktion ins Gedaechtnis schreiben (recorded sagt, ob es geschah)","description":"Haelt EINEN Vorgang der KI fest — welches Werkzeug, welcher Ausgang, ob bestaetigt — als Zeile in `public.ai_memory_event`. Der Aufruf ruft KEIN Modell auf und verbraucht kein Kontingent; er schreibt nur.\n\nER IST ABER DER ANFANG EINER KETTE, DIE KOSTET. Ein naechtlicher Lauf liest die Ereignisse der letzten Tage und laesst ein Sprachmodell daraus deutsche Lernsaetze verdichten, die er in `public.brain_learnings` ablegt. Diese Erkenntnisse fliessen spaeter in die Antworten des Assistenten ein. Was hier gemeldet wird, kann also einen Modellaufruf nach sich ziehen und dauerhaft beeinflussen, was die KI „weiss\".\n\nMANDANT UND BENUTZER STAMMEN AUS DER SITZUNG, nie aus dem Rumpf.\n\nES GIBT KEIN EINZELNES ZURUECKNEHMEN. Diese API kennt kein Loeschen einer Ereigniszeile; rueckgaengig macht das nur `POST /api/v1/ai-brain/reset`, und das loescht das GESAMTE Gedaechtnis des Mandanten.\n\n`recorded` IST DIE EIGENTLICHE ANTWORT, nicht der Status. Vier Ausgaenge unter demselben 200:\n\n· `recorded: true` — die Zeile ist geschrieben.\n· `recorded: false`, `reason: \"learning_disabled\"` — der Mandant hat das   Mitlernen abgeschaltet; bewusst nichts geschrieben.\n· `recorded: false`, `reason: \"confirmed_only\"` — der Mandant nimmt nur   bestaetigte Vorgaenge, und `confirmed` war nicht `true`.\n· `recorded: false` OHNE `reason` — kein Mandant erkannt, kein   Datenbank-Client, ODER das Schreiben ist gescheitert. Diese drei Faelle   sind von aussen NICHT zu unterscheiden; der Fehler wird verschluckt und   nur ins Server-Protokoll geschrieben.\n\nDas Verschlucken ist Absicht: der Erfassungsweg darf den KI-Ablauf, der ihn ausgeloest hat, nicht zum Scheitern bringen. Wer aber `recorded` ignoriert und den Status liest, haelt jeden Ausgang fuer einen Erfolg.\n\nDIE MANDANTENKENNUNG MUSS EINE UUID SEIN. Die Spalte ist `UUID NOT NULL`; ein Kontext mit `tenant_<slug>` faellt in den Zweig „kein Mandant\" und wird still verworfen.\n\n`scope` trennt Mandanten- von persoenlichem Gedaechtnis und ist ohne Angabe `tenant`. `payload` wird ungeprueft als JSONB abgelegt; es gibt keine Zusage ueber die Feldnamen und keine Pruefung auf personenbezogene Inhalte — was hier hineingeschrieben wird, steht spaeter dem naechtlichen Lauf zur Verfuegung.\n\nDIE AUFBEWAHRUNGSFRIST AUS DEN EINSTELLUNGEN WIRKT HIER NICHT: es gibt keinen Lauf, der alte Ereignisse loescht. Die Tabelle waechst, bis `POST /reset` sie leert.\n\nKeine Rollenpruefung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"toolId":{"type":"string","minLength":1,"maxLength":200},"promptSummary":{"type":"string","maxLength":2000},"outcome":{"type":"string","enum":["executed","confirmed","cancelled","undone","failed"]},"confirmed":{"type":"boolean"},"scope":{"type":"string","enum":["tenant","user"]},"payload":{}},"required":["outcome"]},"example":{"toolId":"string","promptSummary":"string","outcome":"executed","confirmed":true,"scope":"tenant"}}}}}},"/api/v1/ai-brain/learnings":{"get":{"responses":{"200":{"description":"Erkenntnisse des Mandanten plus die persoenlichen des Aufrufers. Auch die Antwort ohne erkannten Mandanten — dann leer.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"topic":{"type":["string","null"]},"insight":{"type":"string"},"scope":{"type":"string"},"confidence":{"type":"number"},"sourceCount":{"type":"integer"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","topic","insight","scope","confidence","sourceCount","createdAt","updatedAt"]}}},"required":["data"]},"example":{"data":[{"id":"string","topic":"string","insight":"string","scope":"string","confidence":0,"sourceCount":0,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar (`retryAfter: 5`)."}},"operationId":"getApiV1Ai-brainLearnings","tags":["KI-Gedaechtnis"],"parameters":[],"summary":"Aktive Erkenntnisse des Mandanten","description":"Die aktiven Erkenntnisse, neueste zuerst. Enthaelt die Erkenntnisse mit `scope = \"tenant\"` UND zusaetzlich die persoenlichen des Aufrufers — zwei Mengen in einer Liste, unterscheidbar allein am Feld `scope`. Wer nur die des Mandanten will, filtert selbst.\n\nAbgeschaltete Erkenntnisse (`active = false`) erscheinen NICHT, und es gibt keinen Schalter, sie zu sehen. Nach einem `PATCH /learnings/{id}` ist die Zeile aus dieser Sicht verschwunden.\n\nHarte Grenze bei 200 Zeilen, keine Blaetterung: ab der 201. Erkenntnis ist die Liste unvollstaendig, ohne dass die Antwort das sagt.\n\nOHNE MANDANTENKONTEXT KOMMT 200 MIT LEERER LISTE. Eine leere Liste heisst hier also „keine Erkenntnisse ODER kein Mandant erkannt\". Die schreibenden Routen derselben Datei melden in dem Fall 400."}},"/api/v1/ai-brain/stats":{"get":{"responses":{"200":{"description":"Die Kennzahlen. Auch die Antwort ohne erkannten Mandanten — dann ueberall Null und `lastEventAt: null`.","content":{"application/json":{"schema":{"type":"object","properties":{"eventsTotal":{"type":"integer"},"events30d":{"type":"integer"},"learningsCount":{"type":"integer"},"topTools":{"type":"array","items":{"type":"object","properties":{"toolId":{"type":"string"},"count":{"type":"integer"}},"required":["toolId","count"]}},"lastEventAt":{"type":["string","null"]}},"required":["eventsTotal","events30d","learningsCount","topTools","lastEventAt"]},"example":{"eventsTotal":0,"events30d":0,"learningsCount":0,"topTools":[{"toolId":"string","count":0}],"lastEventAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar (`retryAfter: 5`)."}},"operationId":"getApiV1Ai-brainStats","tags":["KI-Gedaechtnis"],"parameters":[],"summary":"Kennzahlen des KI-Gedaechtnisses","description":"Ereigniszahlen, Zahl der aktiven Erkenntnisse und die zehn am haeufigsten benutzten Werkzeuge.\n\n`events30d` zaehlt die letzten 30 Tage, `eventsTotal` wirklich alles.\n\nKORRIGIERT 30.08.2026: hier stand, `retentionDays` begrenze, wie weit zurueck Ereignisse noch existieren, „Gesamt\" heisse also „seit der letzten Bereinigung\". Das stimmt nicht — es gibt keine Bereinigung. `public.ai_memory_event` ist in ihrer Migration als „append-only log\" angelegt, und kein Job in `apps/api/src/jobs/` loescht nach Frist. `eventsTotal` zaehlt jedes je gemeldete Ereignis, bis `POST /api/v1/ai-brain/reset` die Tabelle leert.\n\n`learningsCount` zaehlt NUR die aktiven. Abgeschaltete Erkenntnisse sind hier unsichtbar, obwohl ihre Zeilen bleiben.\n\n`topTools` ist nach Haeufigkeit sortiert und bei zehn abgeschnitten; Ereignisse ohne Werkzeug fallen heraus. Die Summe der zehn ist deshalb kleiner als `eventsTotal`.\n\nOHNE MANDANTENKONTEXT KOMMEN NULLEN UNTER 200 — nicht zu unterscheiden von einem Mandanten, der noch nichts getan hat."}},"/api/v1/ai-brain/config":{"get":{"responses":{"200":{"description":"Die Einstellungen — ODER die Voreinstellungen, wenn kein Mandant erkannt wurde oder die Datenbank schweigt.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"learningEnabled":{"type":"boolean"},"confirmedOnly":{"type":"boolean"},"retentionDays":{"type":"integer"}},"required":["learningEnabled","confirmedOnly","retentionDays"]}},"required":["data"]},"example":{"data":{"learningEnabled":true,"confirmedOnly":true,"retentionDays":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Ai-brainConfig","tags":["KI-Gedaechtnis"],"parameters":[],"summary":"Einstellungen des KI-Gedaechtnisses lesen","description":"`learningEnabled` schaltet das Mitlernen ein oder aus, `confirmedOnly` beschraenkt es auf bestaetigte Vorgaenge, `retentionDays` ist als Aufbewahrungsfrist gedacht (30 bis 3650 Tage) — WIRD ABER VON NIEMANDEM DURCHGESETZT: kein Lauf loescht alte Ereignisse. Siehe `PUT /api/v1/ai-brain/config`.\n\nDIESE ROUTE ANTWORTET IMMER MIT 200 — auch ohne Mandantenkontext und auch ohne Datenbankverbindung. In beiden Faellen kommen die VOREINSTELLUNGEN zurueck: Lernen an, nicht auf Bestaetigte beschraenkt, 365 Tage. Aus der Antwort ist nicht zu erkennen, ob das die Einstellungen des Mandanten sind oder ob sie nicht gelesen werden konnten. Wer den Unterschied braucht, prueft `/health/ready`.\n\nGespeicherte Werte holt man ueber `PUT /config` zurueck — dessen Antwort ist der wirklich geschriebene Stand."},"put":{"responses":{"200":{"description":"Der neue Stand der Einstellungen, wie er geschrieben wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"learningEnabled":{"type":"boolean"},"confirmedOnly":{"type":"boolean"},"retentionDays":{"type":"integer"}},"required":["learningEnabled","confirmedOnly","retentionDays"]}},"required":["data"]},"example":{"data":{"learningEnabled":true,"confirmedOnly":true,"retentionDays":0}}}}},"400":{"description":"Kein Mandant erkannt (`error: \"tenant_not_resolved\"`) ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"tenant_not_resolved"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin`."},"503":{"description":"ZWEI FORMEN: fehlt der Datenbank-Client, wirft `getClient()` eine HTTPException — die Antwort ist dann `text/plain` mit „database unavailable\", OHNE JSON-Koerper. Scheitert dagegen das Schreiben, kommt JSON mit `error: \"database_unavailable\"` und `retryAfter: 5`. In beiden Faellen wurde nichts gespeichert."}},"operationId":"putApiV1Ai-brainConfig","tags":["KI-Gedaechtnis"],"parameters":[],"summary":"Gedaechtnis-Einstellungen setzen (retentionDays wirkt nirgends)","description":"Schreibt die Einstellungen des Mandanten nach `public.brain_config` — ob mitgelernt wird, ob nur bestaetigte Vorgaenge zaehlen, und die Aufbewahrungsfrist.\n\nDer Aufruf ruft KEIN Modell auf und verbraucht kein Kontingent. Die Wirkung ist dauerhaft und umkehrbar: derselbe Aufruf mit anderen Werten ueberschreibt.\n\nTEILWEISES SETZEN: mitgeschickte Felder werden geschrieben, weggelassene behalten ihren bisherigen Wert (der Handler liest ihn vorher). Ein leerer Rumpf `{}` schreibt den Ist-Zustand zurueck und aendert nichts.\n\n`retentionDays` WIRD VON NIEMANDEM DURCHGESETZT. Der Wert laesst sich setzen und wieder auslesen, aber es gibt keinen Lauf, der alte Zeilen loescht: `public.ai_memory_event` ist in ihrer Migration ausdruecklich als „append-only log\" angelegt, und kein Job in `apps/api/src/jobs/` raeumt sie ab. Wer die Frist auf 30 Tage stellt, hat danach trotzdem alle Ereignisse seit Beginn. Leeren kann sie nur `POST /api/v1/ai-brain/reset`.\n\n`learningEnabled` UND `confirmedOnly` WIRKEN NUR AUF DEN SCHREIBWEG. `POST /api/v1/ai-brain/event` fragt sie ab und verwirft passende Ereignisse. Der naechtliche Lauf, der aus vorhandenen Ereignissen Erkenntnisse verdichtet, liest `brain_config` NICHT: Abschalten stoppt das Sammeln, nicht das Auswerten des bereits Gesammelten. Wer das Verdichten wirklich beenden will, muss zusaetzlich `POST /api/v1/ai-brain/reset` aufrufen.\n\nDie Antwort ist der zusammengesetzte neue Stand — sie kommt aus dem Handler, nicht aus einem `RETURNING`.\n\nDer Wertebereich von `retentionDays` ist 30 bis 3650 Tage; ausserhalb 400.\n\nMindestrolle `admin` — im Unterschied zu `GET /config`, das jeder lesen darf.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"learningEnabled":{"type":"boolean"},"confirmedOnly":{"type":"boolean"},"retentionDays":{"type":"integer","minimum":30,"maximum":3650}}},"example":{"learningEnabled":true,"confirmedOnly":true,"retentionDays":30}}}}}},"/api/v1/ai-brain/learnings/{id}":{"patch":{"responses":{"200":{"description":"Umgeschaltet. `active` sagt, in welchem Zustand die Erkenntnis jetzt ist — `false` heisst, sie erscheint nicht mehr in `/learnings`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"active":{"type":"boolean"}},"required":["ok","active"]},"example":{"ok":true,"active":true}}}},"400":{"description":"Kennung ist keine UUID (`invalid_id`) oder kein Mandant erkannt (`tenant_not_resolved`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"404":{"description":"Nicht gefunden ODER fremder Mandant — `error: \"not_found\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar (`retryAfter: 5`)."}},"operationId":"patchApiV1Ai-brainLearningsById","tags":["KI-Gedaechtnis"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Erkenntnis ab- oder wieder einschalten","description":"Schaltet eine Erkenntnis ab (`{\"active\": false}`) oder wieder ein (`{\"active\": true}`). OHNE Rumpf gilt `false` — so verhalten sich alle bisherigen Aufrufer unveraendert.\n\nGEAENDERT AM 17.08.2026: bis dahin wurde der Rumpf NICHT gelesen. Die Route setzte `active = false`, und zwar immer; ein `{\"active\": true}` wurde stillschweigend ignoriert und schaltete die Erkenntnis trotzdem ab. Es gab damit keinen Weg zurueck: eine versehentlich abgeschaltete Erkenntnis war ueber die API nicht mehr einzuschalten, sie verschwand aus `GET /learnings` und blieb als tote Zeile stehen, bis `POST /reset` oder die Aufbewahrungsfrist sie loeschte.\n\nDie Antwort traegt jetzt `active` zurueck — sie sagt also, in welchem Zustand die Erkenntnis danach ist.\n\nDie Kennung muss eine UUID sein, sonst 400. Der 404 heisst „gibt es nicht ODER gehoert einem anderen Mandanten\"; und weil `WHERE ... RETURNING` bei einer bereits abgeschalteten Zeile trotzdem trifft, meldet ein zweiter Aufruf wieder 200.\n\nMindestrolle `manager`."}},"/api/v1/ai-brain/reset":{"post":{"responses":{"200":{"description":"Beide Loeschbefehle liefen. Nennt KEINE Anzahl und kommt auch dann, wenn es nichts zu loeschen gab.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"400":{"description":"Kein Mandant erkannt — `error: \"tenant_not_resolved\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"tenant_not_resolved"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin`."},"503":{"description":"Datenbankfehler. ACHTUNG: kann NACH dem ersten Loeschvorgang auftreten — dann sind die Erkenntnisse bereits weg."}},"operationId":"postApiV1Ai-brainReset","tags":["KI-Gedaechtnis"],"parameters":[],"summary":"Das KI-Gedaechtnis des Mandanten loeschen","description":"ENDGUELTIG UND OHNE RUECKFRAGE. Loescht ALLE Erkenntnisse und ALLE Ereignisse des Mandanten — auch die abgeschalteten. Es gibt keinen Bestaetigungsschritt, keinen Trockenlauf und keinen Weg zurueck.\n\nDIE EINSTELLUNGEN BLEIBEN. `brain_config` wird nicht angefasst: nach dem Loeschen lernt das Gedaechtnis mit denselben Einstellungen sofort wieder mit. Wer das nicht will, setzt vorher `learningEnabled` auf false.\n\nDIE BEIDEN LOESCHVORGAENGE LAUFEN NICHT IN EINER TRANSAKTION. Schlaegt der zweite fehl, sind die Erkenntnisse weg und die Ereignisse noch da — die Antwort ist dann ein 503, und der Zwischenstand bleibt so stehen. Ein zweiter Aufruf raeumt ihn auf.\n\nDie Antwort nennt keine Zahlen: `ok: true` sagt nicht, wie viel geloescht wurde, und kommt auch bei einem leeren Gedaechtnis.\n\nMindestrolle `admin`."}},"/api/v1/ai-data-ops/reconcile":{"post":{"responses":{"200":{"description":"Die gefundenen Abweichungen und die Zahl der geprueften Belege","content":{"application/json":{"schema":{"type":"object","properties":{"mismatches":{"type":"array","items":{"type":"object","properties":{"docNumber":{"type":"string","description":"Belegnummer des Ausgangsbelegs"},"type":{"type":"string","description":"Art der Abweichung"},"expected":{"anyOf":[{"type":"number"},{"type":"string"}],"description":"Erwarteter Wert aus dem Ausgangsbeleg"},"actual":{"anyOf":[{"type":"number"},{"type":"string"}],"description":"Tatsaechlich gefundener Wert im Folgebeleg"},"deltaEur":{"type":"number","description":"Betragsdifferenz in Euro; fehlt bei nicht-monetaeren Abweichungen"},"severity":{"type":"string","enum":["low","medium","high"],"description":"Einstufung der Abweichung"}},"required":["docNumber","type","expected","actual","severity"]},"description":"Belegketten, die nicht zusammenpassen"},"checkedCount":{"type":"integer","description":"Wie viele Ausgangsbelege im Zeitfenster geprueft wurden"}},"required":["mismatches","checkedCount"],"description":"Ergebnis des Belegabgleichs"},"example":{"mismatches":[{"docNumber":"string","type":"string","expected":0,"actual":0,"deltaEur":0,"severity":"low"}],"checkedCount":0}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsReconcile","tags":["ai-data-ops"],"parameters":[],"summary":"Vergleicht zwei Belegarten und meldet, was nicht zusammenpasst","description":"Vergleicht zwei Belegarten miteinander und meldet, was nicht zusammenpasst — Auftraege gegen Rechnungen, Auftraege gegen Lieferungen oder Angebote gegen Auftraege. AENDERT NICHTS, auch nicht versehentlich: der Adapter liest nur. Der Zeitraum ist ueber `sinceDays` begrenzt (Standard 90), damit die Antwort nicht ins Uferlose waechst.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"string","enum":["orders_vs_invoices","orders_vs_deliveries","quotes_vs_orders"]},"sinceDays":{"type":"integer","exclusiveMinimum":0,"maximum":3650,"default":90},"limit":{"type":"integer","exclusiveMinimum":0,"maximum":500,"default":100}},"required":["scope"]},"example":{"scope":"orders_vs_invoices","sinceDays":1,"limit":1}}}}}},"/api/v1/ai-data-ops/bulk-transition-status":{"post":{"responses":{"200":{"description":"Wie viele Zeilen passten und wie viele geschrieben wurden","content":{"application/json":{"schema":{"type":"object","properties":{"matched":{"type":"integer","description":"Wie viele Zeilen dem Filter entsprachen, innerhalb von `limit`"},"updated":{"type":"integer","description":"Wie viele Zeilen tatsaechlich geschrieben wurden — bei dryRun immer 0"},"dryRun":{"type":"boolean","description":"true, wenn nichts geschrieben wurde"}},"required":["matched","updated","dryRun"],"description":"Ergebnis des Massen-Statuswechsels"},"example":{"matched":0,"updated":0,"dryRun":true}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsBulk-transition-status","tags":["ai-data-ops"],"parameters":[],"summary":"Setzt bei vielen Datensaetzen denselben Statuswechsel","description":"Setzt bei vielen Datensaetzen denselben Statuswechsel — etwa alle Angebote von „offen\" auf „abgelaufen\". `dryRun` ist standardmaessig WAHR: der erste Aufruf sagt nur, WAS geschehen wuerde. Zum Ausfuehren muss `dryRun: false` ausdruecklich mitgeschickt werden. Hoechstens 1000 Datensaetze je Aufruf.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","enum":["orders","quotes","leads","tasks"]},"fromStatus":{"type":"string","minLength":1},"toStatus":{"type":"string","minLength":1},"olderThanDays":{"type":"integer","exclusiveMinimum":0},"limit":{"type":"integer","exclusiveMinimum":0,"maximum":1000,"default":100},"dryRun":{"type":"boolean","default":true}},"required":["entity","fromStatus","toStatus"]},"example":{"entity":"orders","fromStatus":"string","toStatus":"string","olderThanDays":1,"limit":1,"dryRun":true}}}}}},"/api/v1/ai-data-ops/merge-duplicates":{"post":{"responses":{"200":{"description":"Zusammengefuehrte Kennungen, umgehaengte Verweise und uebersprungene Tabellen","content":{"application/json":{"schema":{"type":"object","properties":{"mergedIds":{"type":"array","items":{"type":"string"},"description":"Kennungen der Dubletten, die weich geloescht wurden — bei dryRun die, die es wuerden"},"repointed":{"type":"object","additionalProperties":{"type":"integer"},"description":"Je Verweistabelle die Anzahl der auf den Hauptdatensatz umgehaengten Zeilen"},"skippedRefs":{"type":"array","items":{"type":"string"},"description":"Verweistabellen oder Spalten, die im Mandantenschema fehlten und deshalb uebersprungen wurden"},"dryRun":{"type":"boolean","description":"true, wenn nichts geschrieben wurde"}},"required":["mergedIds","repointed","skippedRefs","dryRun"],"description":"Ergebnis der Dublettenzusammenfuehrung"},"example":{"mergedIds":["string"],"repointed":{"beispiel":0},"skippedRefs":["string"],"dryRun":true}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsMerge-duplicates","tags":["ai-data-ops"],"parameters":[],"description":"Fuehrt erkannte Dubletten zusammen. `dryRun` ist standardmaessig WAHR — der erste Aufruf zeigt die geplanten Zusammenfuehrungen, ohne eine auszufuehren. Zusammengefuehrte Datensaetze lassen sich nicht mit demselben Aufruf trennen; der Trockenlauf ist deshalb nicht Hoeflichkeit, sondern der Rueckweg.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","products","vendors"]},"primaryId":{"type":"string","minLength":1},"duplicateIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"dryRun":{"type":"boolean","default":true}},"required":["entity","primaryId","duplicateIds"]},"example":{"entity":"customers","primaryId":"string","duplicateIds":["string"],"dryRun":true}}}},"summary":"Fuehrt erkannte Dubletten zusammen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai-data-ops/bulk-delete-contacts":{"post":{"responses":{"200":{"description":"Geloeschte und uebersprungene Kontakte","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"array","items":{"type":"string"},"description":"Kennungen der geloeschten Kontakte"},"skipped":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des uebersprungenen Kontakts"},"reason":{"type":"string","description":"Warum er stehen blieb — etwa verknuepfte Belege"}},"required":["id","reason"]},"description":"Kontakte, die nicht geloescht wurden"}},"required":["deleted","skipped"],"description":"Ergebnis des Loeschlaufs"},"example":{"deleted":["string"],"skipped":[{"id":"string","reason":"string"}]}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsBulk-delete-contacts","tags":["ai-data-ops"],"parameters":[],"description":"Loescht Kontakte anhand ihrer Kennungen. ACHTUNG: hier gibt es KEINEN Trockenlauf, anders als bei merge und bulk-transition — der Aufruf loescht sofort, bis zu 500 Kontakte. Wer vorher wissen will, was verschwindet, muss die Kennungen selbst nachschlagen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":500}},"required":["ids"]},"example":{"ids":["string"]}}}},"summary":"Loescht Kontakte anhand ihrer Kennungen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai-data-ops/imports/start":{"post":{"responses":{"200":{"description":"Kennung des Laufs samt Adresse zum Hochladen","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string","description":"Kennung des Importlaufs — sie geht in alle folgenden Schritte ein"},"uploadUrl":{"type":"string","description":"Adresse, unter der die Datei abzulegen ist"},"storageKey":{"type":"string","description":"Speicherschluessel, unter dem die Datei erwartet wird"}},"required":["jobId","uploadUrl","storageKey"],"description":"Der begonnene Importlauf"},"example":{"jobId":"string","uploadUrl":"string","storageKey":"string"}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsImportsStart","tags":["ai-data-ops"],"parameters":[],"summary":"Beginnt einen Datenimport und liefert die Adresse zum Hochladen","description":"Beginnt einen Datenimport und gibt eine `uploadUrl` zurueck, unter der die Datei abzulegen ist. Der Chat kann diesen Schritt ausloesen, die Datei aber NICHT selbst hochladen — das muss der Anwender tun, sonst warten die folgenden Schritte auf etwas, das nie ankommt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","products","contacts"]},"filename":{"type":"string","minLength":1},"contentType":{"type":"string","minLength":1}},"required":["entity","filename","contentType"]},"example":{"entity":"customers","filename":"string","contentType":"string"}}}}}},"/api/v1/ai-data-ops/imports/preview":{"post":{"responses":{"200":{"description":"Kopfzeile, Zeilenzahl und die ersten Zeilen","content":{"application/json":{"schema":{"type":"object","properties":{"headers":{"type":"array","items":{"type":"string"},"description":"Die gelesene Kopfzeile der Datei"},"rowCount":{"type":"integer","description":"Anzahl der Datenzeilen in der Datei"},"preview":{"type":"array","items":{"type":"array","items":{"type":"string"}},"description":"Die ersten Zeilen, so wie der Import sie liest"},"entity":{"type":"string","enum":["customers","orders","products","contacts"],"description":"Datenart, die uebernommen werden soll"}},"required":["headers","rowCount","preview","entity"],"description":"Leseprobe der hochgeladenen Datei"},"example":{"headers":["string"],"rowCount":0,"preview":[["string"]],"entity":"customers"}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsImportsPreview","tags":["ai-data-ops"],"parameters":[],"description":"Zeigt die ersten Zeilen der hochgeladenen Datei, wie der Import sie liest. Schreibt nichts. Der Schritt existiert, damit ein Zahlen- oder Trennzeichenfehler auffaellt, bevor tausend Zeilen falsch ankommen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string","minLength":1}},"required":["jobId"]},"example":{"jobId":"string"}}}},"summary":"Zeigt die ersten Zeilen der hochgeladenen Datei, wie der Import sie liest","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai-data-ops/imports/infer-mapping":{"post":{"responses":{"200":{"description":"Der Zuordnungsvorschlag samt Auffaelligkeiten","content":{"application/json":{"schema":{"type":"object","properties":{"mapping":{"type":"object","additionalProperties":{"type":["string","null"]},"description":"Vorschlag Dateispalte → Zielfeld; null heisst „diese Spalte nicht uebernehmen\""},"confidence":{"type":"number","description":"Wie sicher der Vorschlag ist"},"issues":{"type":"array","items":{"type":"string"},"description":"Auffaelligkeiten, die vor dem Uebernehmen zu pruefen sind"}},"required":["mapping","confidence","issues"],"description":"Vorgeschlagene Spaltenzuordnung"},"example":{"mapping":{"beispiel":"string"},"confidence":0,"issues":["string"]}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsImportsInfer-mapping","tags":["ai-data-ops"],"parameters":[],"description":"Schlaegt vor, welche Spalte der Datei auf welches Feld gehoert. Ein VORSCHLAG, keine Festlegung — der Aufrufer bestaetigt oder aendert ihn vor dem Uebernehmen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string","minLength":1},"entity":{"type":"string","enum":["customers","orders","products","contacts"]}},"required":["jobId"]},"example":{"jobId":"string","entity":"customers"}}}},"summary":"Schlaegt vor, welche Spalte der Datei auf welches Feld gehoert","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai-data-ops/imports/commit":{"post":{"responses":{"200":{"description":"Ergebnis der Uebernahme — Erfolg wie Misserfolg, unterschieden ueber `ok`","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Zeilen wurden uebernommen"},"imported":{"type":"integer","description":"Anzahl der uebernommenen Zeilen"},"durationMs":{"type":"number","description":"Dauer der Uebernahme in Millisekunden"}},"required":["ok","imported","durationMs"]},{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Die Uebernahme ist gescheitert"},"error":{"type":"string","description":"Grund des Abbruchs"},"failedRowIndex":{"type":"integer","description":"Zeile, an der es scheiterte; fehlt wenn nicht zuordenbar"}},"required":["ok","error"]}],"description":"Ergebnis der Uebernahme — der Misserfolg kommt ebenfalls als 200, unterschieden ueber `ok`"},"example":{"ok":true,"imported":0,"durationMs":0}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsImportsCommit","tags":["ai-data-ops"],"parameters":[],"description":"Uebernimmt die Zeilen des Imports in die Datenbank. Das ist der Schritt, der schreibt; die drei davor tun es nicht. Was hier ankommt, ist danach normaler Datenbestand und nicht als Import erkennbar.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string","minLength":1},"mapping":{"type":"object","additionalProperties":{"type":["string","null"]}}},"required":["jobId","mapping"]},"example":{"jobId":"string","mapping":{"beispiel":"string"}}}}},"summary":"Uebernimmt die Zeilen des Imports in die Datenbank","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai-data-ops/documents/get-by-id":{"post":{"responses":{"200":{"description":"Die Kopfdaten des Belegs oder null","content":{"application/json":{"schema":{"type":["object","null"],"properties":{"id":{"type":"string","description":"Kennung des Belegs"},"name":{"type":["string","null"],"description":"Dateiname; null wenn keiner erfasst"},"storageKey":{"type":"string","description":"Speicherschluessel fuer `/documents/load-binary`"},"mimeType":{"type":["string","null"],"description":"Dateityp; null wenn unbekannt"}},"required":["id","name","storageKey","mimeType"],"description":"Kopfdaten des Belegs; null, wenn kein nicht-geloeschter Beleg zu dieser Kennung gehoert"},"example":{"id":"string","name":"string","storageKey":"string","mimeType":"string"}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsDocumentsGet-by-id","tags":["ai-data-ops"],"parameters":[],"description":"Liest die Kopfdaten eines Belegs. Nicht die Datei selbst — dafuer gibt es `/documents/load-binary`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1}},"required":["id"]},"example":{"id":"string"}}}},"summary":"Liest die Kopfdaten eines Belegs","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai-data-ops/documents/load-binary":{"post":{"responses":{"200":{"description":"Der Dateiinhalt Base64-kodiert","content":{"application/json":{"schema":{"type":"object","properties":{"base64":{"type":"string","description":"Dateiinhalt Base64-kodiert"}},"required":["base64"],"description":"Der Dateiinhalt"},"example":{"base64":"string"}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsDocumentsLoad-binary","tags":["ai-data-ops"],"parameters":[],"description":"Holt den Dateiinhalt zu einem Speicherschluessel. Getrennt von `get-by-id`, weil eine Antwort mit eingebetteter Datei bei jedem Kopfdaten-Abruf unnoetig Bandbreite kostete.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"storageKey":{"type":"string","minLength":1}},"required":["storageKey"]},"example":{"storageKey":"string"}}}},"summary":"Holt den Dateiinhalt zu einem Speicherschluessel","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai-data-ops/documents/save-zugferd":{"post":{"responses":{"200":{"description":"Bestaetigung, dass die Felder geschrieben wurden","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des geschriebenen Belegs"},"updated":{"type":"boolean","const":true,"description":"Der Schreibvorgang lief durch"}},"required":["id","updated"],"description":"Bestaetigung des Schreibvorgangs"},"example":{"id":"string","updated":true}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsDocumentsSave-zugferd","tags":["ai-data-ops"],"parameters":[],"description":"Speichert die aus einer ZUGFeRD-Rechnung gelesenen Felder am Beleg. Der Adapter uebernimmt, was im XML stand; er rechnet nichts nach.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1},"patch":{"type":"object","properties":{"zugferdData":{},"zugferdFormat":{"type":["string","null"]},"supplierId":{"type":["string","null"]},"supplierMatchConfidence":{"type":["number","null"]},"extractedTotalGross":{"type":["number","null"]},"extractedInvoiceNumber":{"type":["string","null"]},"extractedIssueDate":{"type":["string","null"]}},"required":["zugferdFormat","supplierId","supplierMatchConfidence","extractedTotalGross","extractedInvoiceNumber","extractedIssueDate"]}},"required":["id","patch"]},"example":{"id":"string","patch":{"zugferdFormat":"string","supplierId":"string","supplierMatchConfidence":0,"extractedTotalGross":0,"extractedInvoiceNumber":"string","extractedIssueDate":"string"}}}}},"summary":"Speichert die aus einer ZUGFeRD-Rechnung gelesenen Felder am Beleg","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai-data-ops/documents/match-supplier":{"post":{"responses":{"200":{"description":"Der gefundene Lieferant oder null","content":{"application/json":{"schema":{"type":["object","null"],"properties":{"id":{"type":"string","description":"Kennung des gefundenen Lieferanten"},"confidence":{"type":"number","description":"1 bei Treffer ueber die Umsatzsteuer-Kennung, 0.6 bei Namensaehnlichkeit"}},"required":["id","confidence"],"description":"Der gefundene Lieferant; null, wenn keiner passt — das ist der Fall „neuer Lieferant\", kein Fehler"},"example":{"id":"string","confidence":0}}}},"401":{"description":"Kein Mandantenkontext (`no tenant in context`). Es gibt hier kein 403."}},"operationId":"postApiV1Ai-data-opsDocumentsMatch-supplier","tags":["ai-data-ops"],"parameters":[],"description":"Sucht den Lieferanten zu einer Umsatzsteuer-Kennung oder einem Namen. Findet er keinen, ist die Antwort leer — das ist kein Fehler, sondern der Fall „neuer Lieferant\".","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"vatId":{"type":"string"},"name":{"type":"string"}}},"example":{"vatId":"string","name":"string"}}}},"summary":"Sucht den Lieferanten zu einer Umsatzsteuer-Kennung oder einem Namen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/data-builder/plan-field":{"post":{"responses":{"200":{"description":"Der geplante Entwurf samt Herkunft","content":{"application/json":{"schema":{"type":"object","properties":{"draft":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"]},"fieldId":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,62}$"},"fieldLabel":{"type":"string","minLength":1,"maxLength":120},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"]},"required":{"type":"boolean","default":false},"defaultValue":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}},"required":["entity","fieldId","fieldLabel","pgType","required"],"description":"Der geplante Feld-Entwurf — unveraendert an /commit-field weiterreichbar"},"source":{"type":"string","enum":["llm","stub","stub-fallback"],"description":"llm = vom Sprachmodell; stub = Ersatzweg, weil kein Modell eingerichtet ist; stub-fallback = das Modell hat geantwortet, aber fehlerhaft"}},"required":["draft","source"],"description":"Geplantes Feld; nichts wurde geschrieben"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Keine Admin-Rolle"}},"operationId":"postApiV1AiData-builderPlan-field","tags":["ai-data-builder"],"parameters":[],"summary":"Plant ein Custom-Feld aus natürlicher Sprache (LLM, kein Schema-Change)","description":"Laesst ein Sprachmodell aus dem deutschen Freitext einen Feld-Entwurf bauen und gibt ihn zurueck — es wird NICHTS geschrieben, kein ALTER TABLE, kein Registry-Eintrag. Dafuer ist /commit-field da. Ist kein Modell eingerichtet oder scheitert der Aufruf, kommt trotzdem eine 200 mit einem aus dem Text geratenen Ersatz-Entwurf; woran man das erkennt, steht in `source`. Ein echter Modellaufruf wird als KI-Kosten des Mandanten verbucht. Die ganze Route-Gruppe verlangt die Admin-Rolle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"]},"prompt":{"type":"string","minLength":3,"maxLength":2000}},"required":["entity","prompt"]},"example":{"entity":"customers","prompt":"string"}}}}}},"/api/v1/ai/data-builder/commit-field":{"post":{"responses":{"200":{"description":"Das Feld wurde angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Das Feld wurde angelegt"},"field":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"]},"fieldId":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,62}$"},"fieldLabel":{"type":"string","minLength":1,"maxLength":120},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"]},"required":{"type":"boolean","default":false},"defaultValue":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}},"required":["entity","fieldId","fieldLabel","pgType","required"],"description":"Der uebernommene Entwurf, wie er hereinkam"},"result":{"type":"object","additionalProperties":{},"description":"Die unveraenderte Antwort von POST /api/v1/custom-fields — dort steht das erzeugte SQL und die Registry-Zeile"}},"required":["ok","field","result"],"description":"Ergebnis der Feldanlage"},"example":{"ok":true,"field":{"entity":"customers","fieldId":"kundenkarte_nr","fieldLabel":"Kundenkarten-Nummer","pgType":"text","required":false,"options":[]},"result":{"dryRun":false,"sql":"ALTER TABLE \"tenant_musterbau_gmbh\".\"customers\" ADD COLUMN IF NOT EXISTS \"cf_kundenkarte_nr\" text","added":true,"beforeColumns":14,"afterColumns":15,"registry":{"id":"6f2b1c8e-4d3a-4f5b-9c1d-2e3f4a5b6c7d","entity":"customers","field_id":"kundenkarte_nr","field_label":"Kundenkarten-Nummer","pg_type":"text","required":false,"created_at":"2026-03-12T09:15:00.000Z"}}}}}},"400":{"description":"Validierungsfehler oder gescheiterte Anlage (`ok: false` mit `error`)"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Keine Admin-Rolle"}},"operationId":"postApiV1AiData-builderCommit-field","tags":["ai-data-builder"],"parameters":[],"summary":"Legt das geplante Custom-Feld dauerhaft an (proxy auf /custom-fields)","description":"Reicht den Entwurf ueber einen internen HTTP-Aufruf an POST /api/v1/custom-fields weiter und uebernimmt damit dessen Pruefungen, Mandantentrennung und Registry-Schreibung — das Sitzungs-Plaetzchen des Aufrufers wird mitgereicht. Weitergegeben werden nur Bereich, Feldname, Anzeigename, Datenbanktyp, Vorbelegung und Pflicht-Kennzeichen: die im Entwurf geplanten AUSWAHLWERTE (`options`) gehen dabei verloren und muessen spaeter ueber PATCH /custom-fields/{entity}/{fieldId}/meta nachgetragen werden. Scheitert der weitergereichte Aufruf, antwortet dieser Endpunkt IMMER 400 mit `ok: false` — der urspruengliche Statuscode geht verloren und steht nur noch als Text in `error`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"]},"fieldId":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,62}$"},"fieldLabel":{"type":"string","minLength":1,"maxLength":120},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"]},"required":{"type":"boolean","default":false},"defaultValue":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}},"required":["entity","fieldId","fieldLabel","pgType"]}}}}}},"/api/v1/ai/data-builder/plan-table":{"post":{"responses":{"200":{"description":"Der geplante Entwurf samt Herkunft","content":{"application/json":{"schema":{"type":"object","properties":{"draft":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$"},"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":2000},"icon":{"type":"string","maxLength":120},"fields":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$"},"label":{"type":"string","minLength":1},"fieldType":{"type":"string","enum":["text","number","date","boolean","select","longtext"]},"required":{"type":"boolean","default":false}},"required":["slug","label","fieldType","required"]},"minItems":1}},"required":["slug","name","fields"],"description":"Der geplante Tabellen-Entwurf — unveraendert an /commit-table weiterreichbar"},"source":{"type":"string","enum":["llm","stub","stub-fallback"],"description":"llm = vom Sprachmodell; stub = Ersatzweg, weil kein Modell eingerichtet ist; stub-fallback = das Modell hat geantwortet, aber fehlerhaft"}},"required":["draft","source"],"description":"Geplante Tabelle; nichts wurde geschrieben"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Keine Admin-Rolle"}},"operationId":"postApiV1AiData-builderPlan-table","tags":["ai-data-builder"],"parameters":[],"summary":"Plant eine neue Custom-Tabelle aus natürlicher Sprache","description":"Laesst ein Sprachmodell aus dem deutschen Freitext eine Tabelle mit ihren Feldern entwerfen und gibt den Entwurf zurueck — geschrieben wird NICHTS, dafuer ist /commit-table da. Ist kein Modell eingerichtet oder scheitert der Aufruf, kommt trotzdem eine 200 mit einem geratenen Ersatz-Entwurf; woran man das erkennt, steht in `source`. Ein echter Modellaufruf wird als KI-Kosten des Mandanten verbucht. Die ganze Route-Gruppe verlangt die Admin-Rolle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"prompt":{"type":"string","minLength":3,"maxLength":4000}},"required":["name","prompt"]},"example":{"name":"string","prompt":"string"}}}}}},"/api/v1/ai/data-builder/commit-table":{"post":{"responses":{"200":{"description":"Tabelle und alle Felder angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"false, wenn mindestens ein Feld nicht angelegt werden konnte — dann traegt die Antwort den Status 207"},"entityId":{"type":"string","description":"Kennung der angelegten Tabelle"},"fieldsCreated":{"type":"integer","description":"Wie viele Felder wirklich entstanden sind"},"fieldsRequested":{"type":"integer","description":"Wie viele der Entwurf verlangt hat"},"errors":{"type":"array","items":{"type":"string"},"description":"Je gescheitertem Feld ein Eintrag „slug: HTTP nnn\"; leer bei vollem Erfolg"}},"required":["ok","entityId","fieldsCreated","fieldsRequested","errors"],"description":"Ergebnis der Tabellenanlage — die Tabelle bleibt auch bei Teilfehlern bestehen"},"example":{"ok":true,"entityId":"string","fieldsCreated":0,"fieldsRequested":0,"errors":["string"]}}}},"207":{"description":"Tabelle angelegt, aber nicht alle Felder — Teilerfolg, nichts wird zurueckgenommen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"false, wenn mindestens ein Feld nicht angelegt werden konnte — dann traegt die Antwort den Status 207"},"entityId":{"type":"string","description":"Kennung der angelegten Tabelle"},"fieldsCreated":{"type":"integer","description":"Wie viele Felder wirklich entstanden sind"},"fieldsRequested":{"type":"integer","description":"Wie viele der Entwurf verlangt hat"},"errors":{"type":"array","items":{"type":"string"},"description":"Je gescheitertem Feld ein Eintrag „slug: HTTP nnn\"; leer bei vollem Erfolg"}},"required":["ok","entityId","fieldsCreated","fieldsRequested","errors"],"description":"Ergebnis der Tabellenanlage — die Tabelle bleibt auch bei Teilfehlern bestehen"},"example":{"ok":true,"entityId":"string","fieldsCreated":0,"fieldsRequested":0,"errors":["string"]}}}},"400":{"description":"Validierungsfehler oder gescheiterte Tabellenanlage (`ok: false` mit `error`)"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Keine Admin-Rolle"}},"operationId":"postApiV1AiData-builderCommit-table","tags":["ai-data-builder"],"parameters":[],"summary":"Legt die geplante Custom-Tabelle dauerhaft an","description":"Legt ueber interne HTTP-Aufrufe zuerst die Tabelle und danach jedes Feld einzeln an; das Sitzungs-Plaetzchen des Aufrufers wird mitgereicht. Es gibt KEINE Transaktion ueber beide Schritte. Scheitert nur ein Teil der Felder, bleibt die Tabelle samt der bereits angelegten Felder bestehen und die Antwort traegt den Status 207 mit `ok: false`; welche Felder fehlen, steht in `errors`. Scheitert schon das Anlegen der Tabelle, kommt 400; liefert es keine Kennung zurueck, 500.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$"},"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":2000},"icon":{"type":"string","maxLength":120},"fields":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$"},"label":{"type":"string","minLength":1},"fieldType":{"type":"string","enum":["text","number","date","boolean","select","longtext"]},"required":{"type":"boolean","default":false}},"required":["slug","label","fieldType"]},"minItems":1}},"required":["slug","name","fields"]}}}}}},"/api/v1/ai/data-builder/custom-schema":{"get":{"responses":{"200":{"description":"Eigene Felder und Tabellen des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"fields":{"type":"array","items":{"type":"object","properties":{"entity":{"type":"string","description":"Bereich, an dem das Feld haengt"},"field_id":{"type":"string","description":"Spaltenname des Feldes"},"pg_type":{"type":"string","description":"Datenbanktyp der Spalte"},"required":{"type":["boolean","null"],"description":"Ob das Feld verlangt wird; null bei Altbestand ohne Angabe"},"created_at":{"type":["string","null"],"description":"Anlagezeitpunkt; null wenn nicht erfasst"}},"required":["entity","field_id","pg_type","required","created_at"]},"description":"Eigene Felder des Mandanten, neueste zuerst, hoechstens 500 — roh aus der Registry, daher snake_case"},"tables":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Tabelle"},"slug":{"type":"string","description":"Technischer Name"},"name":{"type":"string","description":"Anzeigename"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine gepflegt ist"},"icon":{"type":["string","null"],"description":"Sinnbild; null wenn keines gewaehlt wurde"},"created_at":{"type":["string","null"],"description":"Anlagezeitpunkt; null wenn nicht erfasst"},"field_count":{"type":["integer","null"],"description":"Anzahl der Felder dieser Tabelle"}},"required":["id","slug","name","description","icon","created_at","field_count"]},"description":"Aktive eigene Tabellen, neueste zuerst, hoechstens 200"},"dbUnavailable":{"type":"boolean","const":true,"description":"Nur gesetzt, wenn gar keine Datenbankverbindung bestand — beide Listen sind dann leer, ohne dass etwas fehlt"}},"required":["fields","tables"],"description":"Alle eigenen Felder und Tabellen des Mandanten"},"example":{"fields":[{"entity":"string","field_id":"string","pg_type":"string","required":true,"created_at":"string"}],"tables":[{"id":"string","slug":"string","name":"string","description":"string","icon":"string","created_at":"string","field_count":0}],"dbUnavailable":true}}}},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Keine Admin-Rolle"}},"operationId":"getApiV1AiData-builderCustom-schema","tags":["ai-data-builder"],"parameters":[],"summary":"Listet alle Custom-Felder + Custom-Tabellen des Tenants (Stage-4-UI)","description":"Liest zwei getrennte Bestaende: die eigenen Felder aus der Registry (hoechstens 500) und die aktiven eigenen Tabellen samt Feldanzahl (hoechstens 200), beide neueste zuerst und ohne Blaetterung. Beide Abfragen sind nachrangig: scheitert eine, bleibt ihre Liste leer und die Antwort ist trotzdem eine 200 — eine leere Liste heisst hier also nicht zwingend „nichts vorhanden\". Fehlt die Datenbankverbindung ganz, ist zusaetzlich `dbUnavailable` gesetzt. Die Feldzeilen kommen roh aus der Datenbank und tragen deshalb snake_case-Namen."}},"/api/v1/ai/rls-builder/plan":{"post":{"responses":{"200":{"description":"Der Entwurf samt Ansichts-SQL. `source` nennt die Herkunft.","content":{"application/json":{"schema":{"type":"object","properties":{"draft":{"type":"object","properties":{"entity":{"type":"string","enum":["contacts","invoices","orders","quotes","products","projects","tickets","immobilien"]},"condition":{"type":"string","minLength":1,"maxLength":500},"roles":{"type":"array","items":{"type":"string","enum":["user","manager","tenant_admin","partner_admin"]},"minItems":1,"maxItems":4},"explanation":{"type":"string","minLength":1,"maxLength":800}},"required":["entity","condition","roles","explanation"]},"source":{"type":"string","description":"`llm` = vom Modell erzeugt · `heuristic` = ohne Modell, aus festen Mustern · `heuristic-fallback` = das Modell wurde gefragt und ist gescheitert"},"previewSql":{"type":"string","description":"Das CREATE-POLICY-Statement zur Ansicht — hier wird es NICHT ausgefuehrt"}},"required":["draft","source","previewSql"]},"example":{"draft":{"entity":"contacts","condition":"string","roles":["user"],"explanation":"string"},"source":"string","previewSql":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1AiRls-builderPlan","tags":["ai-rls-builder"],"parameters":[],"summary":"Zeilen-Sichtbarkeitsregel entwerfen — nur Vorschau, schreibt nichts","description":"Uebersetzt eine Anweisung in natuerlicher Sprache in einen Entwurf fuer eine Zeilen-Sichtbarkeitsregel: Bedingung, betroffene Rollen und eine Erklaerung, dazu das fertige SQL zur Ansicht. Es wird NICHTS geschrieben und nichts ausgefuehrt — dafuer ist `/commit` da. Der Entwurf des Modells wird vor der Auslieferung NOCH EINMAL geprueft: DDL, ein Aushebeln der Mandantentrennung und Verweise auf fremde Schemas sind ausgeschlossen. Ist kein Modell eingerichtet oder scheitert es, entsteht der Entwurf aus festen Mustern — `source` sagt, was davon zutraf. Ist die Anweisung nicht eindeutig, lautet die Bedingung `FALSE`: im Zweifel sperren. Ab Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","enum":["contacts","invoices","orders","quotes","products","projects","tickets","immobilien"]},"prompt":{"type":"string","minLength":3,"maxLength":2000},"roles":{"type":"array","items":{"type":"string","enum":["user","manager","tenant_admin","partner_admin"]},"minItems":1,"maxItems":4}},"required":["entity","prompt","roles"]},"example":{"entity":"contacts","prompt":"string","roles":["user"]}}}}}},"/api/v1/ai/rls-builder/commit":{"post":{"responses":{"200":{"description":"Die Regel ist aktiv. `snapshot_id` ist der Weg zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"snapshot_id":{"type":"string","description":"Der Rueckkehrpunkt, der VOR dem Anwenden geschrieben wurde"},"policy_name":{"type":"string","description":"Serverseitig vergeben, mit Zeitstempel-Suffix"},"entity":{"type":"string"},"explanation":{"type":"string","description":"Der Text aus dem Entwurf, unveraendert zurueckgespiegelt"}},"required":["ok","snapshot_id","policy_name","entity","explanation"]},"example":{"ok":true,"snapshot_id":"string","policy_name":"string","entity":"string","explanation":"string"}}}},"400":{"description":"Eine der drei Sperren griff, oder das Anwenden schlug fehl"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar oder Rueckkehrpunkt nicht schreibbar"}},"operationId":"postApiV1AiRls-builderCommit","tags":["ai-rls-builder"],"parameters":[],"description":"Legt die Regel wirklich an — der Rumpf ist der Entwurf aus `/plan`, unveraendert oder von Hand nachgebessert. Vor dem Schreiben laufen dieselben drei Sperren noch einmal (DDL, Aushebeln der Mandantentrennung, fremde Schemas); ein Verstoss endet mit 400 und ohne Aenderung. Der Name der Regel wird SERVERSEITIG vergeben und traegt einen Zeitstempel — derselbe Entwurf zweimal ergibt also ZWEI Regeln, die beide gelten. Vor dem Anwenden entsteht ein Rueckkehrpunkt; seine Id kommt als `snapshot_id` zurueck. Ab Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","enum":["contacts","invoices","orders","quotes","products","projects","tickets","immobilien"]},"condition":{"type":"string","minLength":1,"maxLength":500},"roles":{"type":"array","items":{"type":"string","enum":["user","manager","tenant_admin","partner_admin"]},"minItems":1,"maxItems":4},"explanation":{"type":"string","minLength":1,"maxLength":800}},"required":["entity","condition","roles","explanation"]},"example":{"entity":"contacts","condition":"string","roles":["user"],"explanation":"string"}}}},"summary":"Legt die Regel wirklich an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/rls-builder/list":{"get":{"responses":{"200":{"description":"Die geltenden Regeln. Steht `dbUnavailable: true` dabei, ist die leere Liste keine Aussage ueber den Bestand.","content":{"application/json":{"schema":{"type":"object","properties":{"policies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Id des Rueckkehrpunkts, nicht der Policy"},"entity":{"type":"string"},"policy_name":{"type":["string","null"],"description":"null, wenn der Eintrag keinen Namen traegt"},"condition":{"type":["string","null"]},"roles":{"type":"array","items":{"type":"string"},"description":"Leer, wenn der Eintrag beschaedigt ist"},"applied_at":{"type":["string","null"]}},"required":["id","entity","policy_name","condition","roles","applied_at"]}},"source":{"type":"string","const":"tenant_rls_snapshots"},"dbUnavailable":{"type":"boolean","const":true,"description":"Gesetzt statt `source`, wenn die Liste NICHT gelesen werden konnte"}},"required":["policies"]},"example":{"policies":[{"id":"string","entity":"string","policy_name":"string","condition":"string","roles":["string"],"applied_at":"string"}],"source":"tenant_rls_snapshots","dbUnavailable":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AiRls-builderList","tags":["ai-rls-builder"],"parameters":[],"description":"Listet die noch geltenden Regeln, die ueber diesen Bereich angelegt wurden — zurueckgenommene sind ausgenommen, zuletzt angewendete zuerst, hoechstens 200. Regeln, die auf anderem Weg in die Datenbank kamen, stehen hier NICHT: die Liste ist die Spur dieses Bereichs, nicht der Stand der Datenbank. ACHTUNG bei der leeren Liste — sie kann auch heissen, dass gar nicht gelesen werden konnte; das unterscheidet `dbUnavailable`. Ab Rolle `admin`.","summary":"Listet die noch geltenden Regeln, die ueber diesen Bereich angelegt wurden","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/email/generate":{"post":{"responses":{"200":{"description":"Entwurf in beiden Sprachen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"subject_de":{"type":"string"},"body_de":{"type":"string"},"subject_en":{"type":"string"},"body_en":{"type":"string"}},"required":["subject_de","body_de","subject_en","body_en"]}},"required":["data"]},"example":{"data":{"subject_de":"string","body_de":"string","subject_en":"string","body_en":"string"}}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"402":{"description":"Quota exceeded"},"403":{"description":"Recht crm.email.write fehlt"}},"operationId":"postApiV1AiEmailGenerate","tags":["ai","CRM"],"parameters":[],"summary":"Erzeugt einen E-Mail-Entwurf (DE + EN) via EmailWriterTool","description":"Liefert JEDEN Entwurf zweisprachig — Betreff und Text auf Deutsch UND Englisch —, aus Anlass (`context`), Tonfall und optionalem Betreff-Hinweis. Mit `customerId` zieht das Werkzeug die Kundendaten des Mandanten hinzu. Es wird NICHTS verschickt und nichts gespeichert: die Antwort ist ein Entwurf, den der Aufrufer selbst weiterverwendet. Der Aufruf verbraucht eine KI-Aktion aus dem Monatskontingent (402 bei Erschoepfung); scheitert das Modell, wird sie zurueckgebucht und die Antwort ist 400 mit dem allgemeinen Code `ai_error` — die Meldung des Anbieters bleibt im Log. Braucht das Recht crm.email.write, sofern fuer den Nutzer ueberhaupt Rechte vergeben sind.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","format":"email"},"context":{"type":"string","enum":["follow-up","cold-outreach","reply","thank-you","meeting-request"]},"tone":{"type":"string","enum":["formal","friendly","direct"],"default":"friendly"},"customerId":{"type":"string","format":"uuid"},"subjectHint":{"type":"string","maxLength":200}},"required":["to","context"]},"example":{"to":"beispiel@example.com","context":"follow-up","tone":"formal","customerId":"00000000-0000-4000-8000-000000000000","subjectHint":"string"}}}}}},"/api/v1/ai/email/summarize":{"post":{"responses":{"200":{"description":"Zusammenfassung, Aufgaben, Antwortvorschlag und Stimmung","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"summary":{"type":"string"},"actionItems":{"type":"array","items":{"type":"string"}},"suggestedReply":{"type":["string","null"]},"sentiment":{"type":"string","enum":["positive","neutral","negative"]}},"required":["summary","actionItems","suggestedReply","sentiment"]}},"required":["data"]},"example":{"data":{"summary":"string","actionItems":["string"],"suggestedReply":"string","sentiment":"positive"}}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"402":{"description":"Quota exceeded"},"403":{"description":"Recht crm.email.read fehlt"}},"operationId":"postApiV1AiEmailSummarize","tags":["ai","CRM"],"parameters":[],"summary":"Fasst eine E-Mail zusammen via EmailSummarizerTool","description":"Nimmt den Nachrichtentext (mindestens 50 Zeichen) entgegen und liefert eine Zusammenfassung, eine Liste der erkannten Aufgaben, einen Antwortvorschlag (kann null sein) und die Stimmung (positive/neutral/negative). Der Text kommt vollstaendig aus dem Rumpf — es wird keine Nachricht aus dem Postfach nachgeladen, nichts gespeichert und nichts verschickt. Der Aufruf verbraucht eine KI-Aktion aus dem Monatskontingent (402 bei Erschoepfung); scheitert das Modell, wird sie zurueckgebucht und die Antwort ist 400 mit dem allgemeinen Code `ai_error`. Braucht das Recht crm.email.read, sofern fuer den Nutzer ueberhaupt Rechte vergeben sind.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"messageBody":{"type":"string","minLength":50},"language":{"type":"string","enum":["de","en"]}},"required":["messageBody"]},"example":{"messageBody":"stringxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","language":"de"}}}}}},"/api/v1/ai/email/reply-suggestions":{"post":{"responses":{"200":{"description":"Antwortvorschläge — in der Regel drei, garantiert ist das nicht","content":{"application/json":{"schema":{"type":"object","properties":{"suggestions":{"type":"array","items":{"type":"object","properties":{"tone":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"}},"required":["tone","subject","body"]}}},"required":["suggestions"]},"example":{"suggestions":[{"tone":"string","subject":"string","body":"string"}]}}}},"401":{"description":"Unauthorized"},"402":{"description":"Quota exceeded"},"403":{"description":"Recht crm.email.write fehlt"},"500":{"description":"AI error"}},"operationId":"postApiV1AiEmailReply-suggestions","tags":["ai","CRM"],"parameters":[],"summary":"Generiert 3 KI-Antwortvorschläge für eine eingehende E-Mail (W24-A)","description":"Erwartet Betreff und Text der eingehenden Mail im Rumpf; `senderName`, `senderEmail` und `context` schaerfen den Vorschlag, `language` waehlt Deutsch (Vorgabe) oder Englisch. Betreff, Text, Absender und Kontext werden als reine DATEN behandelt und in Markierungen eingefasst, damit eine als Anweisung formulierte Mail den Auftrag nicht uebernehmen kann; der Text wird dabei auf 6000 Zeichen gekuerzt. Ist keine KI konfiguriert, kommen drei feste Textbausteine — ohne Modellaufruf, ohne Kontingentverbrauch und ohne Bezug zum Inhalt der Mail. Sonst kostet der Aufruf eine KI-Aktion (402 bei Erschoepfung), die bei einem Fehler des Modells zurueckgebucht wird (500, Code `ai_error`). Liefert das Modell kein auswertbares JSON, kommt EIN Vorschlag mit dem Rohtext statt drei. Es wird nichts gespeichert und nichts verschickt. Braucht das Recht crm.email.write, sofern fuer den Nutzer ueberhaupt Rechte vergeben sind.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"subject":{"type":"string","maxLength":500},"body":{"type":"string","maxLength":8000},"senderName":{"type":"string"},"senderEmail":{"type":"string"},"context":{"type":"string","maxLength":1000},"language":{"type":"string","enum":["de","en"],"default":"de"}},"required":["subject","body"]},"example":{"subject":"string","body":"string","senderName":"string","senderEmail":"string","context":"string","language":"de"}}}}}},"/api/v1/analytics/cashflow-forecast":{"get":{"responses":{"200":{"description":"Prognose. `mock: true` heisst: gerechnet wurde auf ERFUNDENEN Beispieldaten, weil keine Datenbank erreichbar war — die Kurve sieht plausibel aus und bedeutet nichts. `summary` bleibt leer, wenn der KI-Aufruf scheitert; die Zahlen daneben stimmen trotzdem.","content":{"application/json":{"schema":{"type":"object","properties":{"current_balance":{"type":"number"},"forecast":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"expected_income":{"type":"number"},"expected_expense":{"type":"number"},"expected_balance":{"type":"number"},"confidence":{"type":"string","enum":["high","medium","low"]},"risks":{"type":"array","items":{"type":"string"}}},"required":["date","expected_income","expected_expense","expected_balance","confidence","risks"],"additionalProperties":false}},"warnings":{"type":"array","items":{"type":"string"}},"opportunities":{"type":"array","items":{"type":"string"}},"summary":{"type":"string"},"generated_at":{"type":"string"},"days":{"type":"number"},"mock":{"type":"boolean"},"replica_lag_ms":{"type":"number"}},"required":["current_balance","forecast","warnings","opportunities","summary","generated_at","days","mock"],"additionalProperties":false},"example":{"current_balance":0,"forecast":[{"date":"string","expected_income":0,"expected_expense":0,"expected_balance":0,"confidence":"high","risks":["string"]}],"warnings":["string"],"opportunities":["string"],"summary":"string","generated_at":"string","days":0,"mock":true,"replica_lag_ms":0}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsCashflow-forecast","tags":["analytics"],"parameters":[{"in":"query","name":"days","schema":{"type":"integer","minimum":7,"maximum":365,"default":90}},{"in":"query","name":"paymentShiftDays","schema":{"type":"integer","minimum":-30,"maximum":30,"default":0}}],"summary":"Liquiditaetsprognose ueber N Tage, mit Warnungen und Chancen","description":"Liquiditaetsprognose ueber N Tage, mit Warnungen, Chancen und KI-Zusammenfassung."}},"/api/v1/analytics/dashboard/revenue-by-month":{"get":{"responses":{"200":{"description":"Zeilen samt Herkunft. `source: \"unavailable\"` heisst: es wurde gar nicht gelesen, `rows` ist leer, weil keine Datenbank da war — trotzdem 200. Die Antwort ist 60 s zwischengespeichert; eine Rechnungsaenderung verwirft den Eintrag.","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{}},"mock":{"type":"boolean","const":false},"source":{"type":"string","enum":["olap","raw","unavailable"]}},"required":["rows","mock","source"],"additionalProperties":false},"example":{"rows":[],"mock":false,"source":"olap"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsDashboardRevenue-by-month","tags":["analytics"],"parameters":[],"description":"Monatsumsatz fuer die Dashboard-Rohansicht. Antwort 60 s zwischengespeichert.","summary":"Monatsumsatz fuer die Dashboard-Rohansicht","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/dashboard/orders-by-status":{"get":{"responses":{"200":{"description":"Zeilen samt Herkunft. `source: \"unavailable\"` heisst: nicht gelesen, `rows` leer, weil keine Datenbank da war — trotzdem 200.","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{}},"mock":{"type":"boolean","const":false},"source":{"type":"string","enum":["olap","raw","unavailable"]}},"required":["rows","mock","source"],"additionalProperties":false},"example":{"rows":[],"mock":false,"source":"olap"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsDashboardOrders-by-status","tags":["analytics"],"parameters":[],"description":"Auftraege je Status fuer die Dashboard-Rohansicht. Bevorzugt gelesen wird die materialisierte Sicht aus Migration 0018; fehlt sie in dieser Umgebung, faellt der Aufruf auf eine direkte Tabellenabfrage zurueck. Welcher Weg genommen wurde, steht in source (olap oder raw). Anders als /dashboard/revenue-by-month wird hier nichts zwischengespeichert.","summary":"Auftraege je Status fuer die Dashboard-Rohansicht","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/dashboard/customer-cohort":{"get":{"responses":{"200":{"description":"Zeilen samt Herkunft. `source: \"unavailable\"` heisst: nicht gelesen, `rows` leer, weil keine Datenbank da war — trotzdem 200.","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{}},"mock":{"type":"boolean","const":false},"source":{"type":"string","enum":["olap","raw","unavailable"]}},"required":["rows","mock","source"],"additionalProperties":false},"example":{"rows":[],"mock":false,"source":"olap"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsDashboardCustomer-cohort","tags":["analytics"],"parameters":[],"description":"Kundenkohorten fuer die Dashboard-Rohansicht. Bevorzugt gelesen wird die materialisierte Sicht aus Migration 0018; fehlt sie in dieser Umgebung, faellt der Aufruf auf eine direkte Tabellenabfrage zurueck. Welcher Weg genommen wurde, steht in source (olap oder raw). Anders als /dashboard/revenue-by-month wird hier nichts zwischengespeichert.","summary":"Kundenkohorten fuer die Dashboard-Rohansicht","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/dashboard/top-products":{"get":{"responses":{"200":{"description":"Zeilen samt Herkunft. `source: \"unavailable\"` heisst: nicht gelesen, `rows` leer, weil keine Datenbank da war — trotzdem 200.","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{}},"mock":{"type":"boolean","const":false},"source":{"type":"string","enum":["olap","raw","unavailable"]}},"required":["rows","mock","source"],"additionalProperties":false},"example":{"rows":[],"mock":false,"source":"olap"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsDashboardTop-products","tags":["analytics"],"parameters":[],"description":"Meistverkaufte Produkte fuer die Dashboard-Rohansicht. NICHT zu verwechseln mit `/top-products`, das eine fertig formatierte Listen-Kachel liefert.","summary":"Meistverkaufte Produkte fuer die Dashboard-Rohansicht","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/revenue":{"get":{"responses":{"200":{"description":"Datenpunkte fuer eine Diagramm-Kachel. `degraded: true` heisst: die Liste ist leer, weil die Datenquelle nicht erreichbar war — nicht, weil es nichts zu zeigen gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"points":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name","value"],"additionalProperties":false}},"series":{"type":"array","items":{"type":"string"}},"degraded":{"type":"boolean","const":true}},"required":["points","series"],"additionalProperties":false},"example":{"points":[{"name":"string","value":0}],"series":["string"],"degraded":true}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsRevenue","tags":["analytics"],"parameters":[{"in":"query","name":"months","schema":{"type":"integer","minimum":1,"maximum":36,"default":12}}],"description":"Monatsumsatz der letzten N Monate (fakturiert). Summiert wird ueber invoices im Mandantenschema: alle Rechnungen ausser draft und cancelled, geloeschte ausgenommen, gruppiert auf den Monat des Anlagedatums. months nimmt 1 bis 36 an, Vorgabe 12. Monate ohne Rechnung fehlen in der Punktliste. Faellt die Datenbank aus, kommt trotzdem 200 mit leerer Punktliste und degraded=true.","summary":"Monatsumsatz der letzten N Monate (fakturiert)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/top-customers":{"get":{"responses":{"200":{"description":"Eintraege fuer eine Listen-Kachel. `degraded: true` heisst: die Liste ist leer, weil die Datenquelle nicht erreichbar war — nicht, weil es nichts zu zeigen gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"meta":{"type":"string"}},"required":["id","title","subtitle","meta"],"additionalProperties":false}},"degraded":{"type":"boolean","const":true}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","title":"string","subtitle":"string","meta":"string"}],"degraded":true}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsTop-customers","tags":["analytics"],"parameters":[{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":50,"default":5}}],"description":"Top-Kunden nach bezahltem Umsatz. Gezaehlt werden ausschliesslich Rechnungen im Status paid, gruppiert ueber den Kundennamen — Zeilen ohne customer_id fallen auf den in der Rechnung mitgeschriebenen Namen zurueck, sonst auf \"Unbekannt\". limit nimmt 1 bis 50 an, Vorgabe 5. subtitle nennt die Zahl der Rechnungen, meta den Umsatz bereits als deutschen Eurotext. Faellt die Datenbank aus, kommt 200 mit leerer Liste und degraded=true.","summary":"Top-Kunden nach bezahltem Umsatz","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/customers-abc":{"get":{"responses":{"200":{"description":"Eintraege fuer eine Listen-Kachel. `degraded: true` heisst: die Liste ist leer, weil die Datenquelle nicht erreichbar war — nicht, weil es nichts zu zeigen gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"meta":{"type":"string"}},"required":["id","title","subtitle","meta"],"additionalProperties":false}},"degraded":{"type":"boolean","const":true}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","title":"string","subtitle":"string","meta":"string"}],"degraded":true}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsCustomers-abc","tags":["analytics"],"parameters":[{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"description":"ABC-Analyse der Kunden nach bezahltem Umsatz (Pareto). Grundlage sind dieselben bezahlten Rechnungen wie bei /top-customers, absteigend nach Umsatz. Aus dem kumulierten Anteil entsteht die Klasse: bis 80 Prozent A, bis 95 Prozent B, darueber C. Gerechnet wird ueber ALLE Kunden, erst danach schneidet limit (1 bis 100, Vorgabe 20) die Liste ab — die Klassen bleiben dadurch richtig. Klasse und Umsatzanteil stehen im subtitle. Faellt die Datenbank aus, kommt 200 mit leerer Liste und degraded=true.","summary":"ABC-Analyse der Kunden nach bezahltem Umsatz (Pareto)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/top-products":{"get":{"responses":{"200":{"description":"Eintraege fuer eine Listen-Kachel. `degraded: true` heisst: die Liste ist leer, weil die Datenquelle nicht erreichbar war — nicht, weil es nichts zu zeigen gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"meta":{"type":"string"}},"required":["id","title","subtitle","meta"],"additionalProperties":false}},"degraded":{"type":"boolean","const":true}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","title":"string","subtitle":"string","meta":"string"}],"degraded":true}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsTop-products","tags":["analytics"],"parameters":[{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":50,"default":5}}],"description":"Top-Produkte nach Umsatz. Summiert werden die Auftragspositionen zu Auftraegen im Status completed oder delivered, gruppiert ueber die Positionsbezeichnung — nicht ueber einen Artikelstamm; die id ist deshalb nur eine laufende Nummer und keine Artikelkennung. limit nimmt 1 bis 50 an, Vorgabe 5. Fehlt order_items im Mandantenschema, kommt 200 mit leerer Liste und degraded=true statt eines Fehlers. Nicht zu verwechseln mit /dashboard/top-products, das die BI-Rohansicht liefert.","summary":"Top-Produkte nach Umsatz","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/pipeline-funnel":{"get":{"responses":{"200":{"description":"Datenpunkte fuer eine Diagramm-Kachel. `degraded: true` heisst: die Liste ist leer, weil die Datenquelle nicht erreichbar war — nicht, weil es nichts zu zeigen gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"points":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name","value"],"additionalProperties":false}},"series":{"type":"array","items":{"type":"string"}},"degraded":{"type":"boolean","const":true}},"required":["points","series"],"additionalProperties":false},"example":{"points":[{"name":"string","value":0}],"series":["string"],"degraded":true}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsPipeline-funnel","tags":["analytics"],"parameters":[],"description":"Verkaufstrichter: Leads je Stufe. Gezaehlt werden die leads des Mandanten nach status. Die Antwort traegt immer alle sieben Stufen von Neu bis Gewonnen, auch die mit Wert 0, damit die Kachel den ganzen Trichter zeigt; die gewonnenen sind also enthalten. Ein status ausserhalb dieser sieben taucht nicht auf. Faellt die Datenbank aus, kommt 200 mit leerer Punktliste und degraded=true.","summary":"Verkaufstrichter: Leads je Stufe","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/ai-cost-trend":{"get":{"responses":{"200":{"description":"Datenpunkte fuer eine Diagramm-Kachel. `degraded: true` heisst: die Liste ist leer, weil die Datenquelle nicht erreichbar war — nicht, weil es nichts zu zeigen gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"points":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name","value"],"additionalProperties":false}},"series":{"type":"array","items":{"type":"string"}},"degraded":{"type":"boolean","const":true}},"required":["points","series"],"additionalProperties":false},"example":{"points":[{"name":"string","value":0}],"series":["string"],"degraded":true}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnalyticsAi-cost-trend","tags":["analytics"],"parameters":[{"in":"query","name":"days","schema":{"type":"integer","minimum":1,"maximum":365,"default":30}}],"description":"KI-Kosten je Tag (Verlauf). Gelesen wird public.ai_cost_events, eingegrenzt auf die tenant_id des angemeldeten Mandanten, und je Tag die Summe aus cost_usd gebildet — die Werte sind US-Dollar, nicht Euro. days nimmt 1 bis 365 an, Vorgabe 30. Tage ohne Kosten fehlen in der Punktliste. Gibt es die Tabelle in dieser Umgebung noch nicht, kommt 200 mit leerer Punktliste und degraded=true.","summary":"KI-Kosten je Tag (Verlauf)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/webhooks":{"get":{"responses":{"200":{"description":"Liste der Webhooks — oder leer, wenn keine Datenbank da ist","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"secretPreview":{"type":"string"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"retryPolicy":{"type":"object","properties":{"maxAttempts":{"type":"number"},"backoffMs":{"type":"number"}},"required":["maxAttempts","backoffMs"],"additionalProperties":false},"deliveryCount":{"type":"number"},"lastDeliveryAt":{"type":["string","null"]},"failureCount":{"type":"number"},"lastAttemptAt":{"type":["string","null"]},"lastError":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","name","url","events","active","secretPreview","headers","retryPolicy","deliveryCount","lastDeliveryAt","failureCount","lastAttemptAt","lastError","createdAt"],"additionalProperties":false}},"total":{"type":"integer"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","url":"string","events":["string"],"active":true,"secretPreview":"string","headers":{"beispiel":"string"},"retryPolicy":{"maxAttempts":0,"backoffMs":0},"deliveryCount":0,"lastDeliveryAt":"string","failureCount":0,"lastAttemptAt":"string","lastError":"string","createdAt":"string"}],"total":0}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Weder Admin-Rolle noch API-Key mit `webhooks:write`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"getApiV1Webhooks","tags":["webhooks"],"parameters":[],"summary":"Ausgehende Webhooks auflisten","description":"Listet die eingerichteten ausgehenden Webhooks. Ein API-Key sieht NUR die Kanäle, die er selbst angelegt hat; eine Admin-Sitzung sieht alle. Das Signaturgeheimnis kommt nie mit — nur ein kurzer, nicht umkehrbarer Abdruck, und die Werte gesetzter Kopfzeilen sind durch `***` ersetzt. ACHTUNG: Fehlt der Datenbank-Client, antwortet der Aufruf mit einer LEEREN Liste und Status 200 — „nichts eingerichtet\" ist dann von „keine Datenbank\" nicht zu unterscheiden."},"post":{"responses":{"201":{"description":"Angelegt — mit dem Klartext-Geheimnis. ODER, ohne Datenbank, das nicht gespeicherte Ersatzobjekt (andere Form, siehe Beschreibung).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"secretPreview":{"type":"string"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"retryPolicy":{"type":"object","properties":{"maxAttempts":{"type":"number"},"backoffMs":{"type":"number"}},"required":["maxAttempts","backoffMs"],"additionalProperties":false},"deliveryCount":{"type":"number"},"lastDeliveryAt":{"type":["string","null"]},"failureCount":{"type":"number"},"lastAttemptAt":{"type":["string","null"]},"lastError":{"type":["string","null"]},"createdAt":{"type":"string"},"secret":{"type":"string"}},"required":["id","name","url","events","active","secretPreview","headers","retryPolicy","deliveryCount","lastDeliveryAt","failureCount","lastAttemptAt","lastError","createdAt","secret"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"secret":{"type":"string"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"retryPolicy":{"type":"object","properties":{"maxAttempts":{"type":"number"},"backoffMs":{"type":"number"}},"required":["maxAttempts","backoffMs"]},"tenantId":{"type":"string"},"createdAt":{"type":"string"},"deliveryCount":{"type":"number","const":0},"lastDeliveryAt":{"type":"null"}},"required":["id","name","url","events","active","secret","tenantId","createdAt","deliveryCount","lastDeliveryAt"],"additionalProperties":false}]},"example":{"id":"string","name":"string","url":"string","events":["string"],"active":true,"secretPreview":"string","headers":{"beispiel":"string"},"retryPolicy":{"maxAttempts":0,"backoffMs":0},"deliveryCount":0,"lastDeliveryAt":"string","failureCount":0,"lastAttemptAt":"string","lastError":"string","createdAt":"string","secret":"string"}}}},"400":{"description":"Ziel-URL abgelehnt (`invalid_url`) — ODER Schema-Verstoß; letzterer kommt hier als ROHER Validator-Auswurf `{ success: false, error }`, nicht im sonst üblichen gesäuberten Format.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"invalid_url"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Weder Admin-Rolle noch API-Key mit `webhooks:write`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"postApiV1Webhooks","tags":["webhooks"],"parameters":[],"summary":"Ausgehenden Webhook anlegen","description":"Registriert ein Ziel für ausgehende Ereignisse. Die Ziel-URL wird vorher gegen SSRF geprüft (nur HTTPS, keine internen/Loopback-/Metadaten-Adressen). Wird kein `secret` mitgeschickt, wird eines erzeugt; es steht EINMALIG im Klartext in dieser Antwort und ist danach nur noch als Abdruck sichtbar. Gespeichert wird es AES-256-GCM-verschlüsselt. Legt ein API-Key den Kanal an, gehört er ihm — andere Keys sehen und löschen ihn nicht. ACHTUNG: Ohne Datenbank-Client antwortet der Aufruf trotzdem mit 201 und einem vollständig aussehenden Objekt, das NIRGENDS gespeichert wurde — ein anschließendes GET findet es nicht. (Diese Operation war bis hierher mit `200` dokumentiert; sie sendet `201`.)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"},"minItems":1},"active":{"type":"boolean","default":true},"secret":{"type":"string"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"retryPolicy":{"type":"object","properties":{"maxAttempts":{"type":"number","minimum":1,"maximum":10,"default":3},"backoffMs":{"type":"number","default":60000}}}},"required":["name","url","events"]},"example":{"name":"string","url":"https://example.com","events":["string"],"active":true,"secret":"string","headers":{"beispiel":"string"},"retryPolicy":{"maxAttempts":1,"backoffMs":0}}}}}}},"/api/v1/webhooks/diagnose":{"get":{"responses":{"200":{"description":"Diagnose-Bericht. Nur `schemaFromRequest` ist zugesagt: die übrigen Felder hängen davon ab, wie weit der Trockenlauf kommt (ohne Datenbank-Client etwa nur `{ step, ok, detail }`).","content":{"application/json":{"schema":{"type":"object","properties":{"schemaFromRequest":{"type":"string"}},"required":["schemaFromRequest"],"additionalProperties":true},"example":{"schemaFromRequest":"string"}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Weder Admin-Rolle noch API-Key mit `webhooks:write`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"getApiV1WebhooksDiagnose","tags":["webhooks"],"parameters":[],"summary":"Empfängersuche trocken durchspielen","description":"Trockenlauf der Empfängersuche für ein Ereignis (`?event=`, Standard `customer.created`). SENDET NICHTS. Zeigt jede Stufe einzeln: welches Mandanten-Schema aufgelöst wurde, ob die Tabelle existiert, was darin steht und ob der Ereignisvergleich greift. `schemaFromRequest` ist das Schema, das die normalen Routen benutzen — weicht es vom aufgelösten ab, liegt der Fehler in der Mandanten-Auflösung."}},"/api/v1/webhooks/diagnose/replay":{"post":{"responses":{"200":{"description":"Zählwerk des Versands: gefundene Empfänger, erfolgreiche und fehlgeschlagene Zustellungen. `matched: 0` heißt, dass niemand eingetragen ist — kein Fehler.","content":{"application/json":{"schema":{"type":"object","properties":{"eventType":{"type":"string"},"matched":{"type":"integer"},"delivered":{"type":"integer"},"failed":{"type":"integer"}},"required":["eventType","matched","delivered","failed"],"additionalProperties":false},"example":{"eventType":"string","matched":0,"delivered":0,"failed":0}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Weder Admin-Rolle noch API-Key mit `webhooks:write`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"postApiV1WebhooksDiagnoseReplay","tags":["webhooks"],"parameters":[],"summary":"Testereignis wirklich versenden","description":"Löst denselben Versand aus, den auch eine echte Geschäftsaktion auslöst — nur innerhalb dieser Anfrage. VERSENDET WIRKLICH: alle passenden Empfänger bekommen einen echten HTTP-Aufruf mit dem Rumpf `{ diagnose: true, hinweis: … }`. Fehlversuche wandern wie sonst in die Wiederholungs-Warteschlange. Der Ereignisname kommt aus `?event=` (Standard `customer.created`)."}},"/api/v1/webhooks/{id}/deliveries":{"get":{"responses":{"200":{"description":"Fehlversuche, neueste zuerst. `total` ist die Länge DIESER Seite (hart auf 20 begrenzt), nicht die Gesamtzahl.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"eventType":{"type":"string"},"status":{"type":"string"},"attempts":{"type":"number"},"lastError":{"type":["string","null"]},"nextRetryAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","eventType","status","attempts","lastError","nextRetryAt","createdAt"],"additionalProperties":false}},"total":{"type":"integer"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"string","eventType":"string","status":"string","attempts":0,"lastError":"string","nextRetryAt":"string","createdAt":"string"}],"total":0}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Weder Admin-Rolle noch API-Key mit `webhooks:write`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"404":{"description":"Webhook nicht gefunden — oder er gehört einem anderen API-Key. Bewusst nicht 403: sonst wäre aus der Antwort ablesbar, dass es ihn gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Webhook not found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1WebhooksByIdDeliveries","tags":["webhooks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Zustellversuche eines Webhooks","description":"Zeigt die letzten 20 GESCHEITERTEN Zustellungen eines Kanals — erfolgreiche werden nicht protokolliert. Ein API-Key sieht nur seine eigenen Kanäle. ACHTUNG: Eine leere Liste hat drei mögliche Bedeutungen, die die Antwort nicht auseinanderhält — es gab keine Fehlversuche, die Protokolltabelle existiert noch gar nicht (sie entsteht erst mit dem ersten Fehlversuch, der Zugriffsfehler wird verschluckt), oder es fehlt der Datenbank-Client."}},"/api/v1/webhooks/{id}/test":{"post":{"responses":{"200":{"description":"Der Versuch wurde durchgeführt. `delivered: true` mit dem Statuscode der Gegenstelle — ODER `delivered: false` mit dem Fehlertext, ebenfalls unter HTTP 200.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"delivered":{"type":"boolean","const":true},"statusCode":{"type":"integer"},"latencyMs":{"type":"number"}},"required":["delivered","statusCode","latencyMs"],"additionalProperties":false},{"type":"object","properties":{"delivered":{"type":"boolean","const":false},"error":{"type":"string"},"latencyMs":{"type":"number"}},"required":["delivered","error","latencyMs"],"additionalProperties":false}]},"example":{"delivered":true,"statusCode":0,"latencyMs":0}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Weder Admin-Rolle noch API-Key mit `webhooks:write`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"404":{"description":"Webhook nicht gefunden — oder er gehört einem anderen API-Key","content":{"application/json":{"schema":{"type":"object","properties":{"delivered":{"type":"boolean","const":false},"error":{"type":"string"}},"required":["delivered","error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client — der Kanal lässt sich nicht laden","content":{"application/json":{"schema":{"type":"object","properties":{"delivered":{"type":"boolean","const":false},"error":{"type":"string"}},"required":["delivered","error"],"additionalProperties":false}}}}},"operationId":"postApiV1WebhooksByIdTest","tags":["webhooks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Testereignis an einen Webhook senden","description":"Schickt ein signiertes Testereignis (`event: \"test\"`) an die hinterlegte Ziel-URL, mit denselben Kopfzeilen und derselben HMAC-SHA256-Signatur wie im Echtbetrieb, Zeitgrenze 10 Sekunden. WICHTIG FÜR DIE AUSWERTUNG: Ein GESCHEITERTER Versand kommt ebenfalls mit HTTP 200 zurück — maßgeblich ist allein das Feld `delivered`, nicht der Statuscode. Gezählt wird nur der Erfolg: nach einem Fehlversuch bleiben `failureCount`, `lastAttemptAt` und `lastError` des Kanals unverändert, und der Versuch landet auch nicht in der Wiederholungs-Warteschlange."}},"/api/v1/webhooks/{id}":{"delete":{"responses":{"200":{"description":"Quittung — sagt nichts darüber aus, ob wirklich etwas gelöscht wurde","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Weder Admin-Rolle noch API-Key mit `webhooks:write`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"deleteApiV1WebhooksById","tags":["webhooks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ausgehenden Webhook löschen","description":"Entfernt einen ausgehenden Webhook ENDGÜLTIG (kein `deleted_at`, kein Rückweg). Ein API-Key löscht nur seine eigenen Kanäle. BEWUSST IDEMPOTENT: die Antwort ist immer dieselbe Quittung — auch wenn die id unbekannt ist, einem anderen Key gehört oder gar kein Datenbank-Client vorhanden ist (dann wird nichts gelöscht und trotzdem „entfernt\" gemeldet). Grund: Zapier/Make rufen das beim Aufräumen auf, ein Fehler dort meldete nur einen längst entfernten Kanal als Störung. Wer wissen muss, ob der Kanal weg ist, muss anschließend die Liste lesen."}},"/api/v1/webhooks/inbound":{"get":{"responses":{"200":{"description":"Aktive Eingangskanäle — oder leer, wenn keine Datenbank da ist","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"secretPreview":{"type":["string","null"]},"isActive":{"type":"boolean"},"webhookUrl":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","slug","secretPreview","isActive","webhookUrl","createdAt","updatedAt"],"additionalProperties":false}},"total":{"type":"integer"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"string","slug":"string","secretPreview":"string","isActive":true,"webhookUrl":"string","createdAt":"string","updatedAt":"string"}],"total":0}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Keine Admin-Rolle. Kommt aus zwei verschiedenen Prüfungen mit zwei verschiedenen Körpern: dem Gate dieser Datei (Feld `hint`) und `requireMinRole` (Feld `message`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"getApiV1WebhooksInbound","tags":["webhooks"],"parameters":[],"summary":"Eingangskanäle auflisten","description":"Listet die eingerichteten EINGEHENDEN Kanäle (fremde Systeme rufen uns auf). Geheimnisse kommen nie mit, nur der beim Anlegen erzeugte Abdruck. Abgeschaltete Kanäle erscheinen nicht — `isActive` ist deshalb immer `true`. ACHTUNG: Ohne Datenbank-Client kommt eine LEERE Liste mit Status 200, nicht unterscheidbar von „nichts eingerichtet\". Diese Route verlangt echte Admin-Rolle; ein API-Key mit `webhooks:write` kommt hier NICHT durch."},"post":{"responses":{"201":{"description":"Angelegt bzw. Geheimnis ausgetauscht — mit dem einmaligen Klartext","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"secret":{"type":"string"},"secretPreview":{"type":"string"},"webhookUrl":{"type":"string"},"requiredHeaders":{"type":"object","properties":{"X-Nemix-Tenant-Id":{"type":"string"},"X-Nemix-Webhook-Signature":{"type":"string"},"X-Nemix-Webhook-Nonce":{"type":"string"},"X-Nemix-Webhook-Timestamp":{"type":"string"}},"required":["X-Nemix-Tenant-Id","X-Nemix-Webhook-Signature","X-Nemix-Webhook-Nonce","X-Nemix-Webhook-Timestamp"],"additionalProperties":false},"warning":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","slug","secret","secretPreview","webhookUrl","requiredHeaders","warning","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","slug":"string","secret":"string","secretPreview":"string","webhookUrl":"string","requiredHeaders":{"X-Nemix-Tenant-Id":"string","X-Nemix-Webhook-Signature":"string","X-Nemix-Webhook-Nonce":"string","X-Nemix-Webhook-Timestamp":"string"},"warning":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Ungültiger `slug` (Kleinbuchstaben, Ziffern, Bindestrich). Kommt als ROHER Validator-Auswurf `{ success: false, error }`, nicht im gesäuberten Format.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Keine Admin-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client — es wurde nichts angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1WebhooksInbound","tags":["webhooks"],"parameters":[],"summary":"Eingangskanal anlegen oder Geheimnis austauschen","description":"Legt einen Eingangskanal an — ODER TAUSCHT DAS GEHEIMNIS EINES BESTEHENDEN AUS. Es gibt keinen getrennten Rotations-Aufruf: ein zweiter POST auf denselben `slug` überschreibt das alte Geheimnis, und der bisherige Absender wird ab sofort abgewiesen. Ein zuvor abgeschalteter Kanal wird dabei wieder aktiviert. Das Geheimnis steht EINMALIG im Klartext in dieser Antwort und lässt sich nie wieder abrufen; gespeichert wird es AES-256-GCM-verschlüsselt. `requiredHeaders` zeigt die Kopfzeilen, die der Absender künftig mitschicken muss — die Werte darin sind Platzhalter, keine echten. (Diese Operation war bis hierher mit `200` dokumentiert; sie sendet `201`.)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$"}},"required":["slug"]},"example":{"slug":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/webhooks/inbound/{slug}":{"delete":{"responses":{"200":{"description":"Quittung — auch bei unbekanntem `slug`","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht angemeldet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}},"required":["error","code","message","hint","docs"],"additionalProperties":false}}}},"403":{"description":"Keine Admin-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","enum":["INSUFFICIENT_ROLE","INSUFFICIENT_API_KEY_SCOPE"]},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client — es wurde nichts abgeschaltet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1WebhooksInboundBySlug","tags":["webhooks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Eingangskanal abschalten","description":"Schaltet einen Eingangskanal ab. LÖSCHT NICHT: die Zeile bleibt samt verschlüsseltem Geheimnis stehen und wird nur auf `is_active = FALSE` gesetzt — ein späterer POST auf denselben `slug` aktiviert sie wieder (mit neuem Geheimnis). Die Quittung ist eine Konstante: der Handler prüft nicht, ob es den `slug` überhaupt gibt, ein unbekannter Name liefert dieselbe Antwort."}},"/api/v1/integrations":{"get":{"responses":{"200":{"description":"Integrations-Katalog","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"category":{"type":"string"},"description":{"type":"string"},"icon":{"type":"string"},"authType":{"type":"string"},"available":{"type":"boolean"},"requiresPack":{"type":"string"}},"required":["id","name","category","description","icon","authType","available"]}},"categories":{"type":"array","items":{"type":"string"}}},"required":["data","categories"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","category":"string","description":"string","icon":"string","authType":"string","available":true,"requiresPack":"string"}],"categories":["string"]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Integrations","tags":["integrations"],"parameters":[],"summary":"Katalog aller Integrationen, verrechnet mit den aktiven Paketen","description":"Katalog aller Integrationen — available ist bereits mit den aktiven Paketen verrechnet."}},"/api/v1/integrations/status":{"get":{"responses":{"200":{"description":"Verbindungsstatus — available ist die ANZAHL im Katalog, kein Ja/Nein","content":{"application/json":{"schema":{"type":"object","properties":{"connected":{"type":"array","items":{}},"details":{"type":"array","items":{"type":"object","additionalProperties":{}}},"available":{"type":"number"}},"required":["connected","details","available"],"additionalProperties":false},"example":{"connected":[],"details":[{}],"available":0}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1IntegrationsStatus","tags":["integrations"],"parameters":[],"description":"Welche Integrationen verbunden sind. Bei Datenbankausfall leere Listen statt Fehler.","summary":"Welche Integrationen verbunden sind","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/integrations/{id}/connect":{"post":{"responses":{"201":{"description":"Verbunden","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"tenantId":{"type":"string"},"nextSync":{"type":"string"}},"required":["message","tenantId","nextSync"],"additionalProperties":false},"example":{"message":"string","tenantId":"string","nextSync":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"success":{"type":"boolean"}},"required":["error"]}}}}},"operationId":"postApiV1IntegrationsByIdConnect","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Verbindet eine Integration mit dem Mandanten","description":"Nur ab Rolle „admin\". Legt in public.integration_connections eine Zeile je (Mandant, Integration) an oder ueberschreibt sie — ein zweiter Aufruf ERSETZT Zugangsdaten und Konfiguration vollstaendig und setzt den Status zurueck auf „connected\". Der Rumpf wird nicht gegen ein Schema geprueft: `credentials` und `config` werden als freies JSON uebernommen, und die `:id` wird nicht gegen den Katalog geprueft — jede Zeichenkette legt eine Verbindung an. Es wird nichts an die Gegenstelle gesendet und nichts getestet; ob die Zugangsdaten stimmen, zeigt erst die erste Synchronisation. `nextSync` ist nur eine gerechnete Zeit (jetzt + 1 Stunde) und KEIN eingeplanter Lauf."}},"/api/v1/integrations/{id}":{"delete":{"responses":{"200":{"description":"Getrennt","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"success":{"type":"boolean"}},"required":["error"]}}}}},"operationId":"deleteApiV1IntegrationsById","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Trennt eine verbundene Integration","description":"Nur ab Rolle „admin\". Setzt den Status der Verbindung auf „disconnected\" — die Zeile bleibt samt hinterlegten Zugangsdaten und Konfiguration bestehen und wird durch einen erneuten Aufruf von POST /:id/connect wieder scharf geschaltet. Es wird also NICHTS geloescht und nichts widerrufen; bei der Gegenstelle bleibt der Zugang gueltig. Eine unbekannte Integrations-Kennung ist kein Fehler: die Antwort ist auch dann 200."}},"/api/v1/integrations/{id}/sync":{"post":{"responses":{"200":{"description":"Sync eingereiht — jobId zum Nachverfolgen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"jobId":{"type":"string"},"message":{"type":"string"}},"required":["ok","jobId","message"],"additionalProperties":false},"example":{"ok":true,"jobId":"string","message":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1IntegrationsByIdSync","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Stösst eine Synchronisation an. Die Antwort quittiert die Einreihung, nicht das Ergebnis.","summary":"Stösst eine Synchronisation an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/integrations/custom":{"post":{"responses":{"201":{"description":"Konnektor angelegt — die Antwort enthält die gesendete authConfig","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"baseUrl":{"type":"string","format":"uri"},"authType":{"type":"string","enum":["none","api_key","bearer","basic","oauth2"]},"authConfig":{"type":"object","properties":{"apiKeyHeader":{"type":"string"},"apiKeyValue":{"type":"string"},"bearerToken":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"}}},"endpoints":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"method":{"type":"string","enum":["GET","POST","PUT","PATCH","DELETE"]},"path":{"type":"string"},"description":{"type":"string"},"fieldMapping":{"type":"object","additionalProperties":{"type":"string"}}},"required":["id","name","method","path"]}},"syncConfig":{"type":"object","properties":{"enabled":{"type":"boolean"},"interval":{"type":"string","enum":["hourly","daily","weekly","manual"]},"direction":{"type":"string","enum":["inbound","outbound","bidirectional"]}},"required":["enabled","interval","direction"]},"id":{"type":"string"},"tenantId":{"type":"string"},"createdAt":{"type":"string"}},"required":["name","baseUrl","authType","endpoints","id","tenantId","createdAt"]},"example":{"name":"string","baseUrl":"https://example.com","authType":"none","authConfig":{"apiKeyHeader":"string","apiKeyValue":"string","bearerToken":"string","username":"string","password":"string"},"endpoints":[{"id":"string","name":"string","method":"GET","path":"string","description":"string","fieldMapping":{"beispiel":"string"}}],"syncConfig":{"enabled":true,"interval":"hourly","direction":"inbound"},"id":"string","tenantId":"string","createdAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"success":{"type":"boolean"}},"required":["error"]}}}}},"operationId":"postApiV1IntegrationsCustom","tags":["integrations"],"parameters":[],"summary":"Legt einen eigenen REST-Konnektor an","description":"Nur ab Rolle „admin\". Die Kennung vergibt der Server als `custom_<Zeitstempel>`; sie laesst sich nicht vorgeben, und es gibt hier keinen Weg, einen bestehenden Konnektor zu aendern — jeder Aufruf legt einen weiteren an. Beim Speichern werden die Geheimnisse aus `authConfig` (apiKeyValue, bearerToken, password) in die Zugangsdaten-Spalte getrennt und aus der abgelegten Konfiguration entfernt. Die ANTWORT gibt dagegen die gesendete Konfiguration unveraendert zurueck, einschlieszlich dieser Geheimnisse — wer sie protokolliert, protokolliert Zugangsdaten mit. Die Gegenstelle wird nicht kontaktiert; dafuer gibt es POST /custom/:id/test.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"baseUrl":{"type":"string","format":"uri"},"authType":{"type":"string","enum":["none","api_key","bearer","basic","oauth2"]},"authConfig":{"type":"object","properties":{"apiKeyHeader":{"type":"string"},"apiKeyValue":{"type":"string"},"bearerToken":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"}}},"endpoints":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"method":{"type":"string","enum":["GET","POST","PUT","PATCH","DELETE"]},"path":{"type":"string"},"description":{"type":"string"},"fieldMapping":{"type":"object","additionalProperties":{"type":"string"}}},"required":["id","name","method","path"]}},"syncConfig":{"type":"object","properties":{"enabled":{"type":"boolean"},"interval":{"type":"string","enum":["hourly","daily","weekly","manual"]},"direction":{"type":"string","enum":["inbound","outbound","bidirectional"]}},"required":["enabled","interval","direction"]}},"required":["name","baseUrl","authType","endpoints"]},"example":{"name":"string","baseUrl":"https://example.com","authType":"none","authConfig":{"apiKeyHeader":"string","apiKeyValue":"string","bearerToken":"string","username":"string","password":"string"},"endpoints":[{"id":"string","name":"string","method":"GET","path":"string","description":"string","fieldMapping":{"beispiel":"string"}}],"syncConfig":{"enabled":true,"interval":"hourly","direction":"inbound"}}}}}}},"/api/v1/integrations/custom/{id}/test":{"post":{"responses":{"200":{"description":"Testergebnis — auch ein FEHLGESCHLAGENER Test antwortet mit 200 und success:false","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"statusCode":{"type":"number"},"responseTime":{"type":"number"},"error":{"type":"string"}},"required":["success","responseTime"]},"example":{"success":true,"statusCode":0,"responseTime":0,"error":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Konnektor nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"success":{"type":"boolean"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"success":{"type":"boolean"}},"required":["error"]}}}}},"operationId":"postApiV1IntegrationsCustomByIdTest","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Prueft die Erreichbarkeit eines eigenen Konnektors","description":"Prüft die Erreichbarkeit eines eigenen Konnektors (HEAD-Anfrage, 5 s Zeitgrenze)."}},"/api/v1/integrations/datev/export":{"get":{"responses":{"301":{"description":"Umgezogen — location nennt den neuen Pfad","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"location":{"type":"string"}},"required":["error","message","location"],"additionalProperties":false},"example":{"error":"moved","message":"Use POST /api/v1/datev/export instead — this endpoint is deprecated.","location":"/api/v1/datev/export"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1IntegrationsDatevExport","tags":["integrations"],"parameters":[],"summary":"Veraltet — der echte DATEV-Export liegt unter POST /api/v1/datev/export","description":"Exportiert NICHTS. Der Endpunkt antwortet stets 301 mit einem JSON-Rumpf, dessen `location` den aktuellen Pfad nennt; eine Location-Kopfzeile setzt er nicht, ein HTTP-Client folgt also nicht von selbst. Es gibt keinen Erfolgsfall und darum auch kein 2xx-Antwortschema — die einzige Antwortform steht unter 301. Der wirkliche Export ist ein POST mit Zeitraum im Rumpf; auf ihn umstellen."}},"/api/v1/integrations/zugferd/invoice/{id}":{"get":{"responses":{"307":{"description":"Weiterleitung auf den aktuellen Pfad, ohne Rumpf","headers":{"Location":{"description":"Der neue Pfad, die Rechnungs-Kennung eingesetzt","schema":{"type":"string"},"example":"/api/v1/integrations/erechnung/00000000-0000-4000-8000-000000000000/zugferd"}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1IntegrationsZugferdInvoiceById","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Alter Pfad — leitet per 307 auf /erechnung/:invoiceId/zugferd weiter","description":"Erzeugt selbst nichts und liest nichts. Der Aufruf endet in einer 307-Weiterleitung, die Methode und Rumpf erhaelt; die Rechnungs-Kennung wandert unveraendert in den neuen Pfad. Weil nie ein Rumpf entsteht, hat diese Operation absichtlich KEIN 2xx-Antwortschema — was zurueckkommt, beschreibt das Ziel der Weiterleitung. Neue Aufrufer sollten direkt dorthin gehen."}},"/api/v1/integrations/erechnung/{invoiceId}/xrechnung":{"get":{"responses":{"200":{"description":"XRechnung-XML als Download — kein JSON","content":{"application/xml":{"schema":{"type":"string"}}}},"401":{"description":"Unauthorized"},"422":{"description":"E-Rechnung-Prüfung fehlgeschlagen — details nennt Regel und Feld","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"}},"required":["rule","message"]}}},"required":["error","details"],"additionalProperties":false}}}}},"operationId":"getApiV1IntegrationsErechnungByInvoiceIdXrechnung","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"invoiceId","required":true}],"summary":"XRechnung-XML zum Download","description":"Ohne den Parameter `leitwegId` gilt die im Mandanten hinterlegte Leitweg-ID. Der Endpunkt prueft die Rechnung zuerst gegen EN-16931 und liefert bei Verstoeszen 422 statt einer unvollstaendigen Datei. ACHTUNG: findet er die Rechnungs-Kennung nicht (oder ist die Datenbank nicht erreichbar), antwortet er NICHT mit 404, sondern erzeugt das XML aus einem fest hinterlegten Demo-Beleg. Auch bei einer echten Rechnung stammen Verkaeuferangaben und Bankverbindung aus fest hinterlegten Demo-Werten, nicht aus den Mandanten-Stammdaten. Es wird nichts gespeichert und nichts versendet."}},"/api/v1/integrations/erechnung/{invoiceId}/zugferd":{"get":{"responses":{"200":{"description":"PDF — oder XML, wenn pdf-lib fehlt (Kopfzeile X-Invoice-Stub: true)","content":{"application/pdf":{},"application/xml":{"schema":{"type":"string"}}}},"401":{"description":"Unauthorized"},"422":{"description":"E-Rechnung-Prüfung fehlgeschlagen — details nennt Regel und Feld","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"}},"required":["rule","message"]}}},"required":["error","details"],"additionalProperties":false}}}}},"operationId":"getApiV1IntegrationsErechnungByInvoiceIdZugferd","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"invoiceId","required":true}],"summary":"ZUGFeRD-PDF/A-3 mit eingebettetem XML","description":"Fehlt pdf-lib, kommt stattdessen das reine XML — erkennbar an der Kopfzeile X-Invoice-Stub: true und am Inhaltstyp; der Aufrufer muss beide Faelle behandeln. Geprueft wird zuerst gegen EN-16931; bei Verstoeszen 422 statt einer Datei. Wie beim XRechnung-Download gilt: eine unbekannte Rechnungs-Kennung ergibt KEIN 404, sondern einen fest hinterlegten Demo-Beleg, und Verkaeuferangaben samt Bankverbindung stammen auch bei echten Rechnungen aus Demo-Werten. Ein Anhang zum Mandanten wird nicht abgelegt und nichts versendet."}},"/api/v1/integrations/erechnung/{invoiceId}/send":{"post":{"responses":{"200":{"description":"Versand angestossen — attachment nennt Dateiname, Grösse und Typ","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"invoiceNumber":{"type":"string"},"format":{"type":"string"},"profile":{"type":"string"},"attachment":{"type":"object","properties":{"filename":{"type":"string"},"bytes":{"type":"number"},"mime":{"type":"string"}},"required":["filename","bytes","mime"]},"subject":{"type":"string"},"body":{"type":"string"},"tenantId":{"type":"string"},"sentAt":{"type":"string"}},"required":["message","invoiceNumber","format","profile","attachment","subject","body","tenantId","sentAt"],"additionalProperties":false},"example":{"message":"string","invoiceNumber":"string","format":"string","profile":"string","attachment":{"filename":"string","bytes":0,"mime":"string"},"subject":"string","body":"string","tenantId":"string","sentAt":"string"}}}},"401":{"description":"Unauthorized"},"422":{"description":"E-Rechnung-Prüfung fehlgeschlagen — es wurde nichts versendet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"}},"required":["rule","message"]}}},"required":["error","details"],"additionalProperties":false}}}}},"operationId":"postApiV1IntegrationsErechnungByInvoiceIdSend","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"invoiceId","required":true}],"description":"Versendet die E-Rechnung per Mail. Der Versand läuft nebenläufig — die Antwort belegt ihn nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","format":"email"},"format":{"type":"string","enum":["zugferd","xrechnung"],"default":"zugferd"},"subject":{"type":"string"},"body":{"type":"string"},"leitwegId":{"type":"string"}},"required":["to"]},"example":{"to":"beispiel@example.com","format":"zugferd","subject":"string","body":"string","leitwegId":"string"}}}},"summary":"Versendet die E-Rechnung per Mail","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/integrations/erechnung/{invoiceId}/validate":{"get":{"responses":{"200":{"description":"Prüfergebnis — 200 auch bei valid:false, das ist der Befund, kein Fehler","content":{"application/json":{"schema":{"type":"object","properties":{"invoiceNumber":{"type":"string"},"valid":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"}},"required":["rule","message"]}},"warnings":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"}},"required":["rule","message"]}},"profile":{"type":"string","enum":["xrechnung","en16931","basic","minimum","invalid"]}},"required":["invoiceNumber","valid","errors","warnings","profile"],"additionalProperties":false},"example":{"invoiceNumber":"string","valid":true,"errors":[{"rule":"string","message":"string","field":"string"}],"warnings":[{"rule":"string","message":"string","field":"string"}],"profile":"xrechnung"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1IntegrationsErechnungByInvoiceIdValidate","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"invoiceId","required":true}],"summary":"Prüft die Rechnung gegen EN-16931, ohne etwas zu erzeugen","description":"Liefert Befunde statt einer Datei: `valid`, die Liste der Fehler und Warnungen — jeweils mit Regel, Meldung und betroffenem Feld — sowie das erkannte Profil. Ein Verstosz ist KEIN Fehler des Aufrufs: die Antwort ist auch bei valid=false 200, wer nur den HTTP-Status auswertet, uebersieht jeden Mangel. Wie die beiden Download-Endpunkte greift auch dieser auf einen fest hinterlegten Demo-Beleg zurueck, wenn die Rechnungs-Kennung unbekannt ist — ein „gueltig\" beweist dann nichts ueber die eigene Rechnung. Es wird nichts gespeichert."}},"/api/v1/integrations/shopify/oauth/authorize":{"get":{"responses":{"200":{"description":"Die zusammengebaute Autorisierungs-URL. Kein Nachweis, dass der Laden existiert.","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"],"additionalProperties":false},"example":{"url":"string"}}}},"400":{"description":"`shop` fehlt oder ist keine `<laden>.myshopify.com`-Adresse — `error: \"invalid_shop\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_shop"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Keine Sitzung."}},"operationId":"getApiV1IntegrationsShopifyOauthAuthorize","tags":["integrations"],"parameters":[],"summary":"Shopify-Anmelde-URL bauen (leitet NICHT weiter)","description":"Baut die Autorisierungs-URL fuer den OAuth-Ablauf und gibt sie als JSON\nzurueck. Trotz des Namens findet KEINE Weiterleitung statt — es kommt\nkein 302, sondern `{ url }`. Wer den Nutzer dorthin schicken will, muss\ndas selbst tun.\n\n`shop` MUSS die Form `<laden>.myshopify.com` haben (geaendert\n17.08.2026), sonst 400 `invalid_shop`. Bis dahin wurde der Wert ungeprueft\nin den Host geschrieben: `?shop=beliebig.example` ergab eine URL auf diesen\nHost, und ein fehlender Wert eine URL mit dem Host `undefined`. Wer der\nAntwort blind folgt, hatte damit eine offene Weiterleitung."}},"/api/v1/integrations/shopify/webhook/{topic}":{"post":{"responses":{"200":{"description":"Entgegengenommen. Keine Signaturpruefung, keine Verarbeitung, keine Ablage.","content":{"application/json":{"schema":{"type":"object","properties":{"accepted":{"type":"boolean","const":true},"topic":{"type":"string"},"size":{"type":"number"}},"required":["accepted","topic","size"],"additionalProperties":false},"example":{"accepted":true,"topic":"string","size":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1IntegrationsShopifyWebhookByTopic","tags":["integrations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"topic","required":true}],"summary":"Shopify-Webhook entgegennehmen (bestaetigt und verwirft)","description":"Nimmt einen Webhook entgegen und antwortet `{ accepted: true }`.\n\nWAS DABEI NICHT PASSIERT: die Signatur wird NICHT geprueft, und der\nRumpf wird NICHT verarbeitet. Der Handler liest den Text, misst seine\nLaenge und verwirft ihn. `size` ist die Zahl der Zeichen, kein Beleg\ndafuer, dass etwas ankam.\n\nDie HMAC-Pruefung EXISTIERT — im Adapter dieser Datei. Diese Route ruft\nsie nur nicht auf. `accepted: true` heisst deshalb ausschliesslich\n„angenommen und weggeworfen\"; Shopify wertet die 200 als Zustellung und\nliefert nicht erneut.\n\nUND SHOPIFY KOMMT HIER GAR NICHT AN. Der Pfad steht nicht in den\noeffentlichen Ausnahmen, liegt also hinter der Sitzungspruefung. Shopify\nsendet Webhooks mit einem HMAC-Kopf und ohne Sitzungscookie — der Aufruf\nendet im 401, bevor dieser Handler laeuft. Der 200 unten ist erreichbar,\naber nur fuer einen angemeldeten Aufrufer.\n\nDie Route ist damit dreifach folgenlos: nicht erreichbar fuer den\nAbsender, ungeprueft, und ohne Verarbeitung. Sie ist ein Platzhalter.\n\nDer Pfadteil `topic` nimmt Schraegstriche mit auf (`{.+}`), damit\n`products/create` und `orders/create` als EIN Parameter ankommen."}},"/api/v1/integrations/amazon/health":{"get":{"responses":{"200":{"description":"Router montiert. Keine Aussage ueber den Konnektor selbst.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"connector":{"type":"string"}},"required":["ok","connector"],"additionalProperties":false},"example":{"ok":true,"connector":"string"}}}},"401":{"description":"Keine Sitzung."}},"operationId":"getApiV1IntegrationsAmazonHealth","tags":["integrations"],"parameters":[],"summary":"Erreichbarkeit des amazon-Routers (prueft den Konnektor NICHT)","description":"Antwortet mit einer Konstante: `{ ok: true, connector: \"amazon\" }`.\n\nEs wird NICHTS geprueft. Der Handler liest keine Konfiguration, prueft\nkeine Zugangsdaten und ruft die Gegenstelle nicht auf. Ein `ok: true`\nheisst ausschliesslich: dieser Router ist montiert und der Prozess\nantwortet.\n\nWer wissen will, ob der Konnektor wirklich arbeitet, kann sich darauf\nNICHT stuetzen. Eine Ueberwachung auf diese Route meldet auch dann\ngruen, wenn beim Anbieter seit Wochen nichts mehr ankommt."}},"/api/v1/integrations/ebay/health":{"get":{"responses":{"200":{"description":"Router montiert. Keine Aussage ueber den Konnektor selbst.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"connector":{"type":"string"}},"required":["ok","connector"],"additionalProperties":false},"example":{"ok":true,"connector":"string"}}}},"401":{"description":"Keine Sitzung."}},"operationId":"getApiV1IntegrationsEbayHealth","tags":["integrations"],"parameters":[],"summary":"Erreichbarkeit des ebay-Routers (prueft den Konnektor NICHT)","description":"Antwortet mit einer Konstante: `{ ok: true, connector: \"ebay\" }`.\n\nEs wird NICHTS geprueft. Der Handler liest keine Konfiguration, prueft\nkeine Zugangsdaten und ruft die Gegenstelle nicht auf. Ein `ok: true`\nheisst ausschliesslich: dieser Router ist montiert und der Prozess\nantwortet.\n\nWer wissen will, ob der Konnektor wirklich arbeitet, kann sich darauf\nNICHT stuetzen. Eine Ueberwachung auf diese Route meldet auch dann\ngruen, wenn beim Anbieter seit Wochen nichts mehr ankommt."}},"/api/v1/integrations/notifications/slack/test":{"post":{"responses":{"200":{"description":"Die gebaute Block-Kit-Struktur. Nicht gesendet.","content":{"application/json":{"schema":{"type":"object","properties":{"blockKit":{"type":"object","additionalProperties":{}}},"required":["blockKit"],"additionalProperties":false},"example":{"blockKit":{}}}}},"400":{"description":"Der Rumpf ist kein gueltiges JSON."},"401":{"description":"Keine Sitzung."}},"operationId":"postApiV1IntegrationsNotificationsSlackTest","tags":["integrations"],"parameters":[],"summary":"Block-Kit-Aufbau ansehen (sendet NICHTS an Slack)","description":"Baut aus dem Rumpf die Block-Kit-Struktur und gibt sie zurueck. Es wird\nNICHTS an Slack gesendet — kein Webhook aufgerufen, keine Zugangsdaten\ngelesen, keine Nachricht zugestellt.\n\nDer Name legt eine Zustellprobe nahe; die Route ist eine\nFormatvorschau. Wer pruefen will, ob Slack wirklich erreichbar ist,\nkann sich darauf nicht stuetzen.\n\nDer Rumpf wird NICHT validiert, sondern nur per Typzusicherung\nentgegengenommen. Fehlende Felder fallen erst beim Bauen auf."}},"/api/v1/integrations/notifications/teams/test":{"post":{"responses":{"200":{"description":"Die gebaute Adaptive Card. Nicht gesendet.","content":{"application/json":{"schema":{"type":"object","properties":{"adaptiveCard":{"type":"object","additionalProperties":{}}},"required":["adaptiveCard"],"additionalProperties":false},"example":{"adaptiveCard":{}}}}},"400":{"description":"Der Rumpf ist kein gueltiges JSON."},"401":{"description":"Keine Sitzung."}},"operationId":"postApiV1IntegrationsNotificationsTeamsTest","tags":["integrations"],"parameters":[],"summary":"Adaptive-Card-Aufbau ansehen (sendet NICHTS an Teams)","description":"Baut aus dem Rumpf die Adaptive Card (Schema 1.5) und gibt sie zurueck.\nEs wird NICHTS an Teams gesendet — kein Webhook aufgerufen, keine\nNachricht zugestellt.\n\nWie bei der Slack-Schwesterroute legt der Name eine Zustellprobe nahe;\nes ist eine Formatvorschau.\n\nDer Rumpf wird NICHT validiert, sondern nur per Typzusicherung\nentgegengenommen."}},"/api/v1/integrations/notifications/whatsapp/health":{"get":{"responses":{"200":{"description":"Router montiert. Keine Aussage ueber den Konnektor selbst.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"connector":{"type":"string"}},"required":["ok","connector"],"additionalProperties":false},"example":{"ok":true,"connector":"string"}}}},"401":{"description":"Keine Sitzung."}},"operationId":"getApiV1IntegrationsNotificationsWhatsappHealth","tags":["integrations"],"parameters":[],"summary":"Erreichbarkeit des whatsapp-twilio-Routers (prueft den Konnektor NICHT)","description":"Antwortet mit einer Konstante: `{ ok: true, connector: \"whatsapp-twilio\" }`.\n\nEs wird NICHTS geprueft. Der Handler liest keine Konfiguration, prueft\nkeine Zugangsdaten und ruft die Gegenstelle nicht auf. Ein `ok: true`\nheisst ausschliesslich: dieser Router ist montiert und der Prozess\nantwortet.\n\nWer wissen will, ob der Konnektor wirklich arbeitet, kann sich darauf\nNICHT stuetzen. Eine Ueberwachung auf diese Route meldet auch dann\ngruen, wenn beim Anbieter seit Wochen nichts mehr ankommt."}},"/api/v1/integrations/notifications/telegram/health":{"get":{"responses":{"200":{"description":"Router montiert. Keine Aussage ueber den Konnektor selbst.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"connector":{"type":"string"}},"required":["ok","connector"],"additionalProperties":false},"example":{"ok":true,"connector":"string"}}}},"401":{"description":"Keine Sitzung."}},"operationId":"getApiV1IntegrationsNotificationsTelegramHealth","tags":["integrations"],"parameters":[],"summary":"Erreichbarkeit des telegram-bot-Routers (prueft den Konnektor NICHT)","description":"Antwortet mit einer Konstante: `{ ok: true, connector: \"telegram-bot\" }`.\n\nEs wird NICHTS geprueft. Der Handler liest keine Konfiguration, prueft\nkeine Zugangsdaten und ruft die Gegenstelle nicht auf. Ein `ok: true`\nheisst ausschliesslich: dieser Router ist montiert und der Prozess\nantwortet.\n\nWer wissen will, ob der Konnektor wirklich arbeitet, kann sich darauf\nNICHT stuetzen. Eine Ueberwachung auf diese Route meldet auch dann\ngruen, wenn beim Anbieter seit Wochen nichts mehr ankommt."}},"/api/v1/integrations/shipping/dhl/health":{"get":{"responses":{"200":{"description":"Router montiert. Keine Aussage ueber den Konnektor selbst.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"connector":{"type":"string"}},"required":["ok","connector"],"additionalProperties":false},"example":{"ok":true,"connector":"string"}}}},"401":{"description":"Keine Sitzung."}},"operationId":"getApiV1IntegrationsShippingDhlHealth","tags":["integrations"],"parameters":[],"summary":"Erreichbarkeit des dhl-Routers (prueft den Konnektor NICHT)","description":"Antwortet mit einer Konstante: `{ ok: true, connector: \"dhl\" }`.\n\nEs wird NICHTS geprueft. Der Handler liest keine Konfiguration, prueft\nkeine Zugangsdaten und ruft die Gegenstelle nicht auf. Ein `ok: true`\nheisst ausschliesslich: dieser Router ist montiert und der Prozess\nantwortet.\n\nWer wissen will, ob der Konnektor wirklich arbeitet, kann sich darauf\nNICHT stuetzen. Eine Ueberwachung auf diese Route meldet auch dann\ngruen, wenn beim Anbieter seit Wochen nichts mehr ankommt."}},"/api/v1/integrations/shipping/dpd/health":{"get":{"responses":{"200":{"description":"Router montiert. Keine Aussage ueber den Konnektor selbst.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"connector":{"type":"string"}},"required":["ok","connector"],"additionalProperties":false},"example":{"ok":true,"connector":"string"}}}},"401":{"description":"Keine Sitzung."}},"operationId":"getApiV1IntegrationsShippingDpdHealth","tags":["integrations"],"parameters":[],"summary":"Erreichbarkeit des dpd-Routers (prueft den Konnektor NICHT)","description":"Antwortet mit einer Konstante: `{ ok: true, connector: \"dpd\" }`.\n\nEs wird NICHTS geprueft. Der Handler liest keine Konfiguration, prueft\nkeine Zugangsdaten und ruft die Gegenstelle nicht auf. Ein `ok: true`\nheisst ausschliesslich: dieser Router ist montiert und der Prozess\nantwortet.\n\nWer wissen will, ob der Konnektor wirklich arbeitet, kann sich darauf\nNICHT stuetzen. Eine Ueberwachung auf diese Route meldet auch dann\ngruen, wenn beim Anbieter seit Wochen nichts mehr ankommt."}},"/api/v1/integrations/shipping/ups/health":{"get":{"responses":{"200":{"description":"Router montiert. Keine Aussage ueber den Konnektor selbst.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"connector":{"type":"string"}},"required":["ok","connector"],"additionalProperties":false},"example":{"ok":true,"connector":"string"}}}},"401":{"description":"Keine Sitzung."}},"operationId":"getApiV1IntegrationsShippingUpsHealth","tags":["integrations"],"parameters":[],"summary":"Erreichbarkeit des ups-Routers (prueft den Konnektor NICHT)","description":"Antwortet mit einer Konstante: `{ ok: true, connector: \"ups\" }`.\n\nEs wird NICHTS geprueft. Der Handler liest keine Konfiguration, prueft\nkeine Zugangsdaten und ruft die Gegenstelle nicht auf. Ein `ok: true`\nheisst ausschliesslich: dieser Router ist montiert und der Prozess\nantwortet.\n\nWer wissen will, ob der Konnektor wirklich arbeitet, kann sich darauf\nNICHT stuetzen. Eine Ueberwachung auf diese Route meldet auch dann\ngruen, wenn beim Anbieter seit Wochen nichts mehr ankommt."}},"/api/v1/integrations/shipping/hermes/health":{"get":{"responses":{"200":{"description":"Router montiert. Keine Aussage ueber den Konnektor selbst.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"connector":{"type":"string"}},"required":["ok","connector"],"additionalProperties":false},"example":{"ok":true,"connector":"string"}}}},"401":{"description":"Keine Sitzung."}},"operationId":"getApiV1IntegrationsShippingHermesHealth","tags":["integrations"],"parameters":[],"summary":"Erreichbarkeit des hermes-Routers (prueft den Konnektor NICHT)","description":"Antwortet mit einer Konstante: `{ ok: true, connector: \"hermes\" }`.\n\nEs wird NICHTS geprueft. Der Handler liest keine Konfiguration, prueft\nkeine Zugangsdaten und ruft die Gegenstelle nicht auf. Ein `ok: true`\nheisst ausschliesslich: dieser Router ist montiert und der Prozess\nantwortet.\n\nWer wissen will, ob der Konnektor wirklich arbeitet, kann sich darauf\nNICHT stuetzen. Eine Ueberwachung auf diese Route meldet auch dann\ngruen, wenn beim Anbieter seit Wochen nichts mehr ankommt."}},"/api/v1/integrations/_registry":{"get":{"responses":{"200":{"description":"Alle registrierten Adapter","content":{"application/json":{"schema":{"type":"object","properties":{"adapters":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Adapters, z. B. \"shopify\""},"category":{"type":"string","enum":["ecommerce","marketplace","shipping","notifications","accounting","tax"],"description":"Fachliche Einordnung des Connectors"},"authType":{"type":"string","enum":["oauth2","api_key","webhook_url","bot_token","lwa_refresh_token","twilio"],"description":"Anmeldeverfahren, das der Connector erwartet"}},"required":["id","category","authType"]}}},"required":["adapters"]},"example":{"adapters":[{"id":"string","category":"ecommerce","authType":"oauth2"}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Integrations_registry","tags":["integrations"],"parameters":[],"description":"Gibt die im laufenden Prozess registrierten Connector-Adapter aus — je Adapter Kennung, Kategorie und erwartetes Anmeldeverfahren. Gelesen wird ausschliesslich die In-Memory-Registrierung: kein Datenbankzugriff, keine Filterung nach Mandant, keine Aussage darueber, ob ein Connector fuer diesen Mandanten verbunden ist.","summary":"Gibt die im laufenden Prozess registrierten Connector-Adapter aus","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/metrics":{"get":{"responses":{"200":{"description":"Metrics snapshot — possibly served from the 30 s cache; see `ts`.","content":{"application/json":{"schema":{"type":"object","properties":{"api":{"type":"object","properties":{"p50":{"type":"number"},"p95":{"type":"number"},"p99":{"type":"number"},"rps":{"type":"number"},"requestsInWindow":{"type":"number"},"errorRate":{"type":"number"}},"required":["p50","p95","p99","rps","requestsInWindow","errorRate"],"additionalProperties":false},"db":{"type":"object","properties":{"connections":{"type":"object","properties":{"active":{"type":"number"},"idle":{"type":"number"},"max":{"type":"number"}},"required":["active","idle","max"],"additionalProperties":false},"slowQueries":{"type":"number"},"available":{"type":"boolean"}},"required":["connections","slowQueries","available"],"additionalProperties":false},"redis":{"type":"object","properties":{"memoryBytes":{"type":"number"},"memoryHuman":{"type":"string"},"connected":{"type":"boolean"}},"required":["memoryBytes","memoryHuman","connected"],"additionalProperties":false},"queues":{"type":"object","properties":{"waiting":{"type":"number"},"active":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"},"available":{"type":"boolean"}},"required":["waiting","active","completed","failed","available"],"additionalProperties":false},"replica":{"type":"object","properties":{"lagMs":{"type":"number"},"usingPrimary":{"type":"boolean"}},"required":["lagMs","usingPrimary"],"additionalProperties":false},"history":{"type":"object","properties":{"requestsPerHour":{"type":"array","items":{"type":"number"}},"errorsPerHour":{"type":"array","items":{"type":"number"}}},"required":["requestsPerHour","errorsPerHour"],"additionalProperties":false},"ts":{"type":"string"}},"required":["api","db","redis","queues","replica","history","ts"],"additionalProperties":false},"example":{"api":{"p50":0,"p95":0,"p99":0,"rps":0,"requestsInWindow":0,"errorRate":0},"db":{"connections":{"active":0,"idle":0,"max":0},"slowQueries":0,"available":true},"redis":{"memoryBytes":0,"memoryHuman":"string","connected":true},"queues":{"waiting":0,"active":0,"completed":0,"failed":0,"available":true},"replica":{"lagMs":0,"usingPrimary":true},"history":{"requestsPerHour":[0],"errorsPerHour":[0]},"ts":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin only"}},"operationId":"getApiV1Metrics","tags":["metrics"],"parameters":[],"description":"Performance dashboard snapshot — API/DB/Redis/queue stats with 30s cache. The API numbers and the hourly history are IN-PROCESS: they describe this one container, not the cluster, and they reset when it restarts. Database, Redis, queue and replica figures are probed live, each best-effort — a failing probe returns zeros with its `available` flag false instead of failing the request, so a zero there means „could not measure\", not „idle\". The whole payload is cached per tenant for 30 seconds, which makes `ts` up to that much older than the request. Requires role `admin` or above; the check is a step ladder, so `super_admin` passes it too.","summary":"Performance dashboard snapshot — API/DB/Redis/queue stats with 30s cache","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/gaeb/import":{"post":{"responses":{"201":{"description":"LV angelegt, Positionen gelesen","content":{"application/json":{"schema":{"type":"object","properties":{"lv":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"name":{"type":"string"},"objectName":{"type":["string","null"]},"bauprojektId":{"type":["string","null"]},"customerId":{"type":["string","null"]},"gewerk":{"type":["string","null"]},"status":{"type":"string","enum":["draft","sent","awarded","completed","cancelled"]},"currency":{"type":"string"},"totalNet":{"type":"string","description":"Numerischer Betrag als Zeichenkette"},"totalVat":{"type":"string"},"totalGross":{"type":"string"},"gaebVersion":{"type":["string","null"]},"tender":{"type":"boolean"},"validUntil":{"type":["string","null"]},"notes":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","number","name","objectName","bauprojektId","customerId","gewerk","status","currency","totalNet","totalVat","totalGross","gaebVersion","tender","validUntil","notes","customFields","createdAt","updatedAt"],"description":"Das frisch angelegte LV, Status draft und Summen noch 0"},"positions":{"type":"array","items":{"type":"object","properties":{"oz":{"type":"string","description":"Ordnungszahl der Position"},"kind":{"type":"string","enum":["title","section","position","note"]},"shortText":{"type":"string"},"longText":{"type":["string","null"]},"unit":{"type":["string","null"]},"qty":{"type":["number","null"]},"unitPrice":{"type":["number","null"]},"vatRate":{"type":"number"},"parentOz":{"type":["string","null"]}},"required":["oz","kind","shortText","longText","unit","qty","unitPrice","vatRate","parentOz"]},"description":"Aus der Datei gelesen, NICHT gespeichert"},"header":{"type":"object","properties":{"number":{"type":["string","null"]},"name":{"type":["string","null"]},"objectName":{"type":["string","null"]},"currency":{"type":"string"},"gaebVersion":{"type":"string","enum":["DA83","DA84","DA86","UNKNOWN"]}},"required":["number","name","objectName","currency","gaebVersion"]},"totals":{"type":"object","properties":{"net":{"type":"number"},"vat":{"type":"number"},"gross":{"type":"number"}},"required":["net","vat","gross"],"description":"Aus den gelesenen Positionen gerechnet, mit dem Standard-Steuersatz"},"warnings":{"type":"array","items":{"type":"string"},"description":"Fehlende Felder und Ersatzwerte beim Lesen"},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"gaeb"},"version":{"type":"string","enum":["DA83","DA84","DA86","UNKNOWN"]}},"required":["tenantId","source","version"]}},"required":["lv","positions","header","totals","warnings","meta"]},"example":{"lv":{"id":"string","number":"string","name":"string","objectName":"string","bauprojektId":"string","customerId":"string","gewerk":"string","status":"draft","currency":"string","totalNet":"string","totalVat":"string","totalGross":"string","gaebVersion":"string","tender":true,"validUntil":"string","notes":"string","customFields":{},"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"},"positions":[{"oz":"string","kind":"title","shortText":"string","longText":"string","unit":"string","qty":0,"unitPrice":0,"vatRate":0,"parentOz":"string"}],"header":{"number":"string","name":"string","objectName":"string","currency":"string","gaebVersion":"DA83"},"totals":{"net":0,"vat":0,"gross":0},"warnings":["string"],"meta":{"tenantId":"string","source":"gaeb","version":"DA83"}}}}},"401":{"description":"Unauthorized"},"422":{"description":"Keine Position gefunden — es wurde nichts angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"GAEB_PARSE_EMPTY"},"warnings":{"type":"array","items":{"type":"string"}}},"required":["error","warnings"]}}}}},"operationId":"postApiV1GaebImport","tags":["gaeb"],"parameters":[],"description":"Liest eine GAEB-Datei (DA83/84/86) und legt daraus ein LV im Entwurf an. Die XML kommt als Zeichenkette im Rumpf, nicht als Datei-Upload. Findet der Leser keine einzige Position, wird nichts angelegt und der Aufruf endet mit 422. Sonst entsteht ein LV im Status draft mit Summen 0; die gelesenen Positionen kommen in der Antwort zurueck, werden aber NICHT am LV gespeichert. totals rechnet der Aufruf aus den gelesenen Positionen mit dem Standard-Steuersatz des Mandanten. ACHTUNG: die LV-Ablage dahinter ist derzeit ein prozesslokaler Speicher mit Beispieldaten, keine Datenbank — ein angelegtes LV ueberlebt keinen Neustart.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"xml":{"type":"string","minLength":50},"bauprojektId":{"type":"string"},"customerId":{"type":"string"}},"required":["xml"]},"example":{"xml":"stringxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","bauprojektId":"string","customerId":"string"}}}},"summary":"Liest eine GAEB-Datei (DA83/84/86) und legt daraus ein LV im Entwurf an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/gaeb/export/{lvId}":{"get":{"responses":{"200":{"description":"GAEB-XML als Anhang","content":{"application/xml":{"schema":{"type":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"LV nicht gefunden"}},"operationId":"getApiV1GaebExportByLvId","tags":["gaeb"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"lvId","required":true}],"description":"Gibt ein LV als GAEB-XML zum Herunterladen aus. Die Antwort ist KEIN JSON, sondern application/xml als Anhang, benannt nach LV-Nummer und Fassung. Die Abfragezeichenkette version waehlt DA84; jeder andere Wert — auch ein fehlender — ergibt DA83. Ein unbekanntes LV ergibt 404.","summary":"Gibt ein LV als GAEB-XML zum Herunterladen aus","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/gaeb/analyze/{lvId}":{"post":{"responses":{"200":{"description":"Befunde und Punktzahl — nichts davon wird gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"lvId":{"type":"string"},"analyzedAt":{"type":"string","format":"date-time"},"totalNet":{"type":"number"},"totalPositions":{"type":"integer","minimum":0},"findings":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"severity":{"type":"string","enum":["info","warning","error"]},"message":{"type":"string"},"positionId":{"type":"string"},"suggestion":{"type":"string"}},"required":["id","severity","message"]}},"score":{"type":"number","minimum":0,"maximum":100}},"required":["lvId","analyzedAt","totalNet","totalPositions","findings","score"]},"example":{"lvId":"string","analyzedAt":"2026-01-01T12:00:00.000Z","totalNet":0,"totalPositions":0,"findings":[{"id":"string","severity":"info","message":"string","positionId":"string","suggestion":"string"}],"score":0}}}},"401":{"description":"Unauthorized"},"404":{"description":"LV nicht gefunden"}},"operationId":"postApiV1GaebAnalyzeByLvId","tags":["gaeb"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"lvId","required":true}],"description":"Prueft ein LV und liefert Befunde samt Punktzahl. Geprueft werden die Positionen des LV gegen hinterlegte Branchenspannen fuer Einheitspreise; jeder Befund traegt eine Schwere (info, warning, error) und meist einen Vorschlag. score reicht von 0 bis 100. Der Aufruf aendert nichts am LV — er schreibt weder Befunde noch Punktzahl zurueck. Ein unbekanntes LV ergibt 404.","summary":"Prueft ein LV und liefert Befunde samt Punktzahl","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/gaeb/generate":{"post":{"responses":{"200":{"description":"Vorschlaege — noch nichts angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string","description":"Der eingereichte Text, unveraendert zurueck"},"positions":{"type":"array","items":{"type":"object","properties":{"oz":{"type":"string"},"kind":{"type":"string","enum":["title","position"]},"shortText":{"type":"string"},"unit":{"type":"string"},"qty":{"type":"number"},"unitPrice":{"type":"number"},"vatRate":{"type":"number"}},"required":["oz","kind","shortText","unit","qty","unitPrice","vatRate"]},"description":"Vorschlaege — nichts davon ist gespeichert"},"confirmationRequired":{"type":"boolean","const":true},"message":{"type":"string"}},"required":["description","positions","confirmationRequired","message"]},"example":{"description":"string","positions":[{"oz":"string","kind":"title","shortText":"string","unit":"string","qty":0,"unitPrice":0,"vatRate":0}],"confirmationRequired":true,"message":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1GaebGenerate","tags":["gaeb"],"parameters":[],"description":"Schlaegt aus einem Beschreibungstext LV-Positionen vor. Der Text darf 3 bis 1000 Zeichen haben. Die Vorschlaege entstehen regelbasiert aus Schluesselwoertern und Massangaben im Text, ohne Sprachmodell. Gespeichert wird NICHTS: confirmationRequired ist immer true, die Uebernahme ist ein eigener Schritt. Erkennt der Text kein bekanntes Gewerk, kommt trotzdem ein Vorschlag — ein Titel und eine Pauschalposition mit dem Text selbst und Einheitspreis 0. Eine leere Liste gibt es also nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string","minLength":3,"maxLength":1000}},"required":["description"]},"example":{"description":"string"}}}},"summary":"Schlaegt aus einem Beschreibungstext LV-Positionen vor","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/gaeb/v2/import":{"post":{"responses":{"200":{"description":"Datei eingelesen. `project` ist der normalisierte LV-Baum (flache `nodes`-Liste aus Positionen und Gruppen, Hierarchie ueber `oz`), `stats` die Zaehlung des Durchlaufs, `meta` spiegelt Mandant und Dateiname. Nicht-toedliche Hinweise stehen in `project.warnings` — die Antwort ist auch dann 200 mit `ok: true`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"project":{"type":"object","properties":{"meta":{"type":"object","properties":{"docType":{"type":"string","enum":["D83","D84","D86","UNKNOWN"]},"version":{"type":"string","enum":["3.1","3.2","legacy","unknown"]},"projectName":{"type":["string","null"]},"projectNumber":{"type":["string","null"]},"objectName":{"type":["string","null"]},"currency":{"type":"string"},"issuedAt":{"type":["string","null"]}},"required":["docType","version","projectName","projectNumber","objectName","currency","issuedAt"]},"nodes":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","const":"item"},"oz":{"type":"string"},"ozParts":{"type":"array","items":{"type":"integer"}},"kurztext":{"type":"string"},"langtext":{"type":["object","null"],"properties":{"paragraphs":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","const":"paragraph"},"runs":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"bold":{"type":"boolean"},"italic":{"type":"boolean"}},"required":["text"]}}},"required":["type","runs"]}},"plain":{"type":"string"}},"required":["paragraphs","plain"]},"unit":{"type":["string","null"]},"qty":{"type":["number","null"]},"unitPrice":{"type":["number","null"]},"totalPrice":{"type":["number","null"]},"optional":{"type":"boolean"},"alternative":{"type":"boolean"}},"required":["kind","oz","ozParts","kurztext","langtext","unit","qty","unitPrice","totalPrice","optional","alternative"]},{"type":"object","properties":{"kind":{"type":"string","const":"group"},"oz":{"type":"string"},"ozParts":{"type":"array","items":{"type":"integer"}},"kurztext":{"type":"string"},"langtext":{"type":["object","null"],"properties":{"paragraphs":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","const":"paragraph"},"runs":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"bold":{"type":"boolean"},"italic":{"type":"boolean"}},"required":["text"]}}},"required":["type","runs"]}},"plain":{"type":"string"}},"required":["paragraphs","plain"]}},"required":["kind","oz","ozParts","kurztext","langtext"]}]}},"warnings":{"type":"array","items":{"type":"string"}}},"required":["meta","nodes","warnings"]},"stats":{"type":"object","properties":{"items":{"type":"integer"},"groups":{"type":"integer"},"durationMs":{"type":"number"},"bytes":{"type":"integer"}},"required":["items","groups","durationMs","bytes"]},"meta":{"type":"object","properties":{"tenantId":{"type":["string","null"]},"filename":{"type":["string","null"]}},"required":["tenantId","filename"]}},"required":["ok","project","stats","meta"]},"example":{"ok":true,"project":{"meta":{"docType":"D83","version":"3.1","projectName":"string","projectNumber":"string","objectName":"string","currency":"string","issuedAt":"string"},"nodes":[{"kind":"item","oz":"string","ozParts":[0],"kurztext":"string","langtext":{"paragraphs":[],"plain":"string"},"unit":"string","qty":0,"unitPrice":0,"totalPrice":0,"optional":true,"alternative":true}],"warnings":["string"]},"stats":{"items":0,"groups":0,"durationMs":0,"bytes":0},"meta":{"tenantId":"string","filename":"string"}}}}},"400":{"description":"Rumpf nicht lesbar, `file` fehlt, ist leer oder hat eine unerlaubte Endung."},"401":{"description":"Unauthorized"},"413":{"description":"Datei groesser als 50 MB."},"415":{"description":"Content-Type ist nicht `multipart/form-data`."},"422":{"description":"GAEB-Pruefung fehlgeschlagen (Meldung enthaelt `error`, `strict`, `issues`, `itemCount`, `docType`) oder der Leser hat die Datei als ungueltiges GAEB zurueckgewiesen (`<code>: <meldung>`)."}},"operationId":"postApiV1GaebV2Import","tags":["gaeb"],"parameters":[],"summary":"GAEB-Datei hochladen und als normalisiertes LV einlesen","description":"Nimmt genau EIN Feld `file` aus einem `multipart/form-data`-Rumpf entgegen; ein anderer Content-Type wird mit 415 abgewiesen, ein fehlendes oder leeres `file` mit 400. Erlaubte Endungen sind `.x83`, `.x84`, `.x86` und `.xml` (eine Datei ganz ohne Endung wird durchgelassen), die Obergrenze liegt bei 50 MB — darueber 413.\n\nDer Inhalt wird ZUERST geprueft und erst danach eingelesen. Standardmaessig nachsichtig; `?strict=1` (oder `strict=true`) erzwingt alle Pruefungen. Schlaegt die Pruefung fehl, kommt 422 — die Meldung ist dabei selbst wieder JSON und traegt `error`, `strict`, die Einzelbefunde `issues`, `itemCount` und `docType`, damit die Oberflaeche eine Fehlerliste zeigen kann.\n\nDie Art des Dokuments wird selbst erkannt (D83 / D84 / D86, sonst der allgemeine Leser). Rein lesend: NICHTS wird gespeichert — kein Beleg, keine Datei, kein Datenbankeintrag. Der Mandant aus der Sitzung dient nur dem Protokoll und wird in `meta.tenantId` zurueckgespiegelt. Wer das Ergebnis behalten will, muss es selbst weiterreichen.\n\nLiegt neben dieser Route: das aeltere `POST /api/v1/gaeb/import` mit JSON-Rumpf. Beide bestehen waehrend der Umstellung nebeneinander."}},"/api/v1/push/vapid":{"get":{"responses":{"200":{"description":"Der oeffentliche Schluessel.","content":{"application/json":{"schema":{"type":"object","properties":{"publicKey":{"type":"string","description":"Der OEFFENTLICHE Schluessel — der private verlaesst den Server nie"}},"required":["publicKey"]},"example":{"publicKey":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Kein VAPID-Schluesselpaar eingerichtet"}},"operationId":"getApiV1PushVapid","tags":["push"],"parameters":[],"summary":"Oeffentlichen VAPID-Schluessel fuer Push-Abonnements abrufen","description":"Gibt den oeffentlichen VAPID-Schluessel zurueck, den der Browser braucht, um ein Push-Abonnement anzulegen. Der Schluessel ist SERVERWEIT derselbe, nicht je Mandant. Ist keiner eingerichtet, kommt 503 `vapid_not_configured` — dann geht auch `POST /subscribe` ins Leere."}},"/api/v1/push/subscribe":{"post":{"responses":{"200":{"description":"Eingetragen oder aktualisiert — `created` unterscheidet beides.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"created":{"type":"boolean","description":"false = dieses Geraet war schon eingetragen und wurde aktualisiert"},"id":{"type":"string","description":"Id des Eintrags — bleibt bei einer Aktualisierung dieselbe"}},"required":["ok","created","id"]},"example":{"ok":true,"created":true,"id":"string"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1PushSubscribe","tags":["push"],"parameters":[],"description":"Traegt das Push-Abonnement eines Geraets ein. Mandant und Nutzer kommen aus dem Anmeldekontext und lassen sich NICHT im Rumpf mitgeben. Der Eintrag ist eindeutig ueber Mandant und `endpoint`: dasselbe Geraet zweimal anzumelden legt keinen zweiten Eintrag an, sondern aktualisiert den vorhandenen — `created` sagt, welcher Fall eintrat. Der User-Agent wird mitgeschrieben, damit sich ein Geraet spaeter wiedererkennen laesst.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"type":"string","format":"uri"},"expirationTime":{"type":["number","null"]},"keys":{"type":"object","properties":{"p256dh":{"type":"string"},"auth":{"type":"string"}},"required":["p256dh","auth"]}},"required":["endpoint","keys"]},"example":{"endpoint":"https://example.com","expirationTime":0,"keys":{"p256dh":"string","auth":"string"}}}}},"summary":"Traegt das Push-Abonnement eines Geraets ein","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Entfernt, oder es gab nichts zu entfernen — `removed` unterscheidet das.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"removed":{"type":"boolean","description":"false = zu diesem Endpunkt gab es nichts zu entfernen"}},"required":["ok","removed"]},"example":{"ok":true,"removed":true}}}},"401":{"description":"Unauthorized"}},"operationId":"deleteApiV1PushSubscribe","tags":["push"],"parameters":[],"description":"Entfernt das Abonnement zu einem `endpoint` — endgueltig, ohne Ruecknahme. Entfernt wird nur, was dem eigenen Mandanten gehoert. Ein unbekannter Endpunkt ergibt KEIN 404: die Antwort ist 200 mit `removed: false`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"type":"string","format":"uri"}},"required":["endpoint"]},"example":{"endpoint":"https://example.com"}}}},"summary":"Entfernt das Abonnement zu einem `endpoint` — endgueltig, ohne Ruecknahme","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/push/send":{"post":{"responses":{"200":{"description":"Der Versand ist durchgelaufen — NICHT, dass jede Zustellung gelang. `sent` zaehlt die erfolgreichen, `results` nennt jede einzeln.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"sent":{"type":"integer","description":"Zahl der ERFOLGREICH angenommenen Zustellungen"},"results":{"type":"array","items":{"type":"object","properties":{"endpoint":{"type":"string"},"status":{"type":"integer","description":"Antwort des Push-Dienstes; 0, wenn der Versuch selbst scheiterte"},"ok":{"type":"boolean"},"shouldRemove":{"type":"boolean","description":"true bei 404/410 — der Eintrag wird daraufhin sofort entfernt"},"mode":{"type":"string","description":"ios | standard"},"error":{"type":"string","description":"Nur, wenn der Versuch selbst eine Ausnahme warf"}},"required":["endpoint","status","ok","shouldRemove","mode"]}}},"required":["ok","sent","results"]},"example":{"ok":true,"sent":0,"results":[{"endpoint":"string","status":0,"ok":true,"shouldRemove":true,"mode":"string","error":"string"}]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"operationId":"postApiV1PushSend","tags":["push"],"parameters":[],"description":"Schickt eine Benachrichtigung an alle eingetragenen Geraete. Mit `userId` nur an die dieses Nutzers, sonst an alle des Mandanten. Ein mitgeschicktes `tenantId` STICHT den Anmeldekontext aus — dieser Aufruf ist fuer den internen Gebrauch gedacht und prueft nicht, ob der Aufrufer zu diesem Mandanten gehoert. Gibt es kein Ziel, antwortet er 200 mit `sent: 0`; das ist kein Fehler. Je Geraet steht das Ergebnis einzeln in `results` — `ok: false` heisst, dass DIESE Zustellung scheiterte, waehrend die uebrigen liefen. Nebenwirkung: Geraete, die der Push-Dienst mit 404 oder 410 abweist, werden sofort ausgetragen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"tenantId":{"type":"string"},"payload":{"type":"object","properties":{"title":{"type":"string","minLength":1},"body":{"type":"string","minLength":1},"url":{"type":"string"},"tag":{"type":"string"},"icon":{"type":"string"},"data":{"type":"object","additionalProperties":{}}},"required":["title","body"]}},"required":["payload"]},"example":{"userId":"string","tenantId":"string","payload":{"title":"string","body":"string","url":"string","tag":"string","icon":"string","data":{}}}}}},"summary":"Schickt eine Benachrichtigung an alle eingetragenen Geraete","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/push-subscriptions/vapid":{"get":{"responses":{"200":{"description":"Der oeffentliche Schluessel.","content":{"application/json":{"schema":{"type":"object","properties":{"publicKey":{"type":"string","description":"Der OEFFENTLICHE Schluessel — der private verlaesst den Server nie"}},"required":["publicKey"]},"example":{"publicKey":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Kein VAPID-Schluesselpaar eingerichtet"}},"operationId":"getApiV1Push-subscriptionsVapid","tags":["push"],"parameters":[],"summary":"Oeffentlichen VAPID-Schluessel fuer Push-Abonnements abrufen","description":"Gibt den oeffentlichen VAPID-Schluessel zurueck, den der Browser braucht, um ein Push-Abonnement anzulegen. Der Schluessel ist SERVERWEIT derselbe, nicht je Mandant. Ist keiner eingerichtet, kommt 503 `vapid_not_configured` — dann geht auch `POST /subscribe` ins Leere."}},"/api/v1/push-subscriptions/subscribe":{"post":{"responses":{"200":{"description":"Eingetragen oder aktualisiert — `created` unterscheidet beides.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"created":{"type":"boolean","description":"false = dieses Geraet war schon eingetragen und wurde aktualisiert"},"id":{"type":"string","description":"Id des Eintrags — bleibt bei einer Aktualisierung dieselbe"}},"required":["ok","created","id"]},"example":{"ok":true,"created":true,"id":"string"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1Push-subscriptionsSubscribe","tags":["push"],"parameters":[],"description":"Traegt das Push-Abonnement eines Geraets ein. Mandant und Nutzer kommen aus dem Anmeldekontext und lassen sich NICHT im Rumpf mitgeben. Der Eintrag ist eindeutig ueber Mandant und `endpoint`: dasselbe Geraet zweimal anzumelden legt keinen zweiten Eintrag an, sondern aktualisiert den vorhandenen — `created` sagt, welcher Fall eintrat. Der User-Agent wird mitgeschrieben, damit sich ein Geraet spaeter wiedererkennen laesst.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"type":"string","format":"uri"},"expirationTime":{"type":["number","null"]},"keys":{"type":"object","properties":{"p256dh":{"type":"string"},"auth":{"type":"string"}},"required":["p256dh","auth"]}},"required":["endpoint","keys"]},"example":{"endpoint":"https://example.com","expirationTime":0,"keys":{"p256dh":"string","auth":"string"}}}}},"summary":"Traegt das Push-Abonnement eines Geraets ein","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Entfernt, oder es gab nichts zu entfernen — `removed` unterscheidet das.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"removed":{"type":"boolean","description":"false = zu diesem Endpunkt gab es nichts zu entfernen"}},"required":["ok","removed"]},"example":{"ok":true,"removed":true}}}},"401":{"description":"Unauthorized"}},"operationId":"deleteApiV1Push-subscriptionsSubscribe","tags":["push"],"parameters":[],"description":"Entfernt das Abonnement zu einem `endpoint` — endgueltig, ohne Ruecknahme. Entfernt wird nur, was dem eigenen Mandanten gehoert. Ein unbekannter Endpunkt ergibt KEIN 404: die Antwort ist 200 mit `removed: false`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"type":"string","format":"uri"}},"required":["endpoint"]},"example":{"endpoint":"https://example.com"}}}},"summary":"Entfernt das Abonnement zu einem `endpoint` — endgueltig, ohne Ruecknahme","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/push-subscriptions/send":{"post":{"responses":{"200":{"description":"Der Versand ist durchgelaufen — NICHT, dass jede Zustellung gelang. `sent` zaehlt die erfolgreichen, `results` nennt jede einzeln.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"sent":{"type":"integer","description":"Zahl der ERFOLGREICH angenommenen Zustellungen"},"results":{"type":"array","items":{"type":"object","properties":{"endpoint":{"type":"string"},"status":{"type":"integer","description":"Antwort des Push-Dienstes; 0, wenn der Versuch selbst scheiterte"},"ok":{"type":"boolean"},"shouldRemove":{"type":"boolean","description":"true bei 404/410 — der Eintrag wird daraufhin sofort entfernt"},"mode":{"type":"string","description":"ios | standard"},"error":{"type":"string","description":"Nur, wenn der Versuch selbst eine Ausnahme warf"}},"required":["endpoint","status","ok","shouldRemove","mode"]}}},"required":["ok","sent","results"]},"example":{"ok":true,"sent":0,"results":[{"endpoint":"string","status":0,"ok":true,"shouldRemove":true,"mode":"string","error":"string"}]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"operationId":"postApiV1Push-subscriptionsSend","tags":["push"],"parameters":[],"description":"Schickt eine Benachrichtigung an alle eingetragenen Geraete. Mit `userId` nur an die dieses Nutzers, sonst an alle des Mandanten. Ein mitgeschicktes `tenantId` STICHT den Anmeldekontext aus — dieser Aufruf ist fuer den internen Gebrauch gedacht und prueft nicht, ob der Aufrufer zu diesem Mandanten gehoert. Gibt es kein Ziel, antwortet er 200 mit `sent: 0`; das ist kein Fehler. Je Geraet steht das Ergebnis einzeln in `results` — `ok: false` heisst, dass DIESE Zustellung scheiterte, waehrend die uebrigen liefen. Nebenwirkung: Geraete, die der Push-Dienst mit 404 oder 410 abweist, werden sofort ausgetragen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"tenantId":{"type":"string"},"payload":{"type":"object","properties":{"title":{"type":"string","minLength":1},"body":{"type":"string","minLength":1},"url":{"type":"string"},"tag":{"type":"string"},"icon":{"type":"string"},"data":{"type":"object","additionalProperties":{}}},"required":["title","body"]}},"required":["payload"]},"example":{"userId":"string","tenantId":"string","payload":{"title":"string","body":"string","url":"string","tag":"string","icon":"string","data":{}}}}}},"summary":"Schickt eine Benachrichtigung an alle eingetragenen Geraete","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/gdpr/export":{"get":{"responses":{"200":{"description":"JSON-Bundle als Datei-Download — mode \"demo\" heisst: keine echten Daten","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"exportedAt":{"type":"string","description":"Zeitpunkt der Erstellung (ISO 8601)"},"articleReference":{"type":"string","const":"DSGVO Art. 15 Auskunftsrecht"},"user":{"type":"object","additionalProperties":{},"description":"Die Nutzerzeile mit `id`, `email`, `name`, `role`, `emailVerified`, `lastLoginAt`, `consents`, `createdAt`, `updatedAt`"},"consents":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Alle Zeilen des Einwilligungsprotokolls"},"auditLog":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Hoechstens 5000 EIGENE Eintraege aus dem Protokoll des Mandanten, neueste zuerst; leer, wenn das Protokoll nicht gelesen werden konnte"},"metadata":{"type":"object","properties":{"dataController":{"type":"string"},"dpo":{"type":"string"},"retentionPolicy":{"type":"string"}},"required":["dataController","dpo","retentionPolicy"]}},"required":["exportedAt","articleReference","user","consents","auditLog","metadata"]},{"type":"object","properties":{"exportedAt":{"type":"string"},"mode":{"type":"string","const":"demo"},"articleReference":{"type":"string","const":"DSGVO Art. 15 Auskunftsrecht"},"data":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":"string"}},"required":["id"]},"activities":{"type":"array","items":{},"description":"Immer leer"},"auditLog":{"type":"array","items":{},"description":"Immer leer"}},"required":["user","activities","auditLog"]}},"required":["exportedAt","mode","articleReference","data"]}]},"example":{"exportedAt":"string","articleReference":"DSGVO Art. 15 Auskunftsrecht","user":{},"consents":[{}],"auditLog":[{}],"metadata":{"dataController":"string","dpo":"string","retentionPolicy":"string"}}}}},"401":{"description":"Unauthorized"},"404":{"description":"Zum angemeldeten Nutzer gibt es keine Zeile in `users`"}},"operationId":"getApiV1GdprExport","tags":["gdpr"],"parameters":[],"summary":"DSGVO Art. 15 Auskunft — alle Daten des angemeldeten Nutzers als JSON-Download","description":"Stellt die Auskunft nach DSGVO Art. 15 SOFORT zusammen und sendet sie als Datei-Download (`Content-Disposition: attachment`, Dateiname `dsgvo-export-<Nutzer>.json`) — kein Hintergrund-Auftrag, keine Mail. Gelesen wird ausschliesslich der ANGEMELDETE Nutzer: seine Zeile aus `users`, sein vollstaendiges Einwilligungsprotokoll und hoechstens 5000 eigene Eintraege aus dem Protokoll des Mandanten (neueste zuerst). Es wird nichts geschrieben und nichts geloescht.\n\nKann das Protokoll des Mandanten nicht gelesen werden, kommt `auditLog` LEER zurueck — die Auskunft bleibt trotzdem 200. Eine leere Liste ist hier also kein Beleg dafuer, dass es keine Eintraege gibt.\n\nOhne erreichbare Datenbank antwortet der Endpunkt mit einer ATTRAPPE: `mode: \"demo\"`, andere Form, leere Listen. Das ist KEINE Auskunft im Sinne von Art. 15 — wer die Antwort weiterreicht, muss dieses Feld pruefen. Gibt es die Nutzerzeile nicht, ist die Antwort 404."},"post":{"responses":{"200":{"description":"ZIP-Paket. Kopfzeile X-Export-Hash enthält die Prüfsumme.","content":{"application/zip":{}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1GdprExport","tags":["gdpr"],"parameters":[],"summary":"Erzeugt das Export-Paket sofort als ZIP (synchron)","description":"Nur fuer Mandanten-Administratoren, und anders als /export-request fuer eine FREMDE betroffene Person: `subjectType` (user/customer/contact) und `subjectId` benennen sie. Das Paket wird im Aufruf gebaut und sofort zurueckgegeben — es entsteht kein Auftrag, nichts wird abgelegt, und es gibt spaeter keinen zweiten Download. Der Inhalt ist ein Verzeichnis (Manifest) plus je Tabelle eine JSON- und eine CSV-Datei; die Kopfzeile X-Export-Hash traegt die Pruefsumme der Bytes und gehoert zum Nachweis. Ohne erreichbare Datenbank entsteht ein LEERES, aber gueltiges Paket statt eines Fehlers. Weil die Antwort ein Bytestrom ist, gibt es hier bewusst kein JSON-Antwortschema.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"subjectType":{"type":"string","enum":["user","customer","contact"]},"subjectId":{"type":"string","minLength":1}},"required":["subjectType","subjectId"]},"example":{"subjectType":"user","subjectId":"string"}}}}}},"/api/v1/gdpr/forget":{"post":{"responses":{"200":{"description":"Anonymisiert bzw. eingereiht. `mode: \"demo\"` heisst: es wurde NICHTS geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"mode":{"type":"string","const":"demo"},"gracePeriodDays":{"type":"number"},"cooldownDays":{"type":"number"},"hardDeleteAt":{"type":"string"},"anonymizedAt":{"type":"string"},"requestId":{"type":"string"},"cancelUrl":{"type":"string"},"articleReference":{"type":"string"},"note":{"type":"string"}},"required":["status"]},"example":{"status":"string","mode":"demo","gracePeriodDays":0,"cooldownDays":0,"hardDeleteAt":"string","anonymizedAt":"string","requestId":"string","cancelUrl":"string","articleReference":"string","note":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Passwort falsch — Löschung abgebrochen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"409":{"description":"Konto bereits anonymisiert oder nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Passwort-Verifizierung nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1GdprForget","tags":["gdpr"],"parameters":[],"summary":"Anonymisiert das eigene Konto (30 Tage Frist, danach endgültige Löschung)","description":"Betrifft ausschlieszlich das eigene Konto; ein fremder Nutzer laesst sich nicht angeben. Der Rumpf muss das aktuelle Passwort und `confirm: true` enthalten — das Passwort wird gegen die Anmeldung geprueft, damit eine gestohlene Sitzung allein nicht reicht (403 bei falschem Passwort, 503 wenn die Pruefung nicht erreichbar ist). Der Vorgang wirkt SOFORT und ist nicht widerrufbar: E-Mail wird auf deleted-<id>@anonymized.invalid gesetzt, der Name auf „Gelöschter Nutzer\", Bild und Passwort werden entfernt und der Loeschzeitpunkt gesetzt. Ein zweiter Aufruf ergibt 409. Die endgueltige Loeschung nach 30 Tagen erledigt ein Hintergrundlauf, nicht dieser Endpunkt; buchhaltungsrelevante Eintraege bleiben aus gesetzlichen Gruenden maskiert erhalten. Der Vorgang wird im Audit-Log festgehalten. Ohne erreichbare Datenbank kommt 200 mit mode=\"demo\" — dann wurde NICHTS geaendert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string","minLength":1},"confirm":{"type":"boolean","const":true}},"required":["password","confirm"]},"example":{"password":"string","confirm":true}}}}}},"/api/v1/gdpr/data-processing":{"get":{"responses":{"200":{"description":"Verantwortlicher, Auftragsverarbeiter und Tätigkeiten — feste Aufstellung","content":{"application/json":{"schema":{"type":"object","properties":{"articleReference":{"type":"string"},"dataController":{"type":"object","properties":{"name":{"type":"string"},"address":{"type":"string"},"contact":{"type":"string"}},"required":["name","address","contact"]},"processors":{"type":"array","items":{"type":"object","additionalProperties":{}}},"activities":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["articleReference","dataController","processors","activities"],"additionalProperties":true},"example":{"articleReference":"string","dataController":{"name":"string","address":"string","contact":"string"},"processors":[{}],"activities":[{}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1GdprData-processing","tags":["gdpr"],"parameters":[],"summary":"Verzeichnis der Verarbeitungstätigkeiten (DSGVO Art. 13, 14, 30)","description":"Gibt eine im Code hinterlegte, feste Aufstellung zurueck: Verantwortlicher mit Kontakt, die eingesetzten Auftragsverarbeiter (je mit Zweck, Land, Rechtsgrundlage, Datenkategorien, Garantien und Aufbewahrung in Tagen) und die eigenen Verarbeitungstaetigkeiten. Der Inhalt ist fuer ALLE Mandanten identisch und wird weder aus der Datenbank noch aus den Mandanteneinstellungen gelesen — er aendert sich nur mit einer neuen Programmfassung. `retentionDays: null` heiszt „keine feste Frist\", nicht „keine Aufbewahrung\"."}},"/api/v1/gdpr/consent":{"get":{"responses":{"200":{"description":"Einwilligungen. Ohne Datenbank kommen LEERE Werte mit 200 — nicht als „nichts eingewilligt\" lesen.","content":{"application/json":{"schema":{"type":"object","properties":{"consents":{"type":"object","additionalProperties":{}},"history":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["consents"]},"example":{"consents":{},"history":[{}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1GdprConsent","tags":["gdpr"],"parameters":[],"summary":"Aktuelle Einwilligungen des Nutzers samt Änderungsverlauf","description":"Liest `consents` des ANGEMELDETEN Nutzers (je Art ein Eintrag mit Ja/Nein, Zeitpunkt und IP) und dazu den vollstaendigen Verlauf aus dem Einwilligungs-Protokoll — ungefiltert, ohne Blaetterung und ohne zugesagte Reihenfolge; wer eine Zeitachse braucht, sortiert selbst. Ein fremder Nutzer laesst sich nicht abfragen. Der Verlauf wird nur ergaenzt, nie geaendert: er haelt auch zurueckgezogene Einwilligungen fest."},"post":{"responses":{"200":{"description":"Einwilligungen gesetzt — `logged` nennt die Zahl der Protokollzeilen","content":{"application/json":{"schema":{"type":"object","properties":{"consents":{"type":"object","additionalProperties":{}},"updatedAt":{"type":"string"},"logged":{"type":"integer"}},"required":["consents","updatedAt","logged"],"additionalProperties":false},"example":{"consents":{},"updatedAt":"string","logged":0}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1GdprConsent","tags":["gdpr"],"parameters":[],"summary":"Setzt Einwilligungen und protokolliert jede Änderung","description":"Teil-Update fuer den ANGEMELDETEN Nutzer: nur die gesendeten Arten (marketing, analytics, ai_training, cookies_analytics, cookies_marketing) werden veraendert, alle uebrigen bleiben stehen. Jede Art wird mit Zeitpunkt und aufrufender IP festgehalten. Eine Protokollzeile entsteht NUR bei echter Aenderung — wer denselben Wert erneut sendet, erzeugt keine; `logged` nennt deshalb die Zahl der tatsaechlichen Aenderungen, nicht die der gesendeten Felder. Zu jeder Protokollzeile werden IP und User-Agent mitgeschrieben. Ohne Datenbank antwortet der Endpunkt 200 mit status=\"noop\" und mode=\"demo\", ohne etwas zu speichern.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"marketing":{"type":"boolean"},"analytics":{"type":"boolean"},"ai_training":{"type":"boolean"},"cookies_analytics":{"type":"boolean"},"cookies_marketing":{"type":"boolean"}}},"example":{"marketing":true,"analytics":true,"ai_training":true,"cookies_analytics":true,"cookies_marketing":true}}}}}},"/api/v1/gdpr/erase":{"post":{"responses":{"200":{"description":"Löschquittung. `unscopedTables` nennt Tabellen OHNE Mandantentrennung — dort wurde NICHT geloescht. `exportHash` und `proofId` sind der Nachweis.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"exportHash":{"type":"string"},"erasedRowCount":{"type":"integer"},"proofId":{"type":"string"},"piiColumnsRedacted":{"type":"array","items":{"type":"string"}},"unscopedTables":{"type":"array","items":{"type":"string"}}},"required":["ok","exportHash","erasedRowCount","proofId","piiColumnsRedacted","unscopedTables"],"additionalProperties":false},"example":{"ok":true,"exportHash":"string","erasedRowCount":0,"proofId":"string","piiColumnsRedacted":["string"],"unscopedTables":["string"]}}}},"400":{"description":"confirm muss literal true sein — Löschung ist unumkehrbar"},"401":{"description":"Unauthorized"},"429":{"description":"Mehr als 3 Löschungen pro Stunde — Wartezeit in Retry-After"}},"operationId":"postApiV1GdprErase","tags":["gdpr"],"parameters":[],"description":"Führt die Löschung aus und stellt einen Nachweis aus. Max. 3 pro Stunde je Mandant.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"subjectType":{"type":"string","enum":["user","customer","contact"]},"subjectId":{"type":"string","minLength":1},"confirm":{"type":"boolean","const":true},"reason":{"type":"string","minLength":3}},"required":["subjectType","subjectId","confirm","reason"]},"example":{"subjectType":"user","subjectId":"string","confirm":true,"reason":"string"}}}},"summary":"Führt die Löschung aus und stellt einen Nachweis aus","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/gdpr/export-request":{"post":{"responses":{"202":{"description":"Job eingereiht — die Datei existiert NOCH NICHT. Zustand über GET /gdpr/export-request/{jobId} abfragen.","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string"},"articleReference":{"type":"string"},"message":{"type":"string"}},"required":["jobId","status","articleReference","message"],"additionalProperties":false},"example":{"jobId":"string","status":"string","articleReference":"string","message":"string"}}}},"400":{"description":"confirm missing"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1GdprExport-request","tags":["gdpr"],"parameters":[],"summary":"Plant einen DSGVO-Datenexport (Art. 20) als Hintergrund-Job","description":"Reiht einen Auftrag in `public.gdpr_export_jobs` ein (Zustand `queued`) und ordnet ihn ueber eine nachgeschobene Aktualisierung dem Mandanten zu. Mehr passiert hier NICHT: die Datei entsteht erst spaeter im Hintergrund, es wird keine Mail verschickt und keine Download-Adresse zurueckgegeben. Den Zustand liefert `GET /gdpr/export-request/{jobId}`.\n\nDer Auftrag haengt am ANGEMELDETEN Nutzer; ein Auftrag fuer jemand anderen ist ueber diese Route nicht moeglich. Ein zweiter Aufruf reiht einen ZWEITEN Auftrag ein — es gibt keine Sperre gegen Doppelanfragen und kein 409.\n\nOhne erreichbare Datenbank kommt eine ATTRAPPE mit `mode: \"demo\"` und einer erfundenen `jobId` — es wurde dann nichts eingereiht, und das Abfragen dieser Kennung ergibt spaeter kein Ergebnis.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"confirm":{"type":"boolean","const":true}},"required":["confirm"]},"example":{"confirm":true}}}}}},"/api/v1/gdpr/export-request/{jobId}":{"get":{"responses":{"200":{"description":"Zustand des Jobs. `mode: \"demo\"` heisst: es gibt gar keinen Job — die Antwort ist eine Attrappe, kein Fortschritt.","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string"},"mode":{"type":"string","const":"demo"},"requestedAt":{},"completedAt":{},"downloadUrl":{},"fileSizeBytes":{},"expiresAt":{},"error":{}},"required":["jobId","status"]},"example":{"jobId":"string","status":"string","mode":"demo"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"getApiV1GdprExport-requestByJobId","tags":["gdpr"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"jobId","required":true}],"summary":"Status-Poll fuer einen DSGVO-Export-Job","description":"Liefert den Zustand des per POST /export-request eingereihten Auftrags samt Anforderungs- und Abschlusszeit, Download-Adresse, Dateigroesze, SHA-256-Pruefsumme, Ablaufzeitpunkt und Fehlertext. Nur der Nutzer, der den Auftrag gestellt hat, darf ihn abfragen — ein fremder Auftrag ergibt 403, ein unbekannter 404. Rein lesend: der Aufruf stoeszt weder einen Lauf noch eine Wiederholung an. Ohne erreichbare Datenbank kommt eine ATTRAPPE (status=\"queued\", mode=\"demo\") — sie bedeutet nicht, dass ein Auftrag laeuft."}},"/api/v1/gdpr/export/download/{token}":{"get":{"responses":{"200":{"description":"ZIP-Paket als Download","content":{"application/zip":{}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Token unknown — oder der Paketinhalt fehlt"},"409":{"description":"Auftrag noch nicht fertig"},"410":{"description":"Token expired"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1GdprExportDownloadByToken","tags":["gdpr"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"token","required":true}],"summary":"Streamt einen DSGVO-Export per signiertem Token","description":"Ohne Anmeldung erreichbar — das Token IST der Nachweis, wer es hat, bekommt das Paket. Es wird gegen den Export-Auftrag geprueft: ein unbekanntes Token ergibt 404, ein abgelaufenes 410, und ein noch nicht fertiger Auftrag 409 mit dem aktuellen Zustand im Text. Das Token verfaellt beim Abruf NICHT: solange es gueltig ist, laesst sich das Paket mehrfach laden. Ausgeliefert werden ZIP-Bytes, deshalb hier bewusst kein JSON-Antwortschema; die Fehlerfaelle kommen als reiner Text, nicht als JSON."}},"/api/v1/gdpr/delete-request":{"post":{"responses":{"202":{"description":"Löschung geplant — sie erfolgt erst nach der Frist, JETZT ist nichts geloescht. `cancelUrl` nennt den Weg zum Abbruch.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"mode":{"type":"string","const":"demo"},"gracePeriodDays":{"type":"number"},"cooldownDays":{"type":"number"},"hardDeleteAt":{"type":"string"},"anonymizedAt":{"type":"string"},"requestId":{"type":"string"},"cancelUrl":{"type":"string"},"articleReference":{"type":"string"},"note":{"type":"string"}},"required":["status"]},"example":{"status":"string","mode":"demo","gracePeriodDays":0,"cooldownDays":0,"hardDeleteAt":"string","anonymizedAt":"string","requestId":"string","cancelUrl":"string","articleReference":"string","note":"string"}}}},"400":{"description":"confirm missing"},"401":{"description":"Unauthorized"},"409":{"description":"Bereits eine Löschung geplant (deletion_already_scheduled)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1GdprDelete-request","tags":["gdpr"],"parameters":[],"description":"Plant DSGVO Art. 17 Loeschung mit 7-Tage-Cooldown. Buchhaltungsdaten bleiben (GoBD) anonymisiert erhalten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"confirm":{"type":"boolean","const":true},"reason":{"type":"string","maxLength":1000}},"required":["confirm"]},"example":{"confirm":true,"reason":"string"}}}},"summary":"Plant DSGVO Art. 17 Loeschung mit 7-Tage-Cooldown","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/gdpr/delete-request/{id}":{"delete":{"responses":{"200":{"description":"Abgebrochen. `mode: \"demo\"` heisst: es gab gar nichts abzubrechen.","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"status":{"type":"string","const":"cancelled"},"cancelledAt":{"type":"string"},"mode":{"type":"string","const":"demo"}},"required":["status"]},"example":{"requestId":"string","status":"cancelled","cancelledAt":"string","mode":"demo"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Der Auftrag gehoert einem anderen Nutzer"},"404":{"description":"Not found"},"409":{"description":"Already completed"}},"operationId":"deleteApiV1GdprDelete-requestById","tags":["gdpr"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Bricht eine geplante DSGVO Art. 17 Loeschung ab","description":"Setzt die Zeile in `public.gdpr_deletion_requests` auf `cancelled` und haelt Zeitpunkt und Abbrechenden fest; geloescht wird nichts. Danach laeuft die Frist nicht weiter — eine neue Loeschung muss ueber `POST /gdpr/delete-request` neu geplant werden.\n\nAbbrechen darf nur, wer den Auftrag gestellt hat: ein fremder Auftrag ergibt 403, eine unbekannte Kennung 404, und ein Auftrag, der nicht mehr `scheduled` ist (bereits ausgefuehrt oder schon abgebrochen), 409. Nebenwirkung: an die hinterlegte Adresse geht eine Bestaetigungsmail — nachrangig, ein Fehlschlag beim Versand aendert die Antwort nicht.\n\nOhne erreichbare Datenbank kommt `{ status: \"cancelled\", mode: \"demo\" }`. Dann wurde NICHTS abgebrochen, obwohl die Antwort 200 lautet."}},"/api/v1/audit/events":{"get":{"responses":{"200":{"description":"Seite mit Ereignissen — meta.source sagt, ob wirklich gelesen wurde","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":["string","null"]},"action":{"type":"string"},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"oldData":{"description":"Zustand vor der Aenderung; null bei einem Anlegen"},"newData":{"description":"Zustand nach der Aenderung; null bei einem Loeschen"},"ipAddress":{"type":["string","null"]},"userAgent":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"prevHash":{"type":["string","null"],"description":"Verkettung zum vorherigen Ereignis"},"signature":{"type":["string","null"]}},"required":["id","userId","action","entityType","entityId","ipAddress","userAgent","createdAt","prevHash","signature"]}},"total":{"type":"integer","minimum":0,"description":"Zaehlt mit den gesetzten Filtern"},"page":{"type":"integer","minimum":1},"pageSize":{"type":"integer","minimum":1,"maximum":100},"meta":{"type":"object","properties":{"tenantId":{"type":"string","description":"Fehlt, wenn keine Datenbank verbunden war"},"source":{"type":"string","enum":["db","demo"],"description":"demo heisst: es wurde gar nicht gelesen"}},"required":["source"]}},"required":["data","total","page","pageSize","meta"]},"example":{"data":[{"id":"string","userId":"string","action":"string","entityType":"string","entityId":"string","ipAddress":"string","userAgent":"string","createdAt":"string","prevHash":"string","signature":"string"}],"total":0,"page":1,"pageSize":1,"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Admin-Rolle erforderlich"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1AuditEvents","tags":["audit-log"],"parameters":[{"in":"query","name":"tenant","schema":{"type":"string"}},{"in":"query","name":"user","schema":{"type":"string"}},{"in":"query","name":"action","schema":{"type":"string"}},{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"pageSize","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"description":"Protokoll-Ereignisse des Mandanten, seitenweise. Braucht mindestens die Rolle admin. Gelesen wird audit_log im Mandantenschema, neueste zuerst; user, action, from und to grenzen ein. page beginnt bei 1, pageSize nimmt 1 bis 100 an (Vorgabe 25). Ist keine Datenbank verbunden, kommt eine leere Seite mit meta.source=demo. Ein Lesefehler ergibt dagegen 503 und KEINE leere Liste — sonst saehe ein kaputtes Backend fuer den Pruefer aus wie ein leeres Protokoll.","summary":"Protokoll-Ereignisse des Mandanten, seitenweise","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/audit/events/{id}":{"get":{"responses":{"200":{"description":"Das Ereignis mit seiner Kettenposition","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":["string","null"]},"action":{"type":"string"},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"oldData":{"description":"Zustand vor der Aenderung; null bei einem Anlegen"},"newData":{"description":"Zustand nach der Aenderung; null bei einem Loeschen"},"ipAddress":{"type":["string","null"]},"userAgent":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"prevHash":{"type":["string","null"],"description":"Verkettung zum vorherigen Ereignis"},"signature":{"type":["string","null"]},"chainPosition":{"type":"integer","minimum":0,"description":"Das wievielte Ereignis in der zeitlichen Kette — Hilfsangabe, keine Pruefung"}},"required":["id","userId","action","entityType","entityId","ipAddress","userAgent","createdAt","prevHash","signature","chainPosition"]},"example":{"id":"string","userId":"string","action":"string","entityType":"string","entityId":"string","ipAddress":"string","userAgent":"string","createdAt":"string","prevHash":"string","signature":"string","chainPosition":0}}}},"401":{"description":"Unauthorized"},"403":{"description":"Admin-Rolle erforderlich"},"404":{"description":"Ereignis nicht gefunden"},"500":{"description":"Abfrage fehlgeschlagen"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1AuditEventsById","tags":["audit-log"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Ein einzelnes Protokoll-Ereignis samt Position in der Kette. Braucht mindestens die Rolle admin. chainPosition zaehlt, das wievielte Ereignis dieses in der zeitlichen Kette des Mandanten ist — eine Hilfsangabe, keine Aussage darueber, ob die Kette unversehrt ist; dafuer ist POST /audit/verify-chain da. Eine unbekannte Kennung ergibt 404.","summary":"Ein einzelnes Protokoll-Ereignis samt Position in der Kette","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/audit/verify-chain":{"post":{"responses":{"200":{"description":"Pruefergebnis — ok allein genuegt nicht, eventsChecked mitlesen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"eventsChecked":{"type":"integer","minimum":0,"description":"0 heisst: es wurde nichts geprueft"},"firstBreakAt":{"type":"string","description":"Kennung des ersten gebrochenen Ereignisses"},"breakReason":{"type":"string","enum":["missing_signature","bad_prev_hash","bad_signature","db_error","missing_key"]},"durationMs":{"type":"integer","minimum":0}},"required":["ok","eventsChecked","durationMs"]},"example":{"ok":true,"eventsChecked":0,"firstBreakAt":"string","breakReason":"missing_signature","durationMs":0}}}},"401":{"description":"Unauthorized"},"403":{"description":"Admin-Rolle erforderlich"}},"operationId":"postApiV1AuditVerify-chain","tags":["audit-log"],"parameters":[],"description":"Prueft die Hash-Kette des Protokolls auf Unversehrtheit. Braucht mindestens die Rolle admin. Geprueft wird in Stapeln von 500 Ereignissen; from und to grenzen den Zeitraum ein, ohne sie laeuft die Pruefung ueber das ganze Protokoll. Beim ersten Bruch haelt sie an und nennt Ereignis und Grund. ACHTUNG: ist keine Datenbank verbunden, kommt ok=true mit eventsChecked=0 — diese Zahl gehoert mitgelesen, sonst liest sich ein Ausfall wie eine bestandene Pruefung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}}},"example":{"from":"string","to":"string"}}}},"summary":"Prueft die Hash-Kette des Protokolls auf Unversehrtheit","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/audit/export.csv":{"get":{"responses":{"200":{"description":"CSV-Datei als Anhang; bei Abbruch unvollstaendig, aber weiterhin 200","content":{"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Admin-Rolle erforderlich"}},"operationId":"getApiV1AuditExport.csv","tags":["audit-log"],"parameters":[],"description":"Protokoll als CSV-Download fuer die Betriebspruefung. Braucht mindestens die Rolle admin. Die Antwort ist KEIN JSON, sondern text/csv als Anhang: UTF-8 mit BOM, CRLF-Zeilenenden und den festen Spalten timestamp, tenant, user, action, entity, entity_id, diff_summary, hmac — diese Reihenfolge ist fuer den IDEA-Import verbindlich. from und to grenzen den Zeitraum ein, gelesen wird in Seiten zu 500 Zeilen. Bricht das mittendrin ab, endet die Datei mit einer Kommentarzeile \"# export error\" und der Status bleibt 200 — die Datei ist dann unvollstaendig.","summary":"Protokoll als CSV-Download fuer die Betriebspruefung","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/audit/gobd-check":{"get":{"responses":{"200":{"description":"Compliance-Bericht mit allen sieben Prüfpunkten","content":{"application/json":{"schema":{"type":"object","properties":{"overall":{"type":"string","enum":["green","yellow","red"],"description":"Schlechtester Status ueber alle sieben Punkte"},"checks":{"type":"array","items":{"type":"object","properties":{"requirement":{"type":"string","enum":["Unveränderlichkeit","Vollständigkeit","Richtigkeit","Zeitgerechtheit","Ordnung","Storno-Kette","Verfahrensdokumentation"],"description":"Die gepruefte GoBD-Kernanforderung"},"status":{"type":"string","enum":["green","yellow","red"],"description":"green = erfuellt, red = beanstandet. yellow steht fuer ZWEIERLEI: teilweise erfuellt ODER die Pruefung selbst war nicht moeglich (dann sagt es `details`)."},"details":{"type":"string","minLength":1,"description":"Begruendung im Klartext, mit den gezaehlten Werten"}},"required":["requirement","status","details"],"description":"Das Ergebnis einer der sieben Kernanforderungen"},"minItems":7,"maxItems":7,"description":"Immer alle sieben Kernanforderungen"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, fuer den geprueft wurde"},"checkedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"}},"required":["overall","checks","tenantId","checkedAt"]},"example":{"overall":"green","checks":[{"requirement":"Unveränderlichkeit","status":"green","details":"string"},{"requirement":"Unveränderlichkeit","status":"green","details":"string"},{"requirement":"Unveränderlichkeit","status":"green","details":"string"},{"requirement":"Unveränderlichkeit","status":"green","details":"string"},{"requirement":"Unveränderlichkeit","status":"green","details":"string"},{"requirement":"Unveränderlichkeit","status":"green","details":"string"},{"requirement":"Unveränderlichkeit","status":"green","details":"string"}],"tenantId":"string","checkedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AuditGobd-check","tags":["audit-log","gobd"],"parameters":[],"description":"GoBD-Compliance-Bericht: Ampel-Status für alle 7 Kernanforderungen. Jeder Punkt wird einzeln geprüft; scheitert eine Prüfung, steht der Punkt auf „yellow\" mit dem Hinweis „nicht möglich\" — gelb heißt hier also nicht immer „teilweise erfüllt\".","summary":"GoBD-Compliance-Bericht: Ampel-Status für alle 7 Kernanforderungen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/audit/verfahrensdokumentation":{"get":{"responses":{"200":{"description":"Verfahrensdokumentation als text/plain-Anhang, kein JSON","content":{"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Admin-Rolle erforderlich"}},"operationId":"getApiV1AuditVerfahrensdokumentation","tags":["audit-log"],"parameters":[],"summary":"GoBD-Verfahrensdokumentation als strukturierter Text","description":"GoBD-Verfahrensdokumentation als strukturierter Text gemäß BMF-Schreiben 28.11.2019"}},"/api/v1/audit/permissions":{"get":{"responses":{"200":{"description":"Seite mit Berechtigungs-Ereignissen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"action":{"type":"string"},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"userId":{"type":["string","null"]},"description":{"type":["string","null"],"description":"Das Feld _description aus new_data, sonst null"},"createdAt":{"type":["string","null"]}},"required":["id","action","entityType","entityId","userId","description","createdAt"]}},"total":{"type":"integer","minimum":0},"page":{"type":"integer","minimum":1},"pageSize":{"type":"integer","minimum":1,"maximum":100},"meta":{"type":"object","properties":{"tenantId":{"type":"string","description":"Fehlt, wenn keine Datenbank verbunden war"},"source":{"type":"string","enum":["db","demo"],"description":"demo heisst: es wurde gar nicht gelesen"}},"required":["source"]}},"required":["data","total","page","pageSize","meta"]},"example":{"data":[{"id":"string","action":"string","entityType":"string","entityId":"string","userId":"string","description":"string","createdAt":"string"}],"total":0,"page":1,"pageSize":1,"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Admin-Rolle erforderlich"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1AuditPermissions","tags":["audit-log","permissions"],"parameters":[{"in":"query","name":"user","schema":{"type":"string"}},{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"pageSize","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"description":"Permission-Änderungsprotokoll — alle Berechtigungs-Audit-Events des Tenants. Braucht mindestens die Rolle admin. Gelesen wird audit_log im Mandantenschema, eingegrenzt auf die bekannten permission-Aktionen und neueste zuerst; user, from und to grenzen weiter ein. page beginnt bei 1, pageSize nimmt 1 bis 100 an (Vorgabe 25). Die Antwort ist eine schmale Form OHNE oldData und newData. Ist keine Datenbank verbunden, kommt eine leere Seite mit meta.source=demo; ein Lesefehler dagegen 503.","summary":"Permission-Änderungsprotokoll — alle Berechtigungs-Audit-Events des Tenants","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/datev/export":{"get":{"responses":{"200":{"description":"ZIP-Archiv mit EXTF-CSVs. Kopfzeilen: X-DATEV-Type, X-DATEV-Row-Count, X-DATEV-File-Size, X-DATEV-Kontenrahmen, ggf. X-DATEV-Truncated.","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Ungültige Parameter — zwei Formen, nur eine trägt `details`","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Invalid query","description":"Ein Abfrageparameter fehlt oder hat das falsche Format"},"details":{"type":"object","properties":{"formErrors":{"type":"array","items":{"type":"string"},"description":"Beanstandungen ohne Feldbezug"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Beanstandungen je Feldname"}},"required":["formErrors","fieldErrors"],"description":"Aufgeschluesselte Zod-Beanstandungen"}},"required":["error","details"],"description":"Parameter unbrauchbar"},{"type":"object","properties":{"error":{"type":"string","const":"`from` must be on or before `to`","description":"Der Beginn des Zeitraums liegt nach dem Ende"}},"required":["error"],"description":"Zeitraum verdreht"}]}}}},"401":{"description":"Unauthorized — kein Mandant im Aufrufkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"No tenant context","description":"Kein Mandant im Aufrufkontext — englischer Klartext, keine Kennung"}},"required":["error"]}}}},"500":{"description":"Export nicht erzeugt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["Failed to generate DATEV export","Failed to load mapping","Failed to save mapping","Failed to load export log"],"description":"Grund als englischer Klartext, nicht als auswertbare Kennung"},"details":{"type":"string","description":"Meldung der zugrunde liegenden Ausnahme — sie geht an den Aufrufer hinaus"}},"required":["error","details"]}}}},"503":{"description":"Datenbank nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB unavailable","description":"Datenbank nicht erreichbar — englischer Klartext, keine Kennung"}},"required":["error"]}}}}},"operationId":"getApiV1DatevExport","tags":["datev"],"parameters":[],"description":"DATEV-Export als ZIP. type=buchungen|stammdaten|alle. Der Erfolgsfall liefert ein ZIP, keinen JSON-Rumpf. Reißt der Debitoren-Stapel die Zeilenobergrenze, ist der Export UNVOLLSTÄNDIG — erkennbar nur am Kopf `X-DATEV-Truncated: true`; der Statuscode bleibt 200.","summary":"DATEV-Export als ZIP","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"200":{"description":"ZIP-Archiv mit EXTF-CSVs. Kopfzeilen: X-DATEV-Type, X-DATEV-Row-Count, X-DATEV-File-Size, X-DATEV-Kontenrahmen, ggf. X-DATEV-Truncated.","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Ungültige Parameter — zwei Formen, nur eine trägt `details`","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Invalid query","description":"Ein Abfrageparameter fehlt oder hat das falsche Format"},"details":{"type":"object","properties":{"formErrors":{"type":"array","items":{"type":"string"},"description":"Beanstandungen ohne Feldbezug"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Beanstandungen je Feldname"}},"required":["formErrors","fieldErrors"],"description":"Aufgeschluesselte Zod-Beanstandungen"}},"required":["error","details"],"description":"Parameter unbrauchbar"},{"type":"object","properties":{"error":{"type":"string","const":"`from` must be on or before `to`","description":"Der Beginn des Zeitraums liegt nach dem Ende"}},"required":["error"],"description":"Zeitraum verdreht"}]}}}},"401":{"description":"Unauthorized — kein Mandant im Aufrufkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"No tenant context","description":"Kein Mandant im Aufrufkontext — englischer Klartext, keine Kennung"}},"required":["error"]}}}},"500":{"description":"Export nicht erzeugt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["Failed to generate DATEV export","Failed to load mapping","Failed to save mapping","Failed to load export log"],"description":"Grund als englischer Klartext, nicht als auswertbare Kennung"},"details":{"type":"string","description":"Meldung der zugrunde liegenden Ausnahme — sie geht an den Aufrufer hinaus"}},"required":["error","details"]}}}},"503":{"description":"Datenbank nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB unavailable","description":"Datenbank nicht erreichbar — englischer Klartext, keine Kennung"}},"required":["error"]}}}}},"operationId":"postApiV1DatevExport","tags":["datev"],"parameters":[],"description":"DATEV-Export als ZIP (POST). type=buchungen|stammdaten|alle. Identisch zu GET /datev/export — die Parameter werden auch hier aus der Query gelesen, nicht aus dem Rumpf. Der Erfolgsfall liefert ein ZIP, keinen JSON-Rumpf.","summary":"DATEV-Export als ZIP (POST)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/datev/account-mapping":{"get":{"responses":{"200":{"description":"Mapping-Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"entity_type":{"type":"string","enum":["customer","vendor"],"description":"Kunde oder Lieferant"},"entity_id":{"type":"string","minLength":1,"description":"Kennung des Kunden bzw. Lieferanten in Nemix"},"datev_kto_no":{"type":"integer","exclusiveMinimum":0,"description":"DATEV-Kontonummer — Debitoren ab 10000, Kreditoren ab 70000"},"created_at":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Zuordnung"},"updated_at":{"type":"string","format":"date-time","description":"Letzte Aenderung der Zuordnung"}},"required":["entity_type","entity_id","datev_kto_no","created_at","updated_at"],"description":"Eine Zuordnung von Nemix-Stammdatensatz zu DATEV-Konto"},"description":"Alle Zuordnungen des Mandanten, nach Typ und Kontonummer sortiert"},"count":{"type":"integer","minimum":0,"description":"Anzahl der Eintraege in `data` — kein Gesamtzaehler"}},"required":["data","count"]},"example":{"data":[{"entity_type":"customer","entity_id":"string","datev_kto_no":1,"created_at":"2026-01-01T12:00:00.000Z","updated_at":"2026-01-01T12:00:00.000Z"}],"count":0}}}},"401":{"description":"Unauthorized — kein Mandant im Aufrufkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"No tenant context","description":"Kein Mandant im Aufrufkontext — englischer Klartext, keine Kennung"}},"required":["error"]}}}},"500":{"description":"Mapping nicht ladbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["Failed to generate DATEV export","Failed to load mapping","Failed to save mapping","Failed to load export log"],"description":"Grund als englischer Klartext, nicht als auswertbare Kennung"},"details":{"type":"string","description":"Meldung der zugrunde liegenden Ausnahme — sie geht an den Aufrufer hinaus"}},"required":["error","details"]}}}},"503":{"description":"Datenbank nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB unavailable","description":"Datenbank nicht erreichbar — englischer Klartext, keine Kennung"}},"required":["error"]}}}}},"operationId":"getApiV1DatevAccount-mapping","tags":["datev"],"parameters":[],"description":"Aktuelles DATEV-Konto-Mapping (Debitoren ab 10000, Kreditoren ab 70000). Es gibt keine Seitenaufteilung: `count` zählt die gelieferten Einträge, nicht mehr.","summary":"Aktuelles DATEV-Konto-Mapping (Debitoren ab 10000, Kreditoren ab 70000)","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"200":{"description":"Mapping gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Zuordnung wurde angelegt oder ueberschrieben"},"entity_type":{"type":"string","enum":["customer","vendor"],"description":"Kunde oder Lieferant"},"entity_id":{"type":"string","minLength":1,"description":"Kennung des Kunden bzw. Lieferanten in Nemix"},"datev_kto_no":{"type":"integer","exclusiveMinimum":0,"description":"Die gesetzte DATEV-Kontonummer"}},"required":["ok","entity_type","entity_id","datev_kto_no"]},"example":{"ok":true,"entity_type":"customer","entity_id":"string","datev_kto_no":1}}}},"400":{"description":"Ungültiger Body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Invalid body","description":"Der Rumpf entspricht nicht dem erwarteten Schema"},"details":{"type":"object","properties":{"formErrors":{"type":"array","items":{"type":"string"},"description":"Beanstandungen ohne Feldbezug"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Beanstandungen je Feldname"}},"required":["formErrors","fieldErrors"],"description":"Aufgeschluesselte Zod-Beanstandungen"}},"required":["error","details"]}}}},"401":{"description":"Unauthorized — kein Mandant im Aufrufkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"No tenant context","description":"Kein Mandant im Aufrufkontext — englischer Klartext, keine Kennung"}},"required":["error"]}}}},"500":{"description":"Mapping nicht gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["Failed to generate DATEV export","Failed to load mapping","Failed to save mapping","Failed to load export log"],"description":"Grund als englischer Klartext, nicht als auswertbare Kennung"},"details":{"type":"string","description":"Meldung der zugrunde liegenden Ausnahme — sie geht an den Aufrufer hinaus"}},"required":["error","details"]}}}},"503":{"description":"Datenbank nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB unavailable","description":"Datenbank nicht erreichbar — englischer Klartext, keine Kennung"}},"required":["error"]}}}}},"operationId":"postApiV1DatevAccount-mapping","tags":["datev"],"parameters":[],"description":"DATEV-Konto-Mapping setzen oder überschreiben. Antwortet mit 200, nicht mit 201 — auch wenn die Zuordnung neu angelegt wurde. Zurück kommen die gesetzten Werte, nicht der gespeicherte Datensatz (ohne Zeitstempel).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity_type":{"type":"string","enum":["customer","vendor"]},"entity_id":{"type":"string","minLength":1},"datev_kto_no":{"type":"integer","exclusiveMinimum":0}},"required":["entity_type","entity_id","datev_kto_no"]},"example":{"entity_type":"customer","entity_id":"string","datev_kto_no":1}}}},"summary":"DATEV-Konto-Mapping setzen oder überschreiben","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/datev/export-log":{"get":{"responses":{"200":{"description":"Export-Log, neueste zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Protokolleintrags"},"type":{"type":"string","enum":["buchungen","stammdaten","alle"],"description":"Umfang des Exports"},"period_from":{"type":"string","format":"date-time","description":"Beginn des exportierten Zeitraums; DATE, kommt als Zeitstempel"},"period_to":{"type":"string","format":"date-time","description":"Ende des exportierten Zeitraums; DATE, kommt als Zeitstempel"},"file_size":{"type":"string","description":"Groesse des erzeugten ZIP in Bytes — BIGINT, kommt als Zeichenkette"},"row_count":{"type":"integer","minimum":0,"description":"Anzahl exportierter Zeilen ueber alle CSV-Dateien"},"created_at":{"type":"string","format":"date-time","description":"Zeitpunkt des Exports"}},"required":["id","type","period_from","period_to","file_size","row_count","created_at"],"description":"Ein protokollierter DATEV-Export"},"maxItems":50,"description":"Die letzten 50 Exporte, neueste zuerst — aeltere sind nicht abrufbar"},"count":{"type":"integer","minimum":0,"maximum":50,"description":"Anzahl der Eintraege in `data` — kein Gesamtzaehler"}},"required":["data","count"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","type":"buchungen","period_from":"2026-01-01T12:00:00.000Z","period_to":"2026-01-01T12:00:00.000Z","file_size":"string","row_count":0,"created_at":"2026-01-01T12:00:00.000Z"}],"count":0}}}},"401":{"description":"Unauthorized — kein Mandant im Aufrufkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"No tenant context","description":"Kein Mandant im Aufrufkontext — englischer Klartext, keine Kennung"}},"required":["error"]}}}},"500":{"description":"Log nicht ladbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["Failed to generate DATEV export","Failed to load mapping","Failed to save mapping","Failed to load export log"],"description":"Grund als englischer Klartext, nicht als auswertbare Kennung"},"details":{"type":"string","description":"Meldung der zugrunde liegenden Ausnahme — sie geht an den Aufrufer hinaus"}},"required":["error","details"]}}}},"503":{"description":"Datenbank nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB unavailable","description":"Datenbank nicht erreichbar — englischer Klartext, keine Kennung"}},"required":["error"]}}}}},"operationId":"getApiV1DatevExport-log","tags":["datev"],"parameters":[],"description":"Letzte 50 DATEV-Exporte (Log). Ältere Einträge sind über diesen Endpunkt nicht erreichbar — es gibt keine Seitenaufteilung, und `count` zählt nur die gelieferten Einträge.","summary":"Letzte 50 DATEV-Exporte (Log)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/datev-export":{"post":{"responses":{"200":{"description":"Der Buchungsstapel als CSV-Datei (ISO-8859-15), auch wenn er keine Zeile enthaelt.","content":{"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"Ungueltiger Rumpf — Datum nicht als YYYY-MM-DD."},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext."},"403":{"description":"Rolle unter `manager` oder kein Schreibrecht im Modul `accounting`."},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1Datev-export","tags":["datev"],"parameters":[],"summary":"Buchungsstapel als DATEV-CSV herunterladen","description":"DIE ANTWORT IST EINE DATEI, KEIN JSON. Im Erfolgsfall kommt ein\nDATEV-EXTF-Buchungsstapel als CSV zurueck:\n`Content-Type: text/csv; charset=ISO-8859-15`,\n`Content-Disposition: attachment` und der Dateiname\n`DATEV_<Kontenrahmen>_<von>_<bis>.csv`. Der Kopf\n`X-DATEV-Rows` nennt die Anzahl der Buchungszeilen ohne die zwei\nKopfzeilen. Nur die Fehlerfaelle antworten in JSON.\n\nES WIRD NUR GELESEN. Der Export nimmt die vorhandenen Journalbuchungen\ndes Zeitraums; er bucht nichts, aendert nichts und merkt sich nicht, dass\ner lief. Derselbe Zeitraum laesst sich beliebig oft ziehen, und zwei\nAbrufe nacheinander koennen sich unterscheiden, wenn zwischendurch\ngebucht wurde.\n\nJE JOURNALBUCHUNG ENTSTEHT GENAU EINE ZEILE. `Konto` ist das Sollkonto,\n`Gegenkonto` das Habenkonto, `Umsatz` der Betrag als Absolutwert mit\nKomma, Soll/Haben-Kennzeichen fest `S`. Das `Belegdatum` ist das\nDATEV-Kurzformat TTMM ohne Jahr — der Zeitraum steht im Kopfsatz.\n\nDER BU-SCHLUESSEL BLEIBT LEER. Der Umsatzsteuersatz der Buchung wird\ngelesen, aber nicht in einen DATEV-Steuerschluessel umgesetzt. Wer die\nvolle Steuerzuordnung braucht, nimmt die ausfuehrliche `/datev`-Route.\n\nBerater- und Mandantennummer kommen aus dem Steuerprofil des Mandanten\nund werden auf Ziffern reduziert. Fehlt das Profil oder ist es nicht\nlesbar, bleiben beide Felder im Kopfsatz LEER und der Export laeuft\ntrotzdem — DATEV verlangt sie beim Einlesen dann von Hand.\n\nZEICHENSATZ: die Datei ist ISO-8859-15. Umlaute und das Euro-Zeichen\nueberleben; jedes Zeichen oberhalb davon wird zu `?`. Bei einem leeren\nZeitraum kommt eine Datei mit NUR den zwei Kopfzeilen zurueck, ebenfalls\nmit 200 — `X-DATEV-Rows: 0` ist das Unterscheidungsmerkmal.\n\n`module` grenzt die Herkunft ein: `belege` nimmt nur Buchungen aus\nEingangsrechnungen, Rechnungen und Gutschriften, `finance` nur alles\nuebrige, `all` (Vorgabe) beides.\n\nAb Rolle `manager` fuer den ganzen Router, zusaetzlich greift die\nModul-Wache `accounting`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"to":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"kontenrahmen":{"type":"string","enum":["SKR03","SKR04"],"default":"SKR03"},"module":{"type":"string","enum":["finance","belege","all"],"default":"all"}},"required":["from","to"]},"example":{"from":"2026-01-01","to":"2026-01-01","kontenrahmen":"SKR03","module":"finance"}}}}}},"/api/v1/datev-polish/settings":{"get":{"responses":{"200":{"description":"Gespeicherte Einstellungen — oder Vorgabewerte mit `_defaults: true`","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"tenant_id":{"type":"string","minLength":1,"description":"Mandant, zu dem die Einstellungen gehoeren"},"kontenrahmen":{"type":"string","enum":["SKR03","SKR04"],"description":"Verwendeter Standardkontenrahmen"},"consultant_number":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Beraternummer des Steuerbueros; null wenn nicht hinterlegt"},"client_number":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Mandantennummer beim Steuerbuero; null wenn nicht hinterlegt"},"fiscal_year_start_mmdd":{"type":"string","pattern":"^\\d{4}$","description":"Beginn des Wirtschaftsjahres als MMTT, z. B. \"0101\""},"erloes_kto_19":{"type":"string","pattern":"^\\d{4,8}$","description":"Erloeskonto fuer den Regelsatz 19 %"},"erloes_kto_7":{"type":"string","pattern":"^\\d{4,8}$","description":"Erloeskonto fuer den ermaessigten Satz 7 %"},"erloes_kto_0":{"type":"string","pattern":"^\\d{4,8}$","description":"Erloeskonto fuer steuerfreie Umsaetze"},"we_kto_19":{"type":"string","pattern":"^\\d{4,8}$","description":"Wareneingangskonto fuer den Regelsatz 19 %"},"we_kto_7":{"type":"string","pattern":"^\\d{4,8}$","description":"Wareneingangskonto fuer den ermaessigten Satz 7 %"},"we_kto_0":{"type":"string","pattern":"^\\d{4,8}$","description":"Wareneingangskonto fuer steuerfreie Eingaenge"},"debit_start":{"type":"integer","minimum":10000,"maximum":69999,"description":"Erste Debitorennummer (Kunden), DATEV-Bereich 10000-69999"},"kred_start":{"type":"integer","minimum":70000,"maximum":99999,"description":"Erste Kreditorennummer (Lieferanten), DATEV-Bereich 70000-99999"}},"required":["tenant_id","kontenrahmen","consultant_number","client_number","fiscal_year_start_mmdd","erloes_kto_19","erloes_kto_7","erloes_kto_0","we_kto_19","we_kto_7","we_kto_0","debit_start","kred_start"],"description":"Das DATEV-Kontenmapping des Mandanten"},{"type":"object","properties":{"tenant_id":{"type":"string","minLength":1,"description":"Mandant, zu dem die Einstellungen gehoeren"},"kontenrahmen":{"type":"string","enum":["SKR03","SKR04"],"description":"Verwendeter Standardkontenrahmen"},"consultant_number":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Beraternummer des Steuerbueros; null wenn nicht hinterlegt"},"client_number":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Mandantennummer beim Steuerbuero; null wenn nicht hinterlegt"},"fiscal_year_start_mmdd":{"type":"string","pattern":"^\\d{4}$","description":"Beginn des Wirtschaftsjahres als MMTT, z. B. \"0101\""},"erloes_kto_19":{"type":"string","pattern":"^\\d{4,8}$","description":"Erloeskonto fuer den Regelsatz 19 %"},"erloes_kto_7":{"type":"string","pattern":"^\\d{4,8}$","description":"Erloeskonto fuer den ermaessigten Satz 7 %"},"erloes_kto_0":{"type":"string","pattern":"^\\d{4,8}$","description":"Erloeskonto fuer steuerfreie Umsaetze"},"we_kto_19":{"type":"string","pattern":"^\\d{4,8}$","description":"Wareneingangskonto fuer den Regelsatz 19 %"},"we_kto_7":{"type":"string","pattern":"^\\d{4,8}$","description":"Wareneingangskonto fuer den ermaessigten Satz 7 %"},"we_kto_0":{"type":"string","pattern":"^\\d{4,8}$","description":"Wareneingangskonto fuer steuerfreie Eingaenge"},"debit_start":{"type":"integer","minimum":10000,"maximum":69999,"description":"Erste Debitorennummer (Kunden), DATEV-Bereich 10000-69999"},"kred_start":{"type":"integer","minimum":70000,"maximum":99999,"description":"Erste Kreditorennummer (Lieferanten), DATEV-Bereich 70000-99999"},"_defaults":{"type":"boolean","const":true,"description":"Steht nur hier: es wurde nichts gespeichert, das sind die Vorgabewerte"}},"required":["tenant_id","kontenrahmen","consultant_number","client_number","fiscal_year_start_mmdd","erloes_kto_19","erloes_kto_7","erloes_kto_0","we_kto_19","we_kto_7","we_kto_0","debit_start","kred_start","_defaults"],"description":"Vorgabewerte, weil fuer diesen Mandanten nichts hinterlegt ist"}]},"example":{"tenant_id":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","kontenrahmen":"SKR03","consultant_number":123456,"client_number":78901,"fiscal_year_start_mmdd":"0101","erloes_kto_19":"8400","erloes_kto_7":"8300","erloes_kto_0":"8200","we_kto_19":"3400","we_kto_7":"3300","we_kto_0":"3200","debit_start":10000,"kred_start":70000}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht verfügbar (Klartext)"}},"operationId":"getApiV1Datev-polishSettings","tags":["datev","settings"],"parameters":[],"description":"Per-tenant DATEV-Konto-Mapping + Beraternummer/Mandantennummer. Hat der Mandant nichts gespeichert, kommen die Vorgabewerte aus dem Code — erkennbar am zusätzlichen Feld `_defaults: true`, das im gespeicherten Fall FEHLT.","summary":"Per-tenant DATEV-Konto-Mapping + Beraternummer/Mandantennummer","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Gespeicherter Stand nach dem Schreiben","content":{"application/json":{"schema":{"type":"object","properties":{"tenant_id":{"type":"string","minLength":1,"description":"Mandant, zu dem die Einstellungen gehoeren"},"kontenrahmen":{"type":"string","enum":["SKR03","SKR04"],"description":"Verwendeter Standardkontenrahmen"},"consultant_number":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Beraternummer des Steuerbueros; null wenn nicht hinterlegt"},"client_number":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Mandantennummer beim Steuerbuero; null wenn nicht hinterlegt"},"fiscal_year_start_mmdd":{"type":"string","pattern":"^\\d{4}$","description":"Beginn des Wirtschaftsjahres als MMTT, z. B. \"0101\""},"erloes_kto_19":{"type":"string","pattern":"^\\d{4,8}$","description":"Erloeskonto fuer den Regelsatz 19 %"},"erloes_kto_7":{"type":"string","pattern":"^\\d{4,8}$","description":"Erloeskonto fuer den ermaessigten Satz 7 %"},"erloes_kto_0":{"type":"string","pattern":"^\\d{4,8}$","description":"Erloeskonto fuer steuerfreie Umsaetze"},"we_kto_19":{"type":"string","pattern":"^\\d{4,8}$","description":"Wareneingangskonto fuer den Regelsatz 19 %"},"we_kto_7":{"type":"string","pattern":"^\\d{4,8}$","description":"Wareneingangskonto fuer den ermaessigten Satz 7 %"},"we_kto_0":{"type":"string","pattern":"^\\d{4,8}$","description":"Wareneingangskonto fuer steuerfreie Eingaenge"},"debit_start":{"type":"integer","minimum":10000,"maximum":69999,"description":"Erste Debitorennummer (Kunden), DATEV-Bereich 10000-69999"},"kred_start":{"type":"integer","minimum":70000,"maximum":99999,"description":"Erste Kreditorennummer (Lieferanten), DATEV-Bereich 70000-99999"}},"required":["tenant_id","kontenrahmen","consultant_number","client_number","fiscal_year_start_mmdd","erloes_kto_19","erloes_kto_7","erloes_kto_0","we_kto_19","we_kto_7","we_kto_0","debit_start","kred_start"],"description":"Das DATEV-Kontenmapping des Mandanten"}}}},"400":{"description":"Bad Request"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht verfügbar (Klartext)"}},"operationId":"putApiV1Datev-polishSettings","tags":["datev","settings"],"parameters":[],"description":"Upsert DATEV-Settings für den aktuellen Tenant. ACHTUNG — das ist kein Teil-Update: nicht mitgeschickte Felder werden auf ihre Vorgabewerte ZURÜCKGESETZT, nicht beibehalten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kontenrahmen":{"type":"string","enum":["SKR03","SKR04"]},"consultant_number":{"type":["integer","null"],"exclusiveMinimum":0},"client_number":{"type":["integer","null"],"exclusiveMinimum":0},"fiscal_year_start_mmdd":{"type":"string","pattern":"^\\d{4}$"},"erloes_kto_19":{"type":"string","pattern":"^\\d{4,8}$"},"erloes_kto_7":{"type":"string","pattern":"^\\d{4,8}$"},"erloes_kto_0":{"type":"string","pattern":"^\\d{4,8}$"},"we_kto_19":{"type":"string","pattern":"^\\d{4,8}$"},"we_kto_7":{"type":"string","pattern":"^\\d{4,8}$"},"we_kto_0":{"type":"string","pattern":"^\\d{4,8}$"},"debit_start":{"type":"integer","minimum":10000,"maximum":69999},"kred_start":{"type":"integer","minimum":70000,"maximum":99999}}},"example":{"kontenrahmen":"SKR03","consultant_number":1,"client_number":1,"debit_start":10000,"kred_start":70000}}}},"summary":"Upsert DATEV-Settings für den aktuellen Tenant","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/datev-polish/belege":{"get":{"responses":{"200":{"description":"ZIP-Archiv (application/zip). Kopfzeilen: X-DATEV-Beleg-Count, X-DATEV-Originals-Missing, X-DATEV-Period.","content":{"application/zip":{"schema":{"type":"string"}}}},"400":{"description":"Zeitraum unbrauchbar. Zwei Formen: `invalid_query` mit aufgeschlüsselten Beanstandungen, oder `from_after_to` ohne weitere Angaben.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"invalid_query","description":"Fehlerkennung: from oder to fehlt oder hat nicht das Format YYYY-MM-DD"},"details":{"type":"object","properties":{"formErrors":{"type":"array","items":{"type":"string"},"description":"Beanstandungen ohne Feldbezug"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Beanstandungen je Feldname"}},"required":["formErrors","fieldErrors"],"description":"Aufgeschluesselte Zod-Beanstandungen"}},"required":["error","details"]},{"type":"object","properties":{"error":{"type":"string","const":"from_after_to","description":"Fehlerkennung: der Beginn liegt nach dem Ende"}},"required":["error"]}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Datev-polishBelege","tags":["datev","Belege"],"parameters":[],"description":"DATEV-Beleg-ZIP: index.xml + belege/<nr>.pdf für den Steuerberater. Der Erfolgsfall liefert ein ZIP, keinen JSON-Rumpf. ACHTUNG — die Originaldateien fehlen heute IMMER: keine Migration legt die Spalten `document_url` oder `document_blob` an, deshalb enthält jede Datei unter belege/ nur einen Platzhalter mit der Zeichenfolge \"Nemix placeholder\". Wie viele das sind, steht maschinenlesbar im Kopf `X-DATEV-Originals-Missing` und im Klartext in der README.txt des Archivs.","summary":"DATEV-Beleg-ZIP: index.xml + belege/<nr>.pdf für den Steuerberater","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/datev-polish/validate-xml":{"post":{"responses":{"200":{"description":"XML ist wohlgeformt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"Gleichbedeutend mit wellFormed — es gibt hier keine weitere Pruefung"},"wellFormed":{"type":"boolean","description":"true, wenn der Tag-Stapel aufgeht. Das ist KEINE Pruefung gegen das DATEV-XSD."},"rootTag":{"type":["string","null"],"minLength":1,"description":"Name des Wurzelelements; null wenn keins gefunden wurde"},"belegCount":{"type":"integer","minimum":0,"description":"Anzahl der <Beleg>-Elemente im Dokument"},"warnings":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"description":"Beanstandungen im Klartext. Der erste Eintrag steht IMMER da und nennt die Grenze dieses Stubs — eine leere Liste gibt es nicht."}},"required":["ok","wellFormed","rootTag","belegCount","warnings"],"description":"Ergebnis der Stub-Pruefung — Wohlgeformtheit und Belegzaehler, kein Schema-Abgleich"},"example":{"ok":true,"wellFormed":true,"rootTag":"string","belegCount":0,"warnings":["string"]}}}},"400":{"description":"Bad Request"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"XML ist nicht wohlgeformt — gleicher Rumpf wie bei 200, `wellFormed` ist false","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"Gleichbedeutend mit wellFormed — es gibt hier keine weitere Pruefung"},"wellFormed":{"type":"boolean","description":"true, wenn der Tag-Stapel aufgeht. Das ist KEINE Pruefung gegen das DATEV-XSD."},"rootTag":{"type":["string","null"],"minLength":1,"description":"Name des Wurzelelements; null wenn keins gefunden wurde"},"belegCount":{"type":"integer","minimum":0,"description":"Anzahl der <Beleg>-Elemente im Dokument"},"warnings":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"description":"Beanstandungen im Klartext. Der erste Eintrag steht IMMER da und nennt die Grenze dieses Stubs — eine leere Liste gibt es nicht."}},"required":["ok","wellFormed","rootTag","belegCount","warnings"],"description":"Ergebnis der Stub-Pruefung — Wohlgeformtheit und Belegzaehler, kein Schema-Abgleich"}}}}},"operationId":"postApiV1Datev-polishValidate-xml","tags":["datev"],"parameters":[],"description":"DATEV-XML-Validator (Stub) — Well-formedness + Belegzähler. Es findet KEINE XSD-Schema-Validierung statt; dafür wäre mustangproject-CLI oder libxml2 nötig. Der Statuscode trägt das Ergebnis: 200 bei wohlgeformtem XML, 422 sonst — der Rumpf ist in beiden Fällen derselbe.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"xml":{"type":"string","minLength":1,"maxLength":2000000}},"required":["xml"]},"example":{"xml":"string"}}}},"summary":"DATEV-XML-Validator (Stub) — Well-formedness + Belegzähler","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/webhooks/n8n/subscribe":{"post":{"responses":{"201":{"description":"Subscription created or refreshed - contains the secret","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Id of the subscription row"},"event_type":{"type":"string","description":"The Nemix event this callback is subscribed to"},"callback_url":{"type":"string","description":"The stored n8n callback URL"},"secret":{"type":"string","description":"Shared secret, returned ONLY here - it is never readable again through the list endpoint"},"is_active":{"type":"boolean","description":"Whether deliveries go out to this callback"},"created_at":{"type":"string","description":"When the row was first created - unchanged when an existing row is refreshed"}},"required":["id","event_type","callback_url","secret","is_active","created_at"],"description":"The created (or refreshed) subscription"},"example":{"id":"string","event_type":"string","callback_url":"string","secret":"string","is_active":true,"created_at":"string"}}}},"400":{"description":"Invalid body or unknown event_type"},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1WebhooksN8nSubscribe","tags":["webhooks","n8n"],"parameters":[],"summary":"Subscribe an n8n trigger-node callback URL to a Nemix event","description":"Writes a row to public.n8n_subscriptions. Admin role only, because a stored callback URL is a data-exfiltration surface. The `event_type` must be one of the catalog entries from GET /events, otherwise 400 `unknown_event_type` with the allowed list. The URL is checked before it is stored (HTTPS only, no loopback, private, link-local or metadata address) - a rejected target yields 400 `invalid_callback_url`. Subscribing the same tenant, event and URL again does NOT create a second row: it rotates the secret and reactivates the existing one. Without `secret` the server generates one, and it is returned in this response ONLY - the list endpoint never shows it again.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"event_type":{"type":"string","minLength":3,"maxLength":80},"callback_url":{"type":"string","format":"uri"},"secret":{"type":"string","minLength":16,"maxLength":256}},"required":["event_type","callback_url"]},"example":{"event_type":"string","callback_url":"https://example.com","secret":"stringxxxxxxxxxx"}}}}},"get":{"responses":{"200":{"description":"Active subscriptions of the tenant","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Id of the subscription row"},"event_type":{"type":"string","description":"The subscribed Nemix event"},"callback_url":{"type":"string","description":"The stored n8n callback URL"},"is_active":{"type":"boolean","description":"Always true here - the query filters on active rows"},"last_delivery":{"type":["string","null"],"description":"Timestamp of the last delivery attempt; null if never delivered"},"delivery_count":{"type":"integer","description":"Total delivery attempts so far"},"failure_count":{"type":"integer","description":"How many of those attempts failed"},"created_at":{"type":"string","description":"When the subscription was created"},"updated_at":{"type":"string","description":"When it was last changed"}},"required":["id","event_type","callback_url","is_active","last_delivery","delivery_count","failure_count","created_at","updated_at"]},"description":"Active subscriptions of the current tenant, newest first - the secret is NOT included"},"total":{"type":"integer","description":"Number of rows in `data`; there is no paging"}},"required":["data","total"],"description":"Active n8n subscriptions"},"example":{"data":[{"id":"string","event_type":"string","callback_url":"string","is_active":true,"last_delivery":"string","delivery_count":0,"failure_count":0,"created_at":"string","updated_at":"string"}],"total":0}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1WebhooksN8nSubscribe","tags":["webhooks","n8n"],"parameters":[],"summary":"List active n8n subscriptions for the current tenant","description":"Reads public.n8n_subscriptions for the current tenant, active rows only, newest first - deactivated rows stay in the table but never appear here. The shared secret is deliberately left out; it is only ever returned when the subscription is created. There is no paging and no filter. If the database is unreachable the endpoint answers 200 with an empty list rather than an error."}},"/api/v1/webhooks/n8n/subscribe/{id}":{"delete":{"responses":{"200":{"description":"Subscription deactivated","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"The row was switched to inactive"},"id":{"type":"string","description":"Id that was deactivated"}},"required":["ok","id"],"description":"Deactivation receipt"},"example":{"ok":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"},"503":{"description":"Database unavailable"}},"operationId":"deleteApiV1WebhooksN8nSubscribeById","tags":["webhooks","n8n"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Deactivate an n8n subscription","description":"Sets `is_active` to false for one subscription of the current tenant. The row is kept - this is not a delete, and its delivery counters survive. Resubscribing the same event and URL through POST /subscribe reactivates this very row with a fresh secret. An id belonging to another tenant is treated exactly like an unknown one: 404 `not_found`."}},"/api/v1/webhooks/n8n/events":{"get":{"responses":{"200":{"description":"The catalog of subscribable event types","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"event_type":{"type":"string","description":"A subscribable Nemix event type"}},"required":["event_type"]},"description":"The full catalog - it is a fixed list in code, not a database read"},"total":{"type":"integer","description":"Number of catalog entries"}},"required":["data","total"],"description":"Catalog of subscribable event types"},"example":{"data":[{"event_type":"string"}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1WebhooksN8nEvents","tags":["webhooks","n8n"],"parameters":[],"summary":"List supported n8n event types (catalog)","description":"Returns the fixed catalog of event types POST /subscribe accepts. It is a constant in the API, not a database read, so the answer is the same for every tenant and needs no permissions beyond being signed in. Anything not in this list is rejected at subscribe time."}},"/api/v1/webhooks/n8n/test":{"post":{"responses":{"200":{"description":"Delivery result. A failed dry-run is also a 200 - the caller asked whether the URL works, and the answer \"it does not\" is the result, not an error.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"mode":{"type":"string","const":"callback_url","description":"A one-shot delivery to the URL from the request body"},"event_type":{"type":"string","description":"The event type that was sent"},"delivered":{"type":"boolean","description":"Whether the target answered with a 2xx status"},"status":{"type":"integer","description":"HTTP status the target returned; 0 when the request itself failed"},"error":{"type":"string","description":"Only present when the request failed before a response arrived"}},"required":["mode","event_type","delivered","status"],"description":"Single-URL dry-run result"},{"type":"object","properties":{"mode":{"type":"string","const":"subscriptions","description":"Fan-out to the tenant's own active subscriptions"},"event_type":{"type":"string","description":"The event type that was sent"},"matched":{"type":"integer","description":"How many subscriptions matched the event type"},"delivered":{"type":"integer","description":"How many of them accepted the delivery - a COUNT here, not a boolean"},"failed":{"type":"integer","description":"How many deliveries failed"}},"required":["mode","event_type","matched","delivered","failed"],"description":"Fan-out result"}],"description":"Delivery result - which half you get is decided by whether the body carried a callback_url"},"example":{"mode":"callback_url","event_type":"string","delivered":true,"status":0,"error":"string"}}}},"400":{"description":"Invalid body / unknown event_type / disallowed callback_url"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient role"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1WebhooksN8nTest","tags":["webhooks","n8n"],"parameters":[],"description":"Fire a sample Nemix event to verify n8n wiring. Without a callback_url it fans out to the tenant's own subscriptions; with one it delivers a one-shot dry-run to that (SSRF-validated) URL only.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"event_type":{"type":"string","minLength":3,"maxLength":80},"callback_url":{"type":"string","format":"uri"}}},"example":{"event_type":"string","callback_url":"https://example.com"}}}},"summary":"Fire a sample Nemix event to verify n8n wiring","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/exports/datev/{quarter}":{"get":{"responses":{"200":{"description":"Der Buchungsstapel als CSV-Anhang. Die Zeichensatzangabe im Content-Type folgt `encoding`. Auch ein Stapel mit 0 Zeilen kommt mit 200 — `x-row-count` ist das Unterscheidungsmerkmal.","content":{"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"Quartal ist nicht Q1..Q4, oder der Rumpf ist ungueltig"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"422":{"description":"Die Buchungen liessen sich nicht in einen Stapel wandeln"}},"operationId":"getApiV1ExportsDatevByQuarter","tags":["exports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"quarter","required":true}],"description":"Erzeugt einen DATEV-Buchungsstapel als CSV-Datei. Das Quartal steht im Pfad (`Q1`..`Q4`), alles andere im Rumpf — ja, dieser GET nimmt einen JSON-Rumpf entgegen. WICHTIG: die Buchungen kommen AUS DEM RUMPF, nicht aus der Datenbank. Dieser Aufruf liest nichts; wer ihn ohne Rumpf ruft, bekommt einen gueltigen, aber LEEREN Stapel mit Vorgabewerten fuer Berater- und Mandantennummer. Die Zeichensatz-wahl `encoding` ist ohne Angabe `iso-8859-15`, wie DATEV es erwartet. Zeilenzahl und Umsatzsumme stehen in den Kopfzeilen `x-row-count` und `x-total-umsatz` — nicht im Rumpf. Ab Rolle `accountant`.","summary":"Erzeugt einen DATEV-Buchungsstapel als CSV-Datei","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/exports/elster/ust-va":{"post":{"responses":{"200":{"description":"Die erzeugte Meldung samt Rechenweg und Protokoll. `ok: false` bei gesperrtem oder fehlgeschlagenem Versand — der Status bleibt trotzdem 200.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"Ob die Uebermittlung angenommen wurde"},"mode":{"type":"string","description":"stub | test | production — der angeforderte Modus"},"transferticket":{"type":"string","description":"Quittung der Annahmestelle; im Stub mit `TEST-`-Vorsatz, bei Fehlschlag leer"},"isStub":{"type":"boolean","description":"TRUE heisst: diese Meldung ist NICHT beim Finanzamt angekommen"},"berechnung":{"type":"object","properties":{"ust19":{"type":"number"},"ust7":{"type":"number"},"ust_ig_erwerbe19":{"type":"number"},"ust_summe":{"type":"number"},"vorsteuer":{"type":"number"},"zahllast":{"type":"number","description":"Summe USt minus Vorsteuer; negativ = Erstattung"}},"required":["ust19","ust7","ust_ig_erwerbe19","ust_summe","vorsteuer","zahllast"]},"xmlSize":{"type":"integer","description":"Laenge des XML in Zeichen"},"xml":{"type":"string","description":"Die vollstaendige Meldung als XML"},"log":{"type":"array","items":{"type":"string"},"description":"Die Zeilen des Uebermittlungsprotokolls"}},"required":["ok","mode","transferticket","isStub","berechnung","xmlSize","xml","log"]},"example":{"ok":true,"mode":"string","transferticket":"string","isStub":true,"berechnung":{"ust19":0,"ust7":0,"ust_ig_erwerbe19":0,"ust_summe":0,"vorsteuer":0,"zahllast":0},"xmlSize":0,"xml":"string","log":["string"]}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"422":{"description":"Validation Failed — nichts uebermittelt"}},"operationId":"postApiV1ExportsElsterUst-va","tags":["exports"],"parameters":[],"summary":"Umsatzsteuer-Voranmeldung aus uebergebenen Werten erzeugen","description":"Baut aus den uebergebenen Bemessungsgrundlagen eine Umsatzsteuer-Voranmeldung, rechnet die Steuerbetraege aus und uebermittelt sie. Die Zahlen kommen AUS DEM RUMPF, nicht aus der Buchhaltung — dieser Aufruf liest nichts. Stimmen Steuernummer oder Werte nicht, kommt 422 mit den Einzelbefunden und es wird nichts uebermittelt. `submitMode` ist ohne Angabe `stub`: dann entsteht eine vollstaendige Meldung samt Quittung, die NIRGENDWO ankommt. `production` ist bis zur ERiC-Zertifizierung gesperrt und antwortet mit `ok: false` und den Gruenden im Protokoll — aber mit Status 200. Wer aus einer 200 auf „abgegeben\" schliesst, irrt: dafuer steht `isStub`. Ab Rolle `manager`."}},"/api/v1/data-export/{entity}/export":{"get":{"responses":{"200":{"description":"CSV- oder XLSX-Datei (Attachment). CSV kommt als `text/csv; charset=utf-8` mit BOM, Semikolon als Trenner und de-DE-Formatierung; XLSX als Office-Tabellenblatt. Beide tragen die Zeilenzahl im Kopf `X-Row-Count` — der einzige Weg, sie ohne Oeffnen der Datei zu erfahren. Die Antwort ist die Datei selbst, kein JSON. Jeder Export erzeugt ausserdem einen Eintrag im Aktivitaetsprotokoll.","content":{"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Unbekannte Entity oder ungueltiges Format"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Unzureichende Rolle"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getApiV1Data-exportByEntityExport","tags":["exports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true}],"summary":"Exportiert eine Entitaet als CSV oder XLSX","description":"Exportiert eine Entity (customers, orders, invoices, quotes, deliveries, articles/products, suppliers) als CSV oder XLSX. Tenant-gescopt, Spalten-Whitelist, ohne Zeilen-Cap."}},"/api/v1/marketplace/tools":{"get":{"responses":{"200":{"description":"Liste der Tools","content":{"application/json":{"schema":{"type":"object","properties":{"tools":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"Eindeutige Kennung des Tools im Katalog"},"name":{"type":"string","description":"Anzeigename"},"version":{"type":"string","description":"Version des Tools"},"vendor":{"type":"string","description":"Anbieter"},"category":{"type":"string","enum":["core","verified-partner","community"],"description":"Katalog-Kategorie"},"short_description":{"type":"string","description":"Einzeiler fuer die Kachel"},"long_description_md":{"type":"string","description":"Ausfuehrliche Beschreibung in Markdown"},"screenshots":{"type":"array","items":{"type":"string"},"description":"Pfade zu den Bildschirmfotos"},"demo_url":{"type":["string","null"],"description":"Adresse der Vorfuehrung; null wenn keine existiert"},"embed_url":{"type":["string","null"],"description":"Einbettungspfad; null bei Tools ohne Einbettung"},"pricing":{"type":"string","enum":["free","one-time","subscription"],"description":"Preismodell"},"price_eur_per_month":{"type":["number","null"],"description":"Monatspreis in EUR; null bei kostenfreien Tools"},"required_plan":{"type":"string","enum":["starter","pro","enterprise"],"description":"Mindestens noetiger Tarif"},"required_packs":{"type":"array","items":{"type":"string"},"description":"Branchenpakete, die der Mandant aktiv haben muss"},"required_permissions":{"type":"array","items":{"type":"string"},"description":"Rechte, die die Installation einraeumt"},"webhook_endpoints":{"type":"array","items":{"type":"string"},"description":"Eingangspfade, die das Tool bedient"},"installs_count":{"type":"integer","minimum":0,"description":"Anzahl Installationen"},"avg_rating":{"type":"number","description":"Durchschnittliche Bewertung von 1 bis 5"},"review_count":{"type":"integer","minimum":0,"description":"Anzahl der Bewertungen"}},"required":["slug","name","version","vendor","category","short_description","long_description_md","screenshots","demo_url","embed_url","pricing","price_eur_per_month","required_plan","required_packs","required_permissions","webhook_endpoints","installs_count","avg_rating","review_count"]},"description":"Die gefilterten Tools in der gewaehlten Sortierung"},"total":{"type":"integer","minimum":0,"description":"Anzahl der Treffer — gleich der Laenge von tools, es wird nicht geblaettert"}},"required":["tools","total"]},"example":{"tools":[{"slug":"string","name":"string","version":"string","vendor":"string","category":"core","short_description":"string","long_description_md":"string","screenshots":["string"],"demo_url":"string","embed_url":"string","pricing":"free","price_eur_per_month":0,"required_plan":"starter","required_packs":["string"],"required_permissions":["string"],"webhook_endpoints":["string"],"installs_count":0,"avg_rating":0,"review_count":0}],"total":0}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1MarketplaceTools","tags":["marketplace"],"parameters":[{"in":"query","name":"category","schema":{"type":"string","enum":["core","verified-partner","community"]}},{"in":"query","name":"pricing","schema":{"type":"string","enum":["free","one-time","subscription"]}},{"in":"query","name":"required_plan","schema":{"type":"string","enum":["starter","pro","enterprise"]}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"sort","schema":{"type":"string","enum":["popular","rating","newest","name"],"default":"popular"}}],"description":"Liest den Katalog aus dem Speicher des Prozesses — keine Datenbank im Spiel. `category`, `pricing` und `required_plan` filtern exakt, `search` sucht im Text. `sort` (popular, rating, newest, name; Vorgabe popular) bestimmt die Reihenfolge. Es wird NICHT geblaettert: es kommen immer alle Treffer, und `total` ist deren Anzahl. Welche Tools der Mandant schon installiert hat, steht hier nicht.","summary":"Liest den Katalog aus dem Speicher des Prozesses — keine Datenbank im Spiel","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/marketplace/categories":{"get":{"responses":{"200":{"description":"Belegte Kategorien mit Anzahl","content":{"application/json":{"schema":{"type":"object","properties":{"categories":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string","enum":["core","verified-partner","community"],"description":"Die Kategorie"},"count":{"type":"integer","minimum":1,"description":"Anzahl Tools darin"}},"required":["category","count"]},"description":"Nur Kategorien, in denen mindestens ein Tool steht"}},"required":["categories"]},"example":{"categories":[{"category":"core","count":1}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1MarketplaceCategories","tags":["marketplace"],"parameters":[],"description":"Zaehlt die Tools des Katalogs je Kategorie. Kategorien OHNE Tool fallen heraus — die Liste ist also nicht die feste Menge core, verified-partner, community, sondern nur das, was gerade belegt ist. Nimmt keine Parameter entgegen und liest keine Datenbank.","summary":"Zaehlt die Tools des Katalogs je Kategorie","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/marketplace/tools/{slug}":{"get":{"responses":{"200":{"description":"Tool-Details mit den 20 neuesten Bewertungen","content":{"application/json":{"schema":{"type":"object","properties":{"tool":{"type":"object","properties":{"slug":{"type":"string","description":"Eindeutige Kennung des Tools im Katalog"},"name":{"type":"string","description":"Anzeigename"},"version":{"type":"string","description":"Version des Tools"},"vendor":{"type":"string","description":"Anbieter"},"category":{"type":"string","enum":["core","verified-partner","community"],"description":"Katalog-Kategorie"},"short_description":{"type":"string","description":"Einzeiler fuer die Kachel"},"long_description_md":{"type":"string","description":"Ausfuehrliche Beschreibung in Markdown"},"screenshots":{"type":"array","items":{"type":"string"},"description":"Pfade zu den Bildschirmfotos"},"demo_url":{"type":["string","null"],"description":"Adresse der Vorfuehrung; null wenn keine existiert"},"embed_url":{"type":["string","null"],"description":"Einbettungspfad; null bei Tools ohne Einbettung"},"pricing":{"type":"string","enum":["free","one-time","subscription"],"description":"Preismodell"},"price_eur_per_month":{"type":["number","null"],"description":"Monatspreis in EUR; null bei kostenfreien Tools"},"required_plan":{"type":"string","enum":["starter","pro","enterprise"],"description":"Mindestens noetiger Tarif"},"required_packs":{"type":"array","items":{"type":"string"},"description":"Branchenpakete, die der Mandant aktiv haben muss"},"required_permissions":{"type":"array","items":{"type":"string"},"description":"Rechte, die die Installation einraeumt"},"webhook_endpoints":{"type":"array","items":{"type":"string"},"description":"Eingangspfade, die das Tool bedient"},"installs_count":{"type":"integer","minimum":0,"description":"Anzahl Installationen"},"avg_rating":{"type":"number","description":"Durchschnittliche Bewertung von 1 bis 5"},"review_count":{"type":"integer","minimum":0,"description":"Anzahl der Bewertungen"}},"required":["slug","name","version","vendor","category","short_description","long_description_md","screenshots","demo_url","embed_url","pricing","price_eur_per_month","required_plan","required_packs","required_permissions","webhook_endpoints","installs_count","avg_rating","review_count"]},"reviews":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Bewertung"},"tool_slug":{"type":"string","description":"Bewertetes Tool"},"tenant_id":{"type":"string","description":"Mandant, aus dem die Bewertung stammt"},"user_id":{"type":"string","description":"Anwender, der bewertet hat"},"rating":{"type":"integer","minimum":1,"maximum":5,"description":"Bewertung von 1 bis 5"},"comment":{"type":["string","null"],"description":"Kommentar; null wenn keiner abgegeben wurde"},"flagged":{"type":"boolean","description":"true, wenn die Bewertung gemeldet wurde"},"flag_reason":{"type":["string","null"],"description":"Grund der Meldung; null solange nicht gemeldet"},"created_at":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updated_at":{"type":"string","format":"date-time","description":"Letzte Aenderung"}},"required":["id","tool_slug","tenant_id","user_id","rating","comment","flagged","flag_reason","created_at","updated_at"]},"description":"Hoechstens 20 Bewertungen, neueste zuerst; gemeldete sind nicht dabei"}},"required":["tool","reviews"]},"example":{"tool":{"slug":"string","name":"string","version":"string","vendor":"string","category":"core","short_description":"string","long_description_md":"string","screenshots":["string"],"demo_url":"string","embed_url":"string","pricing":"free","price_eur_per_month":0,"required_plan":"starter","required_packs":["string"],"required_permissions":["string"],"webhook_endpoints":["string"],"installs_count":0,"avg_rating":0,"review_count":0},"reviews":[{"id":"string","tool_slug":"string","tenant_id":"string","user_id":"string","rating":1,"comment":"string","flagged":true,"flag_reason":"string","created_at":"2026-01-01T12:00:00.000Z","updated_at":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Tool mit diesem slug","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"getApiV1MarketplaceToolsBySlug","tags":["marketplace"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"description":"Detail-Daten eines Marketplace-Tools inkl. Reviews abrufen. Die Bewertungen sind auf die 20 neuesten begrenzt; gemeldete (`flagged`) sind nicht dabei, und es gibt keinen Parameter, um weitere nachzuladen.","summary":"Detail-Daten eines Marketplace-Tools inkl. Reviews abrufen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/marketplace/tools/{slug}/install":{"post":{"responses":{"201":{"description":"Tool installiert","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tool_slug":{"type":"string","description":"Das installierte Tool"},"install_id":{"type":"string","description":"Kennung der Installation"},"webhook_endpoints":{"type":"array","items":{"type":"string"},"description":"Eingangspfade, die damit fuer den Mandanten aktiv sind"}},"required":["ok","tool_slug","install_id","webhook_endpoints"]},"example":{"ok":true,"tool_slug":"string","install_id":"string","webhook_endpoints":["string"]}}}},"401":{"description":"Kein Mandanten- oder Anwenderkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"404":{"description":"Kein Tool mit diesem slug","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"tool_slug":{"type":"string","description":"Das angefragte Tool"},"install_id":{"type":"string","description":"Nur bei already_installed: die bestehende Installation"},"error":{"type":"string","enum":["tool_not_found","plan_too_low","missing_pack","already_installed"],"description":"Grund der Ablehnung"}},"required":["ok","tool_slug","error"]}}}},"409":{"description":"Tarif zu niedrig, Branchenpaket fehlt oder bereits installiert","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"tool_slug":{"type":"string","description":"Das angefragte Tool"},"install_id":{"type":"string","description":"Nur bei already_installed: die bestehende Installation"},"error":{"type":"string","enum":["tool_not_found","plan_too_low","missing_pack","already_installed"],"description":"Grund der Ablehnung"}},"required":["ok","tool_slug","error"]}}}}},"operationId":"postApiV1MarketplaceToolsBySlugInstall","tags":["marketplace"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"description":"Installiert ein Tool fuer den Mandanten aus dem Auth-Kontext. Der Aufrufer muss die angefragten Rechte mit `accept_permissions: true` ausdruecklich annehmen. Geprueft wird gegen den im Rumpf UEBERGEBENEN Tarif und die uebergebenen Pakete, nicht gegen den gespeicherten Stand des Mandanten. Reicht der Tarif nicht (`plan_too_low`), fehlt ein Branchenpaket (`missing_pack`) oder ist das Tool schon installiert (`already_installed`, mit der bestehenden `install_id`), kommt 409; ein unbekannter slug ergibt 404. Bei Erfolg werden die Webhook-Eingaenge des Tools fuer den Mandanten freigeschaltet.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["starter","pro","enterprise"]},"active_packs":{"type":"array","items":{"type":"string"},"default":[]},"accept_permissions":{"type":"boolean","const":true}},"required":["plan","accept_permissions"]},"example":{"plan":"starter","active_packs":["string"],"accept_permissions":true}}}},"summary":"Installiert ein Tool fuer den Mandanten aus dem Auth-Kontext","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/marketplace/tools/{slug}/reviews":{"post":{"responses":{"201":{"description":"Bewertung angelegt oder die bestehende ueberschrieben","content":{"application/json":{"schema":{"type":"object","properties":{"review":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Bewertung"},"tool_slug":{"type":"string","description":"Bewertetes Tool"},"tenant_id":{"type":"string","description":"Mandant, aus dem die Bewertung stammt"},"user_id":{"type":"string","description":"Anwender, der bewertet hat"},"rating":{"type":"integer","minimum":1,"maximum":5,"description":"Bewertung von 1 bis 5"},"comment":{"type":["string","null"],"description":"Kommentar; null wenn keiner abgegeben wurde"},"flagged":{"type":"boolean","description":"true, wenn die Bewertung gemeldet wurde"},"flag_reason":{"type":["string","null"],"description":"Grund der Meldung; null solange nicht gemeldet"},"created_at":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updated_at":{"type":"string","format":"date-time","description":"Letzte Aenderung"}},"required":["id","tool_slug","tenant_id","user_id","rating","comment","flagged","flag_reason","created_at","updated_at"]}},"required":["review"]},"example":{"review":{"id":"string","tool_slug":"string","tenant_id":"string","user_id":"string","rating":1,"comment":"string","flagged":true,"flag_reason":"string","created_at":"2026-01-01T12:00:00.000Z","updated_at":"2026-01-01T12:00:00.000Z"}}}}},"400":{"description":"tool_not_found, invalid_rating oder review_failed","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"401":{"description":"Kein Mandanten- oder Anwenderkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"postApiV1MarketplaceToolsBySlugReviews","tags":["marketplace"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"description":"Legt eine Bewertung (1 bis 5, Kommentar freiwillig) fuer ein Tool an. Je Anwender und Tool gibt es GENAU EINE Bewertung: ein zweiter Aufruf ueberschreibt die vorhandene und gibt sie zurueck — ebenfalls mit 201, nicht mit 200. Ein fehlender Kommentar setzt den bestehenden dabei auf null zurueck. Danach werden Durchschnitt und Anzahl am Tool neu gerechnet. Unbekannter slug oder unzulaessige Bewertung ergeben 400 (`tool_not_found` bzw. `invalid_rating`) — NICHT 404. Eine Installation wird nicht vorausgesetzt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":"string","maxLength":2000}},"required":["rating"]},"example":{"rating":1,"comment":"string"}}}},"summary":"Legt eine Bewertung (1 bis 5, Kommentar freiwillig) fuer ein Tool an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/marketplace/reviews/{reviewId}/flag":{"post":{"responses":{"200":{"description":"Die gemeldete Bewertung mit gesetztem flagged und flag_reason","content":{"application/json":{"schema":{"type":"object","properties":{"review":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Bewertung"},"tool_slug":{"type":"string","description":"Bewertetes Tool"},"tenant_id":{"type":"string","description":"Mandant, aus dem die Bewertung stammt"},"user_id":{"type":"string","description":"Anwender, der bewertet hat"},"rating":{"type":"integer","minimum":1,"maximum":5,"description":"Bewertung von 1 bis 5"},"comment":{"type":["string","null"],"description":"Kommentar; null wenn keiner abgegeben wurde"},"flagged":{"type":"boolean","description":"true, wenn die Bewertung gemeldet wurde"},"flag_reason":{"type":["string","null"],"description":"Grund der Meldung; null solange nicht gemeldet"},"created_at":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updated_at":{"type":"string","format":"date-time","description":"Letzte Aenderung"}},"required":["id","tool_slug","tenant_id","user_id","rating","comment","flagged","flag_reason","created_at","updated_at"]}},"required":["review"]},"example":{"review":{"id":"string","tool_slug":"string","tenant_id":"string","user_id":"string","rating":1,"comment":"string","flagged":true,"flag_reason":"string","created_at":"2026-01-01T12:00:00.000Z","updated_at":"2026-01-01T12:00:00.000Z"}}}}},"401":{"description":"Unauthorized"},"404":{"description":"Keine Bewertung mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"postApiV1MarketplaceReviewsByReviewIdFlag","tags":["marketplace"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"reviewId","required":true}],"description":"Setzt `flagged` auf true und haelt den Grund in `flag_reason` fest. Eine gemeldete Bewertung erscheint danach NICHT mehr in der Detailansicht des Tools, und Durchschnitt und Anzahl am Tool werden ohne sie neu gerechnet. Sie wird nicht geloescht. Die Meldung wirkt sofort und ohne Pruefung — jeder angemeldete Aufrufer kann jede Bewertung melden, auch eine bereits gemeldete (der Grund wird dann ueberschrieben). Zurueckgenommen wird sie ueber die Verwaltung, nicht hier.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","minLength":3,"maxLength":500}},"required":["reason"]},"example":{"reason":"string"}}}},"summary":"Setzt `flagged` auf true und haelt den Grund in `flag_reason` fest","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/layers/resolved/{tenant}":{"get":{"responses":{"200":{"description":"Die zusammengefuehrte Konfiguration der ganzen Kette","content":{"application/json":{"schema":{"type":"object","properties":{"tenantLayerId":{"type":["string","null"]},"chain":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["hersteller","land","partner","kunde"]},"slug":{"type":"string"},"parentLayerId":{"type":["string","null"]}},"required":["id","type","slug","parentLayerId"],"additionalProperties":false}},"fields":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"templates":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"layerId":{"type":"string"},"kind":{"type":"string"},"payload":{}},"required":["id","layerId","kind"],"additionalProperties":false}}},"mode":{"type":"string","const":"demo"}},"required":["tenantLayerId","chain","fields","templates"]},"example":{"tenantLayerId":"string","chain":[{"id":"string","type":"hersteller","slug":"string","parentLayerId":"string"}],"fields":{"beispiel":{}},"templates":{"beispiel":[{"id":"string","layerId":"string","kind":"string"}]},"mode":"demo"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1LayersResolvedByTenant","tags":["layers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenant","required":true}],"summary":"Aufgeloeste Ebenen-Konfiguration eines Mandanten","description":"Laeuft die Kette hersteller → land → partner → kunde ab und mischt die Feld-Ueberschreibungen von der allgemeinsten zur speziellsten Ebene. Vorlagen werden je `kind` von der spezielleren Ebene GANZ ersetzt, nicht zusammengefuehrt. Der Pfadwert ist der Mandanten-Slug; er darf vom angemeldeten Mandanten abweichen, sofern die Ebenen-Rolle des Aufrufers hierarchisch darueber steht — sonst 404 statt 403, damit die Existenz fremder Mandanten nicht abfragbar wird. Ohne Datenbank kommt eine leere Huelle mit `mode: \"demo\"`. Widerspricht sich eine Ebene in sich selbst, endet der Aufruf mit 409."}},"/api/v1/layers/definitions":{"post":{"responses":{"201":{"description":"Die angelegte Ebene","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["hersteller","land","partner","kunde"]},"slug":{"type":"string"},"parentLayerId":{"type":["string","null"]},"createdAt":{}},"required":["id","type","slug","parentLayerId"],"additionalProperties":false},"example":{"id":"string","type":"hersteller","slug":"string","parentLayerId":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1LayersDefinitions","tags":["layers"],"parameters":[],"summary":"Ebenen-Definition anlegen","description":"Legt eine Zeile in `layer_definition` an und gibt sie mit 201 zurueck. Ueber die Schema-Pruefung hinaus gelten zwei Formregeln: eine `hersteller`-Ebene darf KEINE Elternebene haben, jede andere Art MUSS eine haben. Die Elternebene wird vorab nachgeschlagen, damit ein Tippfehler 400 ergibt und keinen Datenbankfehler. Das Paar aus `type` und `slug` ist eindeutig; ein zweiter Versuch endet mit 409. Ein Mandant der Produktionsinstanz kann diese Route nicht direkt aufrufen (403).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["hersteller","land","partner","kunde"]},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]*$","minLength":1,"maxLength":63},"parentLayerId":{"type":["string","null"],"format":"uuid"}},"required":["type","slug"]},"example":{"type":"hersteller","slug":"00000000-0000-4000-8000-000000000000","parentLayerId":"00000000-0000-4000-8000-000000000000"}}}}},"get":{"responses":{"200":{"description":"Alle passenden Ebenen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["hersteller","land","partner","kunde"]},"slug":{"type":"string"},"parentLayerId":{"type":["string","null"]},"createdAt":{}},"required":["id","type","slug","parentLayerId"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","type":"hersteller","slug":"string","parentLayerId":"string"}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1LayersDefinitions","tags":["layers"],"parameters":[{"in":"query","name":"type","schema":{"type":"string","enum":["hersteller","land","partner","kunde"]}}],"summary":"Ebenen-Definitionen auflisten","description":"Liest `layer_definition`, wahlweise auf eine Art eingeschraenkt (`?type=hersteller|land|partner|kunde`). Ohne Sortierung und ohne Blaetterung — die Tabelle ist plattformweit und klein. Neben der Ebenen-Rolle verlangt der Handler zusaetzlich die Rolle Admin. Die Liste ist auf die Ebenen beschraenkt, die fuer den Aufrufer bestimmt sind: eine Mandanten-Rolle sieht ausschliesslich die eigene Ebene, eine hierarchisch hoehere Rolle die Arten aus ihrer Rechte-Matrix. Sie ist damit KEIN vollstaendiger Plattform-Bestand mehr."}},"/api/v1/layers/definitions/{id}":{"get":{"responses":{"200":{"description":"Die Ebene","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["hersteller","land","partner","kunde"]},"slug":{"type":"string"},"parentLayerId":{"type":["string","null"]},"createdAt":{}},"required":["id","type","slug","parentLayerId"],"additionalProperties":false},"example":{"id":"string","type":"hersteller","slug":"string","parentLayerId":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1LayersDefinitionsById","tags":["layers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Ebenen-Definition lesen","description":"Liest genau eine Zeile aus `layer_definition` ueber die id im Pfad; eine unbekannte id ergibt 404. Ueberschreibungen und Vorlagen der Ebene stehen NICHT in der Antwort — dafuer gibt es /definitions/{id}/overrides und /resolved/{tenant}. Neben der Ebenen-Rolle verlangt der Handler zusaetzlich die Rolle Admin. Eine Ebene, die weder dem eigenen Mandanten gehoert noch nach der Rechte-Matrix fuer die Rolle des Aufrufers bestimmt ist, antwortet ebenfalls 404 und nicht 403 — sonst bestaetigte die Antwort die Existenz einer fremden id."},"delete":{"responses":{"200":{"description":"Die geloeschten Ebenen-ids","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"array","items":{"type":"string"}}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":["string"]}}}},"401":{"description":"Unauthorized"}},"operationId":"deleteApiV1LayersDefinitionsById","tags":["layers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ebenen-Definition loeschen","description":"Sammelt die Ebene und alle Nachfahren ueber `parent_layer_id` und loescht sie von unten nach oben — ENDGUELTIG, es gibt hier keinen Soft-Delete. Zeigt noch ein Mandant auf eine dieser Ebenen, bricht die Route mit 409 ab und loescht NICHTS; die Mandanten muessen vorher umgehaengt werden. Die Antwort nennt die geloeschten ids, Wurzel zuerst. Ein Mandant der Produktionsinstanz kann diese Route nicht direkt aufrufen (403)."}},"/api/v1/layers/definitions/{id}/overrides":{"post":{"responses":{"201":{"description":"Die angelegte Ueberschreibung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"layerId":{"type":"string"},"entity":{"type":"string"},"fieldId":{"type":"string"},"value":{},"strict":{"type":"boolean"},"version":{"type":"number"},"createdAt":{}},"required":["id","layerId","entity","fieldId","strict","version"],"additionalProperties":false},"example":{"id":"string","layerId":"string","entity":"string","fieldId":"string","strict":true,"version":0}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1LayersDefinitionsByIdOverrides","tags":["layers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Feld-Ueberschreibung anlegen","description":"Schreibt eine Zeile nach `layer_field_override` (201). Die Ebene wird vorab nachgeschlagen; fehlt sie ODER ist sie nicht fuer den Aufrufer bestimmt, kommt 404. Nach dem Einfuegen prueft der Handler ALLE Ueberschreibungen dieser Ebene gegeneinander und nimmt die frische Zeile bei einem Widerspruch wieder zurueck — die Antwort ist dann 409 und gespeichert ist nichts. `value` liegt als JSONB und nimmt jede JSON-Form an.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","minLength":1,"maxLength":128},"fieldId":{"type":"string","minLength":1,"maxLength":255},"value":{},"strict":{"type":"boolean","default":false},"version":{"type":"integer","exclusiveMinimum":0,"default":1}},"required":["entity","fieldId"]},"example":{"entity":"string","fieldId":"string","strict":true,"version":1}}}}},"get":{"responses":{"200":{"description":"Die Ueberschreibungen dieser einen Ebene","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"layerId":{"type":"string"},"entity":{"type":"string"},"fieldId":{"type":"string"},"value":{},"strict":{"type":"boolean"},"version":{"type":"number"},"createdAt":{}},"required":["id","layerId","entity","fieldId","strict","version"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","layerId":"string","entity":"string","fieldId":"string","strict":true,"version":0}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1LayersDefinitionsByIdOverrides","tags":["layers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Feld-Ueberschreibungen einer Ebene auflisten","description":"Liest alle Zeilen aus `layer_field_override` zu dieser `layer_id` — NUR diese eine Ebene. Geerbte Ueberschreibungen der Elternebenen stehen nicht darin; die zusammengefuehrte Sicht liefert /resolved/{tenant}. Eine Ebene, die es nicht gibt, und eine Ebene, die nicht fuer den Aufrufer bestimmt ist, antworten gleichermassen 404. Frueher ergab eine unbekannte Ebene eine leere Liste; das ist mit der Zugehoerigkeitspruefung entfallen, weil zwei verschiedene Antworten die Existenz einer fremden Ebene verraten haetten."}},"/api/v1/layers/overrides/{overrideId}":{"delete":{"responses":{"200":{"description":"Die geloeschte Ueberschreibung","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"deleteApiV1LayersOverridesByOverrideId","tags":["layers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"overrideId","required":true}],"summary":"Feld-Ueberschreibung entfernen","description":"Loescht eine Zeile aus `layer_field_override` endgueltig; eine unbekannte id ergibt 404. Die Antwort nennt die geloeschte id als EINE Zeichenkette, nicht als Liste. Einen Konflikt-Nachlauf wie beim Anlegen gibt es hier nicht. Ein Mandant der Produktionsinstanz kann diese Route nicht direkt aufrufen (403)."}},"/api/v1/layers/definitions/{id}/promote-to/{tenantId}":{"post":{"responses":{"200":{"description":"Mandant und Ebene, wie sie jetzt verknuepft sind","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"layerId":{"type":"string"}},"required":["tenantId","layerId"],"additionalProperties":false},"example":{"tenantId":"string","layerId":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1LayersDefinitionsByIdPromote-toByTenantId","tags":["layers"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"summary":"Mandanten an eine Kunden-Ebene haengen","description":"Schreibt `tenants.layer_id` auf die Ebene aus dem Pfad. Die Ebene MUSS vom Typ `kunde` sein, sonst 400 — eine hoehere Ebene wuerde die Kette abkuerzen und eine halb zusammengefuehrte Konfiguration liefern. Der zweite Pfadwert darf die Mandanten-Id ODER der Slug sein; gesucht wird erst nach Id, dann nach Slug, und ohne Treffer kommt 404. Der Aufruf ist wiederholbar: dasselbe Paar erneut zu senden aendert nichts. Ein Mandant der Produktionsinstanz kann diese Route nicht direkt aufrufen (403)."}},"/api/v1/sandbox/create":{"post":{"responses":{"200":{"description":"Sandbox vorhanden — created sagt, ob sie in diesem Aufruf entstand","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"sandboxSchema":{"type":"string","description":"Immer tenant_<slug>_sandbox"},"created":{"type":"boolean","description":"false heisst: die Sandbox gab es schon, es wurde nichts kopiert"},"tabellen":{"type":"integer","minimum":0,"description":"Zahl der ausgefuehrten CREATE-Befehle"},"anonymized":{"type":"boolean"}},"required":["ok","sandboxSchema","created","tabellen","anonymized"]},"example":{"ok":true,"sandboxSchema":"string","created":true,"tabellen":0,"anonymized":true}}}},"401":{"description":"Unauthorized"},"403":{"description":"Mandanten-Admin-Rechte erforderlich"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1SandboxCreate","tags":["sandbox"],"parameters":[],"description":"Legt fuer den angemeldeten Mandanten eine Sandbox an. Braucht Mandanten-Admin-Rechte. Mandant und Slug kommen aus dem Auth-Kontext und lassen sich NICHT im Rumpf uebersteuern. Der Aufruf ist idempotent: gibt es das Schema tenant_<slug>_sandbox schon, kommt created=false und es wird nichts kopiert; mit force=true wird es vorher verworfen und neu aufgebaut. Kopiert wird die STRUKTUR (Tabellen und Indizes), nicht der Inhalt — tabellen nennt die Zahl der ausgefuehrten CREATE-Befehle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"anonymized":{"type":"boolean"},"force":{"type":"boolean"}}},"example":{"anonymized":true,"force":true}}}},"summary":"Legt fuer den angemeldeten Mandanten eine Sandbox an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sandbox/instances":{"get":{"responses":{"200":{"description":"Sandboxen des Mandanten, neueste zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"instances":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"sandboxSchema":{"type":"string"},"instanceType":{"type":"string","const":"sandbox"},"anonymized":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}},"required":["tenantId","sandboxSchema","instanceType","anonymized","createdAt"]}}},"required":["instances"]},"example":{"instances":[{"tenantId":"string","sandboxSchema":"string","instanceType":"sandbox","anonymized":true,"createdAt":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1SandboxInstances","tags":["sandbox"],"parameters":[],"description":"Sandboxen des angemeldeten Mandanten. Braucht Mandanten-Admin-Rechte. Gelesen wird public.sandbox_instances, gefiltert auf den eigenen Mandanten und neueste zuerst — dieselbe Quelle, in die POST /sandbox/create schreibt. Es gibt keine Blaetterung, und instanceType ist fest sandbox.","summary":"Sandboxen des angemeldeten Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sandbox/diff":{"get":{"responses":{"200":{"description":"Der gerechnete Aenderungsplan","content":{"application/json":{"schema":{"type":"object","properties":{"fromInstance":{"type":"string","enum":["dev","sandbox","prod"]},"toInstance":{"type":"string","enum":["dev","sandbox","prod"]},"layerType":{"type":"string","enum":["hersteller","landes","partner","kunden"]},"ownerId":{"type":["string","null"]},"operations":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["add","update","remove"]},"path":{"type":"string"},"before":{},"after":{}},"required":["kind","path"]}},"noop":{"type":"boolean","description":"true, wenn Quelle und Ziel gleich sind — nichts zu tun"}},"required":["fromInstance","toInstance","layerType","ownerId","operations","noop"]},"example":{"fromInstance":"dev","toInstance":"dev","layerType":"hersteller","ownerId":"string","operations":[{"kind":"add","path":"string"}],"noop":true}}}},"401":{"description":"Unauthorized"},"403":{"description":"Rolle passend zur Schicht erforderlich"},"404":{"description":"Kein Quellstand fuer diese Schicht und Instanz"},"409":{"description":"Richtung nicht erlaubt"}},"operationId":"getApiV1SandboxDiff","tags":["sandbox"],"parameters":[{"in":"query","name":"from","schema":{"type":"string","enum":["dev","sandbox","prod"]},"required":true},{"in":"query","name":"to","schema":{"type":"string","enum":["dev","sandbox","prod"]},"required":true},{"in":"query","name":"layer","schema":{"type":"string","enum":["hersteller","landes","partner","kunden"]},"required":true},{"in":"query","name":"owner","schema":{"type":"string"},"required":false}],"description":"Zeigt den Unterschied zweier Schichtstaende als Aenderungsplan. Welche Rolle noetig ist, richtet sich nach layer: hersteller verlangt System-, landes Landes-, partner Partner- und kunden Mandanten-Admin. ACHTUNG: gelesen wird ein prozesslokaler Zwischenspeicher, den POST /customization/publish-layer fuellt — nach einem Neustart ist er leer. Fehlt der Quellstand, kommt 404; ist die Richtung nicht erlaubt, 409.","summary":"Zeigt den Unterschied zweier Schichtstaende als Aenderungsplan","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sandbox/promote":{"post":{"responses":{"200":{"description":"Plan samt Auskunft, ob wirklich befoerdert wurde","content":{"application/json":{"schema":{"type":"object","properties":{"promoted":{"type":"boolean","description":"false bei dryRun und bei noop — dann wurde nichts geschrieben"},"plan":{"type":"object","properties":{"fromInstance":{"type":"string","enum":["dev","sandbox","prod"]},"toInstance":{"type":"string","enum":["dev","sandbox","prod"]},"layerType":{"type":"string","enum":["hersteller","landes","partner","kunden"]},"ownerId":{"type":["string","null"]},"operations":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["add","update","remove"]},"path":{"type":"string"},"before":{},"after":{}},"required":["kind","path"]}},"noop":{"type":"boolean","description":"true, wenn Quelle und Ziel gleich sind — nichts zu tun"}},"required":["fromInstance","toInstance","layerType","ownerId","operations","noop"]}},"required":["promoted","plan"]},"example":{"promoted":true,"plan":{"fromInstance":"dev","toInstance":"dev","layerType":"hersteller","ownerId":"string","operations":[{"kind":"add","path":"string"}],"noop":true}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Rolle passend zur Schicht erforderlich"},"404":{"description":"Quellstand fehlt"},"409":{"description":"Richtung nicht erlaubt"}},"operationId":"postApiV1SandboxPromote","tags":["sandbox"],"parameters":[],"description":"Befoerdert einen Schichtstand von einer Instanz in die naechste. Welche Rolle noetig ist, richtet sich nach layer. Erlaubte Richtungen prueft die Flussmatrix, eine unerlaubte ergibt 409; fehlt der Quellstand, 404. Mit dryRun=true wird nur der Plan gerechnet und nichts geschrieben, und auch ein Plan mit noop=true schreibt nichts. ACHTUNG: geschrieben wird in denselben prozesslokalen Zwischenspeicher wie bei /customization/publish-layer — die Befoerderung ueberlebt keinen Neustart und steht in keiner Datenbank.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"layer":{"type":"string","enum":["hersteller","landes","partner","kunden"]},"ownerId":{"type":["string","null"]},"from":{"type":"string","enum":["dev","sandbox","prod"]},"to":{"type":"string","enum":["dev","sandbox","prod"]},"dryRun":{"type":"boolean"}},"required":["layer","from","to"]},"example":{"layer":"hersteller","ownerId":"string","from":"dev","to":"dev","dryRun":true}}}},"summary":"Befoerdert einen Schichtstand von einer Instanz in die naechste","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sandbox/promote-request":{"post":{"responses":{"201":{"description":"Antrag angelegt, Status pending_approval","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"diffPreview":{"type":"object","properties":{"fromInstance":{"type":"string"},"toInstance":{"type":"string"},"layerType":{"type":"string"},"ownerId":{"type":"string"},"operations":{"type":"array","items":{}},"noop":{"type":"boolean"},"note":{"type":"string"}},"required":["fromInstance","toInstance","layerType","ownerId","operations","noop","note"]},"validationResults":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"passed":{"type":"boolean"},"detail":{"type":"string"}},"required":["rule","passed"]}},"signature":{"type":"string","description":"SHA-256 des Antrags als Hex-Zeichenkette"}},"required":["requestId","diffPreview","validationResults","signature"]},"example":{"requestId":"string","diffPreview":{"fromInstance":"string","toInstance":"string","layerType":"string","ownerId":"string","operations":[],"noop":true,"note":"string"},"validationResults":[{"rule":"string","passed":true,"detail":"string"}],"signature":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Rolle fehlt oder Buendel-Signatur ungueltig"},"409":{"description":"Richtung nicht erlaubt"},"500":{"description":"Antrag konnte nicht angelegt werden"}},"operationId":"postApiV1SandboxPromote-request","tags":["instances"],"parameters":[],"description":"Legt einen Befoerderungsantrag sandbox nach prod an. Braucht mindestens die Rolle admin. Geprueft werden die erlaubte Richtung und die HMAC-SHA256-Signatur des Buendels gegen LAYER_BUNDLE_SECRET: eine falsche Signatur ergibt 403, eine unerlaubte Richtung 409. Der Antrag entsteht in public.promotion_requests mit Status pending_approval — befoerdert wird dabei nichts. ACHTUNG: diffPreview ist derzeit ein Platzhalter mit leerer operations-Liste, solange die Ebenen-Versionen nicht gefuellt sind.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sandboxId":{"type":"string","format":"uuid"},"targetInstanceType":{"type":"string","const":"prod"},"bundleSignature":{"type":"string","pattern":"^[0-9a-f]{64}$"},"note":{"type":"string","maxLength":2000}},"required":["sandboxId","targetInstanceType","bundleSignature"]}}}},"summary":"Legt einen Befoerderungsantrag sandbox nach prod an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sandbox/promote-request/{id}":{"get":{"responses":{"200":{"description":"Der Antrag mit seinem aktuellen Stand","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"fromInstanceType":{"type":"string"},"toInstanceType":{"type":"string"},"status":{"type":"string","description":"pending_approval, approved, rejected oder applied"},"diffPreview":{"description":"Der beim Anlegen hinterlegte JSON-Block"},"validationResults":{"description":"Der beim Anlegen hinterlegte JSON-Block"},"note":{"type":["string","null"]},"requestedBy":{"type":"string"},"approvedBy":{"type":["string","null"]},"appliedBy":{"type":["string","null"]},"requestedAt":{"type":"string"},"approvedAt":{"type":["string","null"]},"appliedAt":{"type":["string","null"]}},"required":["id","tenantId","fromInstanceType","toInstanceType","status","note","requestedBy","approvedBy","appliedBy","requestedAt","approvedAt","appliedAt"]},"example":{"id":"string","tenantId":"string","fromInstanceType":"string","toInstanceType":"string","status":"string","note":"string","requestedBy":"string","approvedBy":"string","appliedBy":"string","requestedAt":"string","approvedAt":"string","appliedAt":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Mindestens die Rolle admin erforderlich"},"404":{"description":"Antrag nicht gefunden"},"500":{"description":"Abfrage fehlgeschlagen"}},"operationId":"getApiV1SandboxPromote-requestById","tags":["instances"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Ruft einen Befoerderungsantrag ab. Braucht mindestens die Rolle admin und findet nur Antraege des eigenen Mandanten — ein fremder Antrag ist von einem fehlenden nicht zu unterscheiden, beide ergeben 404. Die Antwort ist camelCase; diffPreview und validationResults kommen unveraendert so heraus, wie sie beim Anlegen hinterlegt wurden.","summary":"Ruft einen Befoerderungsantrag ab","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sandbox/promote-approve/{id}":{"post":{"responses":{"200":{"description":"Antrag freigegeben","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"requestId":{"type":"string"},"status":{"type":"string","const":"approved"},"approvedBy":{"type":"string"}},"required":["ok","requestId","status","approvedBy"]},"example":{"ok":true,"requestId":"string","status":"approved","approvedBy":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Rolle super_admin erforderlich"},"404":{"description":"Antrag nicht gefunden"},"409":{"description":"Antrag steht nicht auf pending_approval"},"500":{"description":"Freigabe fehlgeschlagen"}},"operationId":"postApiV1SandboxPromote-approveById","tags":["instances"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Gibt einen Befoerderungsantrag frei. Braucht die Rolle super_admin. Freigeben laesst sich nur ein Antrag im Status pending_approval; jeder andere Status ergibt 409. Der Antrag geht auf approved, mit Freigeber und Zeitpunkt. Befoerdert wird dabei nichts — dafuer ist POST /sandbox/promote-apply/{id} zustaendig.","summary":"Gibt einen Befoerderungsantrag frei","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sandbox/promote-apply/{id}":{"post":{"responses":{"200":{"description":"Antrag auf applied gesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"newLayerVersion":{"type":"string","description":"Frisch erzeugte Kennung — kein Ergebnis eines Abgleichs"},"auditId":{"type":"string"},"auditSignature":{"type":"string","description":"SHA-256 der Pruefspur als Hex-Zeichenkette"},"appliedBy":{"type":"string"},"appliedAt":{"type":"string","format":"date-time"}},"required":["success","newLayerVersion","auditId","auditSignature","appliedBy","appliedAt"]},"example":{"success":true,"newLayerVersion":"string","auditId":"string","auditSignature":"string","appliedBy":"string","appliedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Rolle super_admin erforderlich"},"404":{"description":"Antrag nicht gefunden"},"409":{"description":"Antrag steht nicht auf approved"},"500":{"description":"Anwendung fehlgeschlagen"}},"operationId":"postApiV1SandboxPromote-applyById","tags":["instances"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Wendet einen freigegebenen Befoerderungsantrag an. Braucht die Rolle super_admin. Zulaessig ist nur ein Antrag im Status approved, jeder andere ergibt 409. Der Antrag geht auf applied, und in public.tenant_instances wird last_promote_at der Zielinstanz nachgezogen — scheitert das, bleibt die Befoerderung trotzdem stehen. ACHTUNG: die eigentliche Uebertragung der Ebenen ist noch nicht verdrahtet, newLayerVersion ist eine frisch erzeugte Kennung und kein Ergebnis eines Abgleichs.","summary":"Wendet einen freigegebenen Befoerderungsantrag an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sandbox/promote-history":{"get":{"responses":{"200":{"description":"Bis zu 20 Antraege, neueste zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"history":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"fromInstanceType":{"type":"string"},"toInstanceType":{"type":"string"},"status":{"type":"string"},"note":{"type":["string","null"]},"requestedBy":{"type":"string"},"approvedBy":{"type":["string","null"]},"appliedBy":{"type":["string","null"]},"requestedAt":{"type":"string"},"approvedAt":{"type":["string","null"]},"appliedAt":{"type":["string","null"]}},"required":["id","fromInstanceType","toInstanceType","status","note","requestedBy","approvedBy","appliedBy","requestedAt","approvedAt","appliedAt"]}},"total":{"type":"integer","minimum":0,"description":"Laenge dieser Seite, hoechstens 20 — nicht die Gesamtzahl"}},"required":["history","total"]},"example":{"history":[{"id":"string","fromInstanceType":"string","toInstanceType":"string","status":"string","note":"string","requestedBy":"string","approvedBy":"string","appliedBy":"string","requestedAt":"string","approvedAt":"string","appliedAt":"string"}],"total":0}}}},"401":{"description":"Unauthorized"},"403":{"description":"Mindestens die Rolle admin erforderlich"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1SandboxPromote-history","tags":["instances"],"parameters":[],"description":"Die letzten Befoerderungsantraege des Mandanten. Braucht mindestens die Rolle admin. Gelesen werden hoechstens 20 Zeilen aus public.promotion_requests, neueste Anfrage zuerst; es gibt keine Blaetterung, und total nennt die Laenge dieser Seite, nicht die Gesamtzahl. Gibt es die Tabelle noch nicht, kommt eine leere Historie mit 200 — jeder andere Ausfall ergibt 503 statt einer leeren Liste.","summary":"Die letzten Befoerderungsantraege des Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/instances":{"get":{"responses":{"200":{"description":"Aktiver Kontext und vorhandene Instanzen","content":{"application/json":{"schema":{"type":"object","properties":{"current":{"type":"object","properties":{"tenantId":{"type":"string"},"instanceType":{"type":"string","enum":["dev","sandbox","prod"]},"parentTenantId":{"type":["string","null"]},"lastSyncAt":{"type":["string","null"]},"lastPromoteAt":{"type":["string","null"]},"pendingChanges":{"type":"integer","minimum":0,"description":"Antraege im Status pending_approval"},"status":{"type":"string","enum":["clean","pending_changes","promotion_in_progress"]}},"required":["tenantId","instanceType","parentTenantId","lastSyncAt","lastPromoteAt","pendingChanges","status"]},"available":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"instance_type":{"type":"string","enum":["dev","sandbox","prod"]},"parent_tenant_id":{"type":["string","null"]},"last_sync_at":{"type":["string","null"]},"last_promote_at":{"type":["string","null"]},"status":{"type":"string","enum":["clean","pending_changes","promotion_in_progress"]}},"required":["id","tenant_id","instance_type","parent_tenant_id","last_sync_at","last_promote_at","status"]}}},"required":["current","available"]},"example":{"current":{"tenantId":"string","instanceType":"dev","parentTenantId":"string","lastSyncAt":"string","lastPromoteAt":"string","pendingChanges":0,"status":"clean"},"available":[{"id":"string","tenant_id":"string","instance_type":"dev","parent_tenant_id":"string","last_sync_at":"string","last_promote_at":"string","status":"clean"}]}}}},"400":{"description":"Kein Mandantenkontext"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Instances","tags":["instances"],"parameters":[],"description":"Instanzen des Mandanten und der daraus abgeleitete aktive Kontext. Gelesen wird public.tenant_instances, dazu die Zahl der offenen Befoerderungsantraege (Status pending_approval). Als aktueller Kontext gilt die hoechste vorhandene Stufe in der Reihenfolge prod, sandbox, dev; ist gar keine Instanz eingetragen, antwortet die Route mit einem gedachten prod-Kontext und leerer Liste. current ist camelCase, available reicht die Datenbankzeilen unveraendert in snake_case durch. Auch ein Lesefehler ergibt den Ersatzkontext mit 200, nicht 503.","summary":"Instanzen des Mandanten und der daraus abgeleitete aktive Kontext","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/instances/switch":{"post":{"responses":{"400":{"description":"Kein Mandantenkontext"},"401":{"description":"Unauthorized"},"403":{"description":"Mindestens die Rolle admin erforderlich"},"501":{"description":"Nicht umgesetzt — die einzige Antwort dieses Endpunkts","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","const":"not_implemented"},"requestedInstance":{"type":"string","enum":["dev","sandbox","prod"]},"tenantId":{"type":"string"},"message":{"type":"string"}},"required":["ok","error","requestedInstance","tenantId","message"]}}}}},"operationId":"postApiV1InstancesSwitch","tags":["instances"],"parameters":[],"description":"Umschalten der aktiven Instanz — nicht umgesetzt. Der Endpunkt antwortet IMMER mit 501 und ok=false; er schaltet nichts um und schreibt nirgends hin. Bis 07.08.2026 quittierte er mit ok=true, obwohl weder Sitzung noch Mandantenkontext angefasst wurden. Einen Erfolgsfall gibt es nicht — deshalb steht hier bewusst kein 2xx.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"targetInstanceType":{"type":"string","enum":["dev","sandbox","prod"]}},"required":["targetInstanceType"]},"example":{"targetInstanceType":"dev"}}}},"summary":"Umschalten der aktiven Instanz — nicht umgesetzt","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/customization/publish-layer":{"post":{"responses":{"200":{"description":"Stand abgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"signature":{"type":"string","description":"SHA-256 ueber den blob als Hex-Zeichenkette"}},"required":["id","signature"]},"example":{"id":"string","signature":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Rolle passend zur Schicht erforderlich"}},"operationId":"postApiV1CustomizationPublish-layer","tags":["sandbox"],"parameters":[],"description":"Veroeffentlicht einen Schichtstand (Hersteller, Land, Partner oder Kunde). Welche Rolle noetig ist, richtet sich nach layerType. Der Stand wird unter Schicht, Eigentuemer und Quellinstanz abgelegt und dabei mit einer SHA-256-Signatur ueber den blob versehen; ein vorhandener Stand derselben Kombination wird ersetzt. ACHTUNG: Ablageort ist ein prozesslokaler Zwischenspeicher, keine Datenbank — nach einem Neustart ist der Stand weg, und GET /sandbox/diff findet ihn dann nicht mehr.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"layerType":{"type":"string","enum":["hersteller","landes","partner","kunden"]},"ownerId":{"type":["string","null"]},"version":{"type":"integer","exclusiveMinimum":0},"blob":{"type":"object","additionalProperties":{}},"sourceInstance":{"type":"string","enum":["dev","sandbox","prod"],"default":"dev"}},"required":["layerType","ownerId","version","blob"]},"example":{"layerType":"hersteller","ownerId":"string","version":1,"blob":{},"sourceInstance":"dev"}}}},"summary":"Veroeffentlicht einen Schichtstand (Hersteller, Land, Partner oder Kunde)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/customizations":{"get":{"responses":{"200":{"description":"Nach Art gruppierte Umbauten. Leer heisst „keine\" ODER „nicht lesbar\" — siehe Beschreibung.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string","description":"Art des Umbaus; bestimmt, in welche Gruppe der Eintrag faellt."},"entity":{"type":["string","null"],"description":"Betroffene Entitaet, falls der Umbau einer gilt."},"artifactId":{"type":"string"},"manifest":{"type":"object","additionalProperties":{},"description":"Die Bauanweisung selbst. Form je nach `kind` — hier nicht zugesagt."},"version":{"type":"integer"},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":["string","null"]}},"required":["id","kind","entity","artifactId","manifest","version","status","createdBy","createdAt"]}}},"unavailable":{"type":"boolean","const":true,"description":"Nur gesetzt, wenn die Abfrage scheiterte. Dann sind die Gruppen leer, WEIL nichts gelesen werden konnte — nicht, weil nichts da ist."}},"required":["data"]},"example":{"data":{"beispiel":[{"id":"string","kind":"string","entity":"string","artifactId":"string","manifest":{},"version":0,"status":"string","createdBy":"string","createdAt":"string"}]},"unavailable":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Customizations","tags":["Anpassungen"],"parameters":[],"summary":"Aktives Umbau-Manifest des Mandanten","description":"Liefert alle aktiven, nicht abgeloesten Umbauten des Mandanten, nach Art\ngruppiert. Grundlage fuer die Oberfläche, die daraus entscheidet, welche\nzusaetzlichen Felder und Ansichten sie rendert.\n\nLEERE GRUPPEN SIND MEHRDEUTIG — das ist beim Lesen dieser Antwort das\nWichtigste: dieselbe leere Struktur kommt zurueck, wenn der Mandant\nnichts umgebaut hat, wenn kein Mandantenkontext vorliegt UND wenn die\nAbfrage scheitert. Der `catch` antwortet ausdruecklich mit leeren\nGruppen unter 200, mit der Begruendung, eine streikende Manifest-Tabelle\nduerfe den Renderer nicht brechen.\n\nDER STOERFALL TRAEGT SEIT 17.08. EIN KENNZEICHEN: schlaegt die Abfrage\nfehl, kommt zusaetzlich `unavailable: true`. Damit ist „nichts umgebaut\"\nvon „gerade nicht lesbar\" unterscheidbar, ohne dass die Antwort ihre\nForm aendert oder auf einen Fehlerstatus wechselt. Fehlt das Feld, war\ndie Abfrage erfolgreich.\n\nKein Mandantenkontext gibt weiterhin leere Gruppen OHNE Kennzeichen —\ndort ist nichts gescheitert, es war nur nichts zu holen.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."}},"/api/v1/customizations/{artifactId}/versions":{"get":{"responses":{"200":{"description":"Die Kette, neueste zuerst. Durchgehend leer heisst „unbekanntes Artefakt\" ODER „kein Mandantenkontext\".","content":{"application/json":{"schema":{"type":"object","properties":{"current":{"type":["object","null"],"properties":{"id":{"type":"string"},"kind":{"type":"string","description":"Art des Umbaus; bestimmt, in welche Gruppe der Eintrag faellt."},"entity":{"type":["string","null"],"description":"Betroffene Entitaet, falls der Umbau einer gilt."},"artifactId":{"type":"string"},"manifest":{"type":"object","additionalProperties":{},"description":"Die Bauanweisung selbst. Form je nach `kind` — hier nicht zugesagt."},"version":{"type":"integer"},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":["string","null"]}},"required":["id","kind","entity","artifactId","manifest","version","status","createdBy","createdAt"]},"prior":{"type":["object","null"],"properties":{"id":{"type":"string"},"kind":{"type":"string","description":"Art des Umbaus; bestimmt, in welche Gruppe der Eintrag faellt."},"entity":{"type":["string","null"],"description":"Betroffene Entitaet, falls der Umbau einer gilt."},"artifactId":{"type":"string"},"manifest":{"type":"object","additionalProperties":{},"description":"Die Bauanweisung selbst. Form je nach `kind` — hier nicht zugesagt."},"version":{"type":"integer"},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":["string","null"]}},"required":["id","kind","entity","artifactId","manifest","version","status","createdBy","createdAt"]},"versions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string","description":"Art des Umbaus; bestimmt, in welche Gruppe der Eintrag faellt."},"entity":{"type":["string","null"],"description":"Betroffene Entitaet, falls der Umbau einer gilt."},"artifactId":{"type":"string"},"manifest":{"type":"object","additionalProperties":{},"description":"Die Bauanweisung selbst. Form je nach `kind` — hier nicht zugesagt."},"version":{"type":"integer"},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":["string","null"]}},"required":["id","kind","entity","artifactId","manifest","version","status","createdBy","createdAt"]}}},"required":["current","prior","versions"]},"example":{"current":{"id":"string","kind":"string","entity":"string","artifactId":"string","manifest":{},"version":0,"status":"string","createdBy":"string","createdAt":"string"},"prior":{"id":"string","kind":"string","entity":"string","artifactId":"string","manifest":{},"version":0,"status":"string","createdBy":"string","createdAt":"string"},"versions":[{"id":"string","kind":"string","entity":"string","artifactId":"string","manifest":{},"version":0,"status":"string","createdBy":"string","createdAt":"string"}]}}}},"400":{"description":"Leere `artifactId`, oder eine Laengengrenze verletzt (artifactId 200, entity 120)."},"401":{"description":"Keine Sitzung."},"503":{"description":"`database_unavailable`, `retryAfter: 5` — die Historie ist gerade nicht lesbar. Eine bloss FEHLENDE Tabelle faellt nicht hierunter, die gibt 200 mit leerer Kette."}},"operationId":"getApiV1CustomizationsByArtifactIdVersions","tags":["Anpassungen"],"parameters":[{"in":"path","name":"artifactId","schema":{"type":"string","minLength":1,"maxLength":200},"required":true},{"in":"query","name":"entity","schema":{"type":"string","minLength":1,"maxLength":120}}],"summary":"Versions-Historie eines Umbaus","description":"Liefert alle Versionen EINES Umbau-Artefakts, neueste zuerst — die\nGrundlage fuer Versionsvergleich und Ruecknahme.\n\nDrei Felder: `versions` ist die vollstaendige Kette. `current` ist die\naktive Spitze, also die erste nicht abgeloeste Version; ist keine als\naktiv markiert, wird ersatzweise die hoechste Versionsnummer genommen.\n`prior` ist die naechstniedrigere Version darunter — der Stand, gegen\nden verglichen wird. Bei nur einer Version ist `prior` `null`.\n\nOHNE `entity` KOENNEN SICH KETTEN MISCHEN. Feldkennungen tragen\nunabhaengig von der Entitaet den Vorsatz `kunde_`, also gibt es\n`kunde_prio` bei Kunden UND bei Auftraegen als zwei getrennte Ketten.\nOhne `?entity=` kommen beide in EINER Liste zurueck, und `current`\nstammt dann moeglicherweise aus der anderen Entitaet. `?entity=` grenzt\nauf die gewuenschte Kette ein; mandantenweite Artefakte ohne Entitaet\nwerden dabei mit getroffen.\n\nEINE LEERE HISTORIE HAT ZWEI URSACHEN, DIE NICHT ZU UNTERSCHEIDEN SIND:\ndas Artefakt gibt es nicht, oder es liegt kein Mandantenkontext vor. In\nbeiden Faellen kommt 200 mit `current: null`, `prior: null`,\n`versions: []`. Einen 404 gibt es hier nicht.\n\nEIN AUSFALL IST DAGEGEN SICHTBAR: fehlt die Manifest-Tabelle, gilt die\nleere Historie oben; jeder ANDERE Datenbankfehler antwortet 503, statt\nLeere vorzutaeuschen. Das ist der Unterschied zu `GET /customizations`,\ndas jeden Fehler unter 200 verbirgt.\n\nNur lesend — Ruecknahme oder Wechsel der aktiven Version passieren\nnicht hier. Keine Rollenpruefung: jeder angemeldete Benutzer des\nMandanten."}},"/api/v1/ai-build-docs":{"post":{"responses":{"200":{"description":"Eingetragen ODER uebersprungen — der Unterschied steht im Rumpf, nicht im Status. `skipped` kommt, wenn der Aufruf gar kein Bau-Vorgang war; `recorded: false` heisst, dass es den Eintrag durch den Doppel-Schutz schon gab.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":["string","null"],"description":"Kennung des Eintrags; null, wenn es ihn wegen des Doppel-Schutzes schon gab"},"recorded":{"type":"boolean","description":"true nur bei einem NEU geschriebenen Eintrag"}},"required":["ok","id","recorded"]},{"type":"object","properties":{"skipped":{"type":"boolean","const":true},"reason":{"type":"string","description":"Warum nichts eingetragen wurde, z. B. `no_tool` oder `not_a_build_action`"}},"required":["skipped","reason"]}]},"example":{"ok":true,"id":"string","recorded":true}}}},"401":{"description":"Kein Mandantenkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}}},"operationId":"postApiV1Ai-build-docs","tags":["ai","build-docs"],"parameters":[],"summary":"Dokumentiert eine erfolgreiche KI-Bau-Aktion","description":"Dokumentiert eine erfolgreiche KI-Bau-Aktion (deterministisch aus toolName+input+result)."}},"/api/v1/ai-build-docs/record":{"post":{"responses":{"200":{"description":"Eingetragen ODER uebersprungen — der Unterschied steht im Rumpf, nicht im Status. `skipped` mit `no_artifact` kommt, wenn der Draft kein Artefakt benennt; `recorded: false` heisst, dass es den Eintrag durch den Doppel-Schutz schon gab.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":["string","null"],"description":"Kennung des Eintrags; null, wenn es ihn wegen des Doppel-Schutzes schon gab"},"recorded":{"type":"boolean","description":"true nur bei einem NEU geschriebenen Eintrag"}},"required":["ok","id","recorded"]},{"type":"object","properties":{"skipped":{"type":"boolean","const":true},"reason":{"type":"string","description":"Warum nichts eingetragen wurde, z. B. `no_tool` oder `not_a_build_action`"}},"required":["skipped","reason"]}]},"example":{"ok":true,"id":"string","recorded":true}}}},"400":{"description":"Validation error (zod)"},"401":{"description":"Kein Mandantenkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}}},"operationId":"postApiV1Ai-build-docsRecord","tags":["ai","build-docs"],"parameters":[],"summary":"Dokumentiert eine KI-Bau-Aktion aus dem Direktpfad (Rechtsklick)","description":"Dokumentiert eine KI-Bau-Aktion des deterministischen Direktpfads (Rechtsklick) — der Client liefert den fertigen Draft, source=direct.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"actionType":{"type":"string","enum":["feld_anlegen","feld_aendern","feld_loeschen","modul_anlegen","tabelle_anlegen","workflow_anlegen","webhook_anlegen","regel_anlegen","entscheidungstabelle_anlegen"]},"targetEntity":{"type":["string","null"],"minLength":1,"maxLength":200},"artifactId":{"type":"string","minLength":1,"maxLength":200},"intentText":{"type":["string","null"],"maxLength":4000},"functionSummary":{"type":"string","minLength":1,"maxLength":500},"changeDetails":{"type":"object","additionalProperties":{}}},"required":["actionType","functionSummary"]},"example":{"actionType":"feld_anlegen","targetEntity":"string","artifactId":"string","intentText":"string","functionSummary":"string","changeDetails":{}}}}}}},"/api/v1/ai-build-docs/export":{"get":{"responses":{"200":{"description":"Der Inhaltstyp haengt an `?format=`: ohne Angabe (oder mit allem ausser `json`) kommt Markdown als `text/markdown`, mit `format=json` ein JSON-Rumpf. Beide entstehen aus DEMSELBEN Renderer, damit sich Text und Daten nicht auseinanderentwickeln.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"actionType":{"type":"string","description":"Art des Bau-Vorgangs, z. B. `feld_anlegen` oder `modul_anlegen`"},"targetEntity":{"type":["string","null"],"description":"Betroffene Entitaet; null wenn keine benannt ist"},"intentText":{"type":["string","null"],"description":"Der urspruengliche Wunsch im Klartext; null beim Nachtrag"},"functionSummary":{"type":"string","description":"Was der Vorgang bewirkt hat"},"changeDetails":{"type":"object","additionalProperties":{},"description":"Die Einzelheiten der Aenderung als JSON"},"actor":{"type":["string","null"],"description":"Wer den Vorgang ausgeloest hat; null wenn nicht erfasst"},"createdAt":{"type":["string","null"],"description":"Zeitpunkt des Vorgangs"},"source":{"type":["string","null"],"description":"`command` (KI-Chat), `direct` (Rechtsklick) oder `backfill` (nachgetragen)"},"artifactId":{"type":["string","null"],"description":"Das Artefakt, ueber das die Zeile spricht — Bindeglied zum Bestand"},"changeSummary":{"type":"string","description":"Dieselbe Aenderung als fertiger Satz, aus demselben Renderer wie das Markdown"},"group":{"type":"object","properties":{"key":{"type":"string","description":"Kennung der Rubrik"},"order":{"type":"integer","description":"Reihenfolge der Rubrik"},"title":{"type":"string","description":"Ueberschrift der Rubrik"}},"required":["key","order","title"],"description":"Rubrik, unter der die Zeile steht"}},"required":["actionType","targetEntity","intentText","functionSummary","changeDetails","actor","createdAt","changeSummary","group"]},"description":"Die Eintraege des Mandanten"},"total":{"type":"integer","minimum":0,"description":"Anzahl der Eintraege — gleich der Laenge von data"},"meta":{"type":"object","properties":{"darfNachtragen":{"type":"boolean","description":"Ob der Aufrufer den Nachtrag ausloesen darf (ab Manager)"},"zieladressenGekuerzt":{"type":"boolean","description":"true unterhalb der Manager-Rolle: Webhook-Zieladressen sind dann auf den Host gekuerzt"}},"required":["darfNachtragen","zieladressenGekuerzt"],"description":"Was der Aufrufer hier darf und was ihm vorenthalten wurde"}},"required":["data","total","meta"]},"example":{"data":[{"actionType":"string","targetEntity":"string","intentText":"string","functionSummary":"string","changeDetails":{},"actor":"string","createdAt":"string","source":"string","artifactId":"string","changeSummary":"string","group":{"key":"string","order":0,"title":"string"}}],"total":0,"meta":{"darfNachtragen":true,"zieladressenGekuerzt":true}}},"text/markdown":{"schema":{"type":"string"}}}},"401":{"description":"Kein Mandantenkontext oder keine Rolle im Kontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}},"503":{"description":"Database unavailable — NICHT als leere Doku ausgeben","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}}},"operationId":"getApiV1Ai-build-docsExport","tags":["ai","build-docs"],"parameters":[],"summary":"Exportiert die Umbauten-Dokumentation als Markdown oder JSON","description":"Exportiert die KI-Umbauten-Dokumentation des Mandanten als Markdown (default) oder JSON. Lesen: jede*r authentifizierte Mandanten-User; Webhook-Zieladressen sind unterhalb der Manager-Rolle auf den Host gekürzt."}},"/api/v1/ai-build-docs/bestand":{"get":{"responses":{"200":{"description":"Eine Zeile je Artefakt plus `quellen`. In `quellen` steht je Quelle, ob sie gelesen werden konnte — `gelesen: false` heisst „konnte nicht nachsehen\", NICHT „nichts vorhanden\". Eine leere `data` bei `gelesen: false` ist deshalb KEIN Beleg dafuer, dass der Mandant nichts angepasst hat.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"art":{"type":"string","description":"Art der Anpassung, z. B. `regel`, `entscheidungstabelle`, `automatisierung`, `webhook`, `branchenpaket`"},"artefaktId":{"type":"string","description":"Kennung des Artefakts — Bindeglied zur Bau-Doku"},"name":{"type":"string","description":"Anzeigename"},"beschreibung":{"type":"string","description":"Was die Anpassung tut, in deutscher Sprache"},"aktiv":{"type":["boolean","null"],"description":"Ob sie gerade wirkt; null wenn die Quelle das nicht hergibt"},"entity":{"type":["string","null"],"description":"Betroffene Entitaet; null wenn keine"},"angelegtVon":{"type":["string","null"],"description":"Wer sie angelegt hat; null wenn nicht erfasst"},"angelegtAm":{"type":["string","null"],"description":"Wann sie angelegt wurde; null wenn nicht erfasst"}},"required":["art","artefaktId","name","beschreibung","aktiv","entity","angelegtVon","angelegtAm"]},"description":"Eine Zeile je Artefakt, ueber alle Quellen hinweg"},"total":{"type":"integer","minimum":0,"description":"Anzahl der ausgelieferten Zeilen"},"quellen":{"type":"array","items":{"type":"object","properties":{"art":{"type":"string","description":"Die Quelle"},"gelesen":{"type":"boolean","description":"false heisst „konnte nicht nachsehen\" — NICHT „nichts vorhanden\". Genau diese Verwechslung soll das Feld verhindern"},"anzahl":{"type":"integer","minimum":0,"description":"Wie viele Zeilen diese Quelle beigesteuert hat"},"gesamt":{"type":"integer","minimum":0,"description":"Gesamtzahl, falls die Quelle mehr hat als ausgeliefert wird"}},"required":["art","gelesen","anzahl"]},"description":"Meldung je Quelle — hier steht, welcher Teil des Bestands fehlt und warum"}},"required":["data","total","quellen"]},"example":{"data":[{"art":"string","artefaktId":"string","name":"string","beschreibung":"string","aktiv":true,"entity":"string","angelegtVon":"string","angelegtAm":"string"}],"total":0,"quellen":[{"art":"string","gelesen":true,"anzahl":0,"gesamt":0}]}}}},"401":{"description":"Kein Mandantenkontext oder keine Rolle im Kontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}}},"operationId":"getApiV1Ai-build-docsBestand","tags":["ai","build-docs"],"parameters":[],"summary":"Listet die Anpassungen des Mandanten, die nicht aus der KI-Bau-Doku stammen","description":"Listet die heute geltenden Anpassungen des Mandanten, die NICHT aus der KI-Bau-Doku stammen: Regeln, Entscheidungstabellen, Automatisierungen, Webhooks, installierte Branchenpakete. Je Quelle wird gemeldet, ob sie gelesen werden konnte."}},"/api/v1/ai-build-docs/backfill":{"post":{"responses":{"200":{"description":"Bilanz des Laufs: `scanned` nennt je Quelle die durchgesehenen Artefakte, `created` die daraus NEU entstandenen Eintraege. Der Lauf ist wiederholbar — beim zweiten Mal ist `created` in der Regel 0, weil der Doppel-Schutz greift. Sind die Automatisierungen nicht lesbar, laeuft der Nachtrag fuer Felder und Module trotzdem durch; das steht dann nur im Serverprotokoll.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"scanned":{"type":"object","properties":{"fields":{"type":"integer","minimum":0,"description":"Geprüfte Felder"},"modules":{"type":"integer","minimum":0,"description":"Geprüfte eigene Module"},"workflows":{"type":"integer","minimum":0,"description":"Geprüfte KI-Automatisierungen"}},"required":["fields","modules","workflows"],"description":"Was durchgesehen wurde"},"created":{"type":"integer","minimum":0,"description":"Wie viele Eintraege daraus NEU entstanden sind"}},"required":["ok","scanned","created"]},"example":{"ok":true,"scanned":{"fields":0,"modules":0,"workflows":0},"created":0}}}},"401":{"description":"Kein Mandantenkontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}},"403":{"description":"Forbidden — mind. Manager-Rolle nötig"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error"]}}}}},"operationId":"postApiV1Ai-build-docsBackfill","tags":["ai","build-docs"],"parameters":[],"summary":"Traegt vorhandene KI-Umbauten nachtraeglich als Doku-Eintraege ein","description":"Rekonstruiert die vorhandenen KI-Umbauten des Mandanten (Felder/Module/Workflows) als Doku-Einträge."}},"/api/v1/change-history":{"get":{"responses":{"200":{"description":"Eine Seite des Verlaufs; Form aus dem Sammler.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}}},"operationId":"getApiV1Change-history","tags":["Historie"],"parameters":[],"summary":"Aenderungsverlauf des Mandanten","description":"Fuehrt die Aenderungen aus mehreren Quellen zu EINER absteigend\nsortierten Seite zusammen (derzeit KI-Bau-Log und rueckrollbare\nFeld-Versionen). Geblaettert wird ueber `before` (Zeitpunkt des zuletzt\ngesehenen Eintrags) und `pageSize` (hoechstens 100).\n\nDie Antwortform stammt aus dem Sammler und wird unveraendert\ndurchgereicht — Feldnamen sagt diese Route deshalb nicht zu.\n\nEINE QUELLE KANN STILL FEHLEN: sie ist ueber die Mandanten-UUID\nverschluesselt, und laesst sich die nicht aufloesen, faehrt der Aufruf\nmit `null` weiter statt abzubrechen. Dann fehlen deren Eintraege, ohne\ndass die Antwort es sagt — der Verlauf ist unvollstaendig und sieht\nvollstaendig aus.\n\nAb Rolle `manager`. Die mandantenuebergreifende Fassung fuer den\nBetreiber liegt getrennt unter `/api/admin/customers/{id}/history`."}},"/api/v1/custom-fields":{"post":{"responses":{"200":{"description":"Vorschau oder Ergebnis der Anlage","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"dryRun":{"type":"boolean","const":true,"description":"Es wurde nichts geaendert"},"sql":{"type":"string","description":"Die ALTER-Anweisung, die ein echter Aufruf ausfuehren wuerde"},"dataPreview":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"],"description":"Betroffener Bereich"},"entityLabel":{"type":"string","description":"Deutscher Name des Bereichs"},"fieldId":{"type":"string","description":"Der auf kunde_ gehobene Spaltenname"},"fieldLabel":{"type":"string","description":"Anzeigename, aus dem urspruenglichen Namen abgeleitet"},"fieldTypeLabel":{"type":"string","description":"Typ im Klartext; mit Optionen immer „Auswahl\""},"totalRows":{"type":"integer","description":"Wie viele Zeilen das neue Feld bekaemen; 0 falls die Zaehlung scheiterte"},"sampleRows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"}},"required":["id","label"]},"description":"Bis zu fuenf Beispielzeilen; leer, wenn die Leseprobe scheiterte"}},"required":["entity","entityLabel","fieldId","fieldLabel","fieldTypeLabel","totalRows","sampleRows"]}},"required":["dryRun","sql","dataPreview"],"description":"Vorschau — nichts wurde geschrieben"},{"type":"object","properties":{"dryRun":{"type":"boolean","const":false,"description":"Die Aenderung wurde ausgefuehrt"},"sql":{"type":"string","description":"Die ausgefuehrte ALTER-Anweisung"},"added":{"type":"boolean","description":"false bei einem wiederholten Anlegen: die Spalte gab es schon, die Registry-Zeile wurde nur aufgefrischt"},"beforeColumns":{"type":"array","items":{"type":"string"},"description":"Spalten der Tabelle vor der Aenderung"},"afterColumns":{"type":"array","items":{"type":"string"},"description":"Spalten der Tabelle danach"},"registry":{"type":["object","null"],"properties":{"id":{"type":"string","description":"Kennung der Registry-Zeile"},"tenantId":{"type":"string","description":"Mandant, dem das Feld gehoert"},"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"],"description":"Bereich, an dem das Feld haengt"},"fieldId":{"type":"string","description":"Spaltenname in der Tabelle — beim Anlegen auf den kunde_-Namensraum gehoben"},"fieldLabel":{"type":"string","description":"Anzeigename in der Oberflaeche"},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"],"description":"Datenbanktyp der Spalte"},"defaultValue":{"type":["string","null"],"description":"Vorbelegung; null wenn keine gesetzt ist"},"required":{"type":"boolean","description":"Ob das Feld beim Speichern verlangt wird"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"fieldIconType":{"type":["string","null"],"description":"Darstellungsart in der Maske; null wenn keine gewaehlt wurde"},"showInList":{"type":"boolean","description":"Ob das Feld als Spalte in der Liste erscheint"},"showOnDocuments":{"type":"boolean","description":"Ob der Wert im Kunden-Infoblock der Belege gedruckt wird"},"sortOrder":{"type":"number","description":"Position innerhalb der eigenen Felder"},"groupName":{"type":["string","null"],"description":"Gruppenueberschrift in der Maske; null wenn ungruppiert"},"helpText":{"type":["string","null"],"description":"Hilfetext am Feld; null wenn keiner gepflegt ist"},"scope":{"type":"string","enum":["tenant","user"],"description":"tenant = fuer alle sichtbar, user = nur fuer den Ersteller"},"ownerUserId":{"type":["string","null"],"description":"Ersteller bei einem persoenlichen Feld; sonst null"},"options":{"type":["array","null"],"items":{"type":"string"},"description":"Auswahlwerte bei einem Auswahlfeld; sonst null"},"anchorFieldId":{"type":["string","null"],"description":"Feld, unter dem es in der Maske erscheint; null heisst Block „Eigene Felder\""},"anchorColumn":{"type":["string","null"],"description":"Native Spalte, nach der es in der Liste einsortiert wird; null heisst ans Ende"}},"required":["id","tenantId","entity","fieldId","fieldLabel","pgType","defaultValue","required","createdAt","fieldIconType","showInList","showOnDocuments","sortOrder","groupName","helpText","scope","ownerUserId","options","anchorFieldId","anchorColumn"],"description":"Die geschriebene Registry-Zeile"}},"required":["dryRun","sql","added","beforeColumns","afterColumns","registry"],"description":"Ausgefuehrt"}],"description":"Vorschau oder Ergebnis — unterschieden ueber `dryRun`"},"example":{"dryRun":true,"sql":"string","dataPreview":{"entity":"customers","entityLabel":"string","fieldId":"string","fieldLabel":"string","fieldTypeLabel":"string","totalRows":0,"sampleRows":[{"id":"string","label":"string"}]}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"409":{"description":"Tabelle des Bereichs existiert fuer diesen Mandanten nicht"},"422":{"description":"Bereich hat keinen Wertspeicher fuer eigene Felder"},"429":{"description":"Tageskontingent fuer Schemaaenderungen erschoepft"}},"operationId":"postApiV1Custom-fields","tags":["custom-fields"],"parameters":[],"summary":"Legt ein eigenes Feld an","description":"Fuehrt ein ALTER TABLE auf der Mandantentabelle und den Registry-Eintrag in EINER Transaktion aus. Der Feldname wird dabei in den kunde_-Namensraum gehoben, damit er nie mit einer kuenftigen Kernspalte kollidiert. Die WERTE liegen spaeter im JSONB custom_fields, nicht in der angelegten Spalte. Mit `dryRun: true` kommt nur die Vorschau samt erzeugtem SQL und einer Leseprobe — geschrieben wird dann nichts. Der Aufruf antwortet 200, auch im Anlage-Fall, nicht 201. Nebenwirkungen eines echten Anlegens: ein Eintrag in der Aenderungshistorie, eine Manifest-Fassung und, bei Optionen mit Rabatt-Angabe, eine abgeleitete Rabatt-Tabelle. Sonderfaelle: 422 fuer Bereiche ohne JSONB-Wertspeicher (auch im dryRun), 409 wenn es die Mandantentabelle nicht gibt, 429 bei erschoepftem Tageskontingent fuer Schemaaenderungen. Ein wiederholtes Anlegen desselben Feldes ist zulaessig und liefert `added: false`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"]},"fieldId":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,62}$"},"fieldLabel":{"type":"string","maxLength":255},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"]},"defaultValue":{"type":"string"},"required":{"type":"boolean","default":false},"dryRun":{"type":"boolean","default":false},"fieldIconType":{"type":"string","maxLength":40},"showInList":{"type":"boolean"},"showOnDocuments":{"type":"boolean"},"sortOrder":{"type":"integer"},"groupName":{"type":"string","maxLength":120},"helpText":{"type":"string"},"scope":{"type":"string","enum":["tenant","user"]},"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":120},"maxItems":50},"anchorFieldId":{"type":"string","maxLength":64},"anchorColumn":{"type":"string","maxLength":64}},"required":["entity","fieldId","pgType"]}}}}},"get":{"responses":{"200":{"description":"Die sichtbaren eigenen Felder","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Registry-Zeile"},"tenantId":{"type":"string","description":"Mandant, dem das Feld gehoert"},"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"],"description":"Bereich, an dem das Feld haengt"},"fieldId":{"type":"string","description":"Spaltenname in der Tabelle — beim Anlegen auf den kunde_-Namensraum gehoben"},"fieldLabel":{"type":"string","description":"Anzeigename in der Oberflaeche"},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"],"description":"Datenbanktyp der Spalte"},"defaultValue":{"type":["string","null"],"description":"Vorbelegung; null wenn keine gesetzt ist"},"required":{"type":"boolean","description":"Ob das Feld beim Speichern verlangt wird"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"fieldIconType":{"type":["string","null"],"description":"Darstellungsart in der Maske; null wenn keine gewaehlt wurde"},"showInList":{"type":"boolean","description":"Ob das Feld als Spalte in der Liste erscheint"},"showOnDocuments":{"type":"boolean","description":"Ob der Wert im Kunden-Infoblock der Belege gedruckt wird"},"sortOrder":{"type":"number","description":"Position innerhalb der eigenen Felder"},"groupName":{"type":["string","null"],"description":"Gruppenueberschrift in der Maske; null wenn ungruppiert"},"helpText":{"type":["string","null"],"description":"Hilfetext am Feld; null wenn keiner gepflegt ist"},"scope":{"type":"string","enum":["tenant","user"],"description":"tenant = fuer alle sichtbar, user = nur fuer den Ersteller"},"ownerUserId":{"type":["string","null"],"description":"Ersteller bei einem persoenlichen Feld; sonst null"},"options":{"type":["array","null"],"items":{"type":"string"},"description":"Auswahlwerte bei einem Auswahlfeld; sonst null"},"anchorFieldId":{"type":["string","null"],"description":"Feld, unter dem es in der Maske erscheint; null heisst Block „Eigene Felder\""},"anchorColumn":{"type":["string","null"],"description":"Native Spalte, nach der es in der Liste einsortiert wird; null heisst ans Ende"}},"required":["id","tenantId","entity","fieldId","fieldLabel","pgType","defaultValue","required","createdAt","fieldIconType","showInList","showOnDocuments","sortOrder","groupName","helpText","scope","ownerUserId","options","anchorFieldId","anchorColumn"],"description":"Ein eigenes Feld, wie es in der Registry steht"},"description":"Die sichtbaren eigenen Felder — fremde persoenliche Felder fehlen hier"}},"required":["data"]},"example":{"data":[{"id":"string","tenantId":"string","entity":"customers","fieldId":"string","fieldLabel":"string","pgType":"text","defaultValue":"string","required":true,"createdAt":"string","fieldIconType":"string","showInList":true,"showOnDocuments":true,"sortOrder":0,"groupName":"string","helpText":"string","scope":"tenant","ownerUserId":"string","options":["string"],"anchorFieldId":"string","anchorColumn":"string"}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Custom-fields","tags":["custom-fields"],"parameters":[],"summary":"Listet die eigenen Felder des Mandanten","description":"Liest die Registry-Eintraege des Mandanten. Ohne Angabe kommen die Felder ALLER Bereiche; der Abfrageparameter `entity` schraenkt auf einen ein. Ein unbekannter Bereich fuehrt hier NICHT zu 400, sondern zu einer leeren Liste — anders als bei GET /custom-fields/{entity}. Persoenliche Felder anderer Benutzer werden herausgefiltert; die eigenen und die mandantenweiten bleiben. Es gibt keine Blaetterung."}},"/api/v1/custom-fields/{entity}":{"get":{"responses":{"200":{"description":"Die sichtbaren eigenen Felder dieses Bereichs","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Registry-Zeile"},"tenantId":{"type":"string","description":"Mandant, dem das Feld gehoert"},"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"],"description":"Bereich, an dem das Feld haengt"},"fieldId":{"type":"string","description":"Spaltenname in der Tabelle — beim Anlegen auf den kunde_-Namensraum gehoben"},"fieldLabel":{"type":"string","description":"Anzeigename in der Oberflaeche"},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"],"description":"Datenbanktyp der Spalte"},"defaultValue":{"type":["string","null"],"description":"Vorbelegung; null wenn keine gesetzt ist"},"required":{"type":"boolean","description":"Ob das Feld beim Speichern verlangt wird"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"fieldIconType":{"type":["string","null"],"description":"Darstellungsart in der Maske; null wenn keine gewaehlt wurde"},"showInList":{"type":"boolean","description":"Ob das Feld als Spalte in der Liste erscheint"},"showOnDocuments":{"type":"boolean","description":"Ob der Wert im Kunden-Infoblock der Belege gedruckt wird"},"sortOrder":{"type":"number","description":"Position innerhalb der eigenen Felder"},"groupName":{"type":["string","null"],"description":"Gruppenueberschrift in der Maske; null wenn ungruppiert"},"helpText":{"type":["string","null"],"description":"Hilfetext am Feld; null wenn keiner gepflegt ist"},"scope":{"type":"string","enum":["tenant","user"],"description":"tenant = fuer alle sichtbar, user = nur fuer den Ersteller"},"ownerUserId":{"type":["string","null"],"description":"Ersteller bei einem persoenlichen Feld; sonst null"},"options":{"type":["array","null"],"items":{"type":"string"},"description":"Auswahlwerte bei einem Auswahlfeld; sonst null"},"anchorFieldId":{"type":["string","null"],"description":"Feld, unter dem es in der Maske erscheint; null heisst Block „Eigene Felder\""},"anchorColumn":{"type":["string","null"],"description":"Native Spalte, nach der es in der Liste einsortiert wird; null heisst ans Ende"}},"required":["id","tenantId","entity","fieldId","fieldLabel","pgType","defaultValue","required","createdAt","fieldIconType","showInList","showOnDocuments","sortOrder","groupName","helpText","scope","ownerUserId","options","anchorFieldId","anchorColumn"],"description":"Ein eigenes Feld, wie es in der Registry steht"},"description":"Die sichtbaren eigenen Felder — fremde persoenliche Felder fehlen hier"}},"required":["data"]},"example":{"data":[{"id":"string","tenantId":"string","entity":"customers","fieldId":"string","fieldLabel":"string","pgType":"text","defaultValue":"string","required":true,"createdAt":"string","fieldIconType":"string","showInList":true,"showOnDocuments":true,"sortOrder":0,"groupName":"string","helpText":"string","scope":"tenant","ownerUserId":"string","options":["string"],"anchorFieldId":"string","anchorColumn":"string"}]}}}},"400":{"description":"Bereich nicht freigegeben"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Custom-fieldsByEntity","tags":["custom-fields"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true}],"summary":"Listet die eigenen Felder eines Bereichs","description":"Liest die Registry-Eintraege des Mandanten fuer genau einen Bereich. Ein nicht freigegebener Bereich ist hier ein Fehler: 400 mit `entity ... is not whitelisted` — die bare Liste beantwortet denselben Fall dagegen mit einer leeren Liste. Persoenliche Felder anderer Benutzer werden herausgefiltert. Es gibt keine Blaetterung."}},"/api/v1/custom-fields/{entity}/list-columns":{"get":{"responses":{"200":{"description":"Spaltendefinitionen und Werte","content":{"application/json":{"schema":{"type":"object","properties":{"columns":{"type":"array","items":{"type":"object","properties":{"fieldId":{"type":"string","description":"Spaltenname, zugleich Schluessel in `values`"},"fieldLabel":{"type":"string","description":"Spaltenueberschrift"},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"],"description":"Datenbanktyp der Spalte"},"fieldIconType":{"type":["string","null"],"description":"Darstellungsart; null wenn keine gewaehlt wurde"},"sortOrder":{"type":"number","description":"Reihenfolge untereinander"},"anchorColumn":{"type":["string","null"],"description":"Native Spalte, nach der eingefuegt wird; null heisst ans Ende"}},"required":["fieldId","fieldLabel","pgType","fieldIconType","sortOrder","anchorColumn"]},"description":"Nur die als Listenspalte markierten Felder, nach sortOrder"},"values":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}},"description":"Werte je Zeilenkennung und Feld, gelesen aus custom_fields; leeres Objekt, wenn keine `ids` kamen oder der Wert-Abruf scheiterte"}},"required":["columns","values"],"description":"Listenspalten samt Werten"},"example":{"columns":[{"fieldId":"string","fieldLabel":"string","pgType":"text","fieldIconType":"string","sortOrder":0,"anchorColumn":"string"}],"values":{"beispiel":{}}}}}},"400":{"description":"Bereich nicht freigegeben"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Custom-fieldsByEntityList-columns","tags":["custom-fields"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true}],"summary":"Listet die für die Liste markierten eigenen Felder samt Werten","description":"Liefert in einem Aufruf die Spaltendefinitionen (nur Felder mit „in Liste anzeigen\", nach Sortierung) UND die zugehoerigen Werte. Die Zeilen kommen als Abfrageparameter `ids` als kommagetrennte Liste, hoechstens 500 — ohne `ids` bleibt `values` leer. Die Werte werden aus dem JSONB custom_fields gelesen, nicht aus den angelegten Spalten; die sind absichtlich immer leer. Der Wert-Abruf ist nachrangig: scheitert er, kommen die Spalten trotzdem und `values` bleibt leer, damit die Liste anzeigbar bleibt. Ein nicht freigegebener Bereich fuehrt zu 400."}},"/api/v1/custom-fields/{entity}/{fieldId}":{"get":{"responses":{"200":{"description":"Der Registry-Eintrag","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Registry-Zeile"},"tenantId":{"type":"string","description":"Mandant, dem das Feld gehoert"},"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"],"description":"Bereich, an dem das Feld haengt"},"fieldId":{"type":"string","description":"Spaltenname in der Tabelle — beim Anlegen auf den kunde_-Namensraum gehoben"},"fieldLabel":{"type":"string","description":"Anzeigename in der Oberflaeche"},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"],"description":"Datenbanktyp der Spalte"},"defaultValue":{"type":["string","null"],"description":"Vorbelegung; null wenn keine gesetzt ist"},"required":{"type":"boolean","description":"Ob das Feld beim Speichern verlangt wird"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"fieldIconType":{"type":["string","null"],"description":"Darstellungsart in der Maske; null wenn keine gewaehlt wurde"},"showInList":{"type":"boolean","description":"Ob das Feld als Spalte in der Liste erscheint"},"showOnDocuments":{"type":"boolean","description":"Ob der Wert im Kunden-Infoblock der Belege gedruckt wird"},"sortOrder":{"type":"number","description":"Position innerhalb der eigenen Felder"},"groupName":{"type":["string","null"],"description":"Gruppenueberschrift in der Maske; null wenn ungruppiert"},"helpText":{"type":["string","null"],"description":"Hilfetext am Feld; null wenn keiner gepflegt ist"},"scope":{"type":"string","enum":["tenant","user"],"description":"tenant = fuer alle sichtbar, user = nur fuer den Ersteller"},"ownerUserId":{"type":["string","null"],"description":"Ersteller bei einem persoenlichen Feld; sonst null"},"options":{"type":["array","null"],"items":{"type":"string"},"description":"Auswahlwerte bei einem Auswahlfeld; sonst null"},"anchorFieldId":{"type":["string","null"],"description":"Feld, unter dem es in der Maske erscheint; null heisst Block „Eigene Felder\""},"anchorColumn":{"type":["string","null"],"description":"Native Spalte, nach der es in der Liste einsortiert wird; null heisst ans Ende"}},"required":["id","tenantId","entity","fieldId","fieldLabel","pgType","defaultValue","required","createdAt","fieldIconType","showInList","showOnDocuments","sortOrder","groupName","helpText","scope","ownerUserId","options","anchorFieldId","anchorColumn"],"description":"Ein eigenes Feld, wie es in der Registry steht"},"example":{"id":"string","tenantId":"string","entity":"customers","fieldId":"string","fieldLabel":"string","pgType":"text","defaultValue":"string","required":true,"createdAt":"string","fieldIconType":"string","showInList":true,"showOnDocuments":true,"sortOrder":0,"groupName":"string","helpText":"string","scope":"tenant","ownerUserId":"string","options":["string"],"anchorFieldId":"string","anchorColumn":"string"}}}},"400":{"description":"Bereich nicht freigegeben"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"getApiV1Custom-fieldsByEntityByFieldId","tags":["custom-fields"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true},{"schema":{"type":"string"},"in":"path","name":"fieldId","required":true}],"summary":"Liest ein einzelnes eigenes Feld","description":"Liest einen Registry-Eintrag ueber Bereich und Feldname und liefert ihn ohne Umschlag, also nicht unter `data`. Anders als die Listen filtert dieser Abruf NICHT nach Sichtbarkeit: auch das persoenliche Feld eines Kollegen wird ausgeliefert, wenn sein Feldname bekannt ist. Ein nicht freigegebener Bereich fuehrt zu 400, ein unbekanntes Feld zu 404."},"delete":{"responses":{"200":{"description":"Ergebnis des Loeschens","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Der Registry-Eintrag wurde entfernt"},"fieldId":{"type":"string","description":"Das geloeschte Feld"},"transferred":{"type":"integer","description":"Wie viele Werte vor dem Loeschen in die Notiz uebernommen wurden; 0 ohne transferToNotes"}},"required":["ok","fieldId","transferred"],"description":"Ergebnis des Loeschens"},"example":{"ok":true,"fieldId":"string","transferred":0}}}},"400":{"description":"Bereich nicht freigegeben oder unzulaessiger Feldname"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}},"operationId":"deleteApiV1Custom-fieldsByEntityByFieldId","tags":["custom-fields"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true},{"schema":{"type":"string"},"in":"path","name":"fieldId","required":true}],"summary":"Löscht ein Custom-Feld (Registry-Eintrag + echte Spalte)","description":"Entfernt zuerst den Registry-Eintrag — das ist der massgebliche Schritt — und laesst danach die echte Spalte fallen. Der zweite Schritt ist nachrangig: scheitert er, ist das Feld aus der Oberflaeche trotzdem verschwunden und die Spalte bleibt verwaist stehen. Die im JSONB custom_fields gespeicherten WERTE werden nicht mitgeloescht. Mit dem Abfrageparameter `transferToNotes` werden sie vorher zeilenweise als „Label: Wert\" an das Notizfeld angehaengt; wie viele das waren, steht in `transferred`. Zusaetzlich wird die Fassung im Manifest stillgelegt und der Pflichtfeld-Zwischenspeicher dieses Bereichs geleert, damit das Feld nicht bis zu einer Minute weiter als Pflicht gilt. Ein fremdes persoenliches Feld: 403; ein unbekanntes: 404."}},"/api/v1/custom-fields/{entity}/{fieldId}/usage":{"get":{"responses":{"200":{"description":"Befuellte und alle Zeilen","content":{"application/json":{"schema":{"type":"object","properties":{"filled":{"type":"integer","description":"Nicht geloeschte Zeilen mit einem nicht-leeren Wert in diesem Feld"},"total":{"type":"integer","description":"Nicht geloeschte Zeilen des Bereichs insgesamt"}},"required":["filled","total"],"description":"Belegungs-Zaehler — bei einem Lesefehler bewusst 0 von 0 statt eines Fehlers"},"example":{"filled":0,"total":0}}}},"400":{"description":"Bereich nicht freigegeben oder unzulaessiger Feldname"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Custom-fieldsByEntityByFieldIdUsage","tags":["custom-fields"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true},{"schema":{"type":"string"},"in":"path","name":"fieldId","required":true}],"summary":"Belegungs-Zähler eines Custom-Felds","description":"Zaehlt in EINER Abfrage, wie viele nicht geloeschte Zeilen des Bereichs einen nicht-leeren Wert in diesem Feld tragen und wie viele es insgesamt gibt — Grundlage fuer den Hinweis „X von Y befuellt\" vor dem Loeschen. Gelesen wird das JSONB custom_fields. Der Zaehler ist nachrangig: fehlt der Wertspeicher oder scheitert die Abfrage, kommt 0 von 0 mit Status 200 statt eines Fehlers — 0 von 0 heisst hier also nicht zwingend „leer\"."}},"/api/v1/custom-fields/{entity}/{fieldId}/meta":{"patch":{"responses":{"200":{"description":"Der Eintrag nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Registry-Zeile"},"tenantId":{"type":"string","description":"Mandant, dem das Feld gehoert"},"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"],"description":"Bereich, an dem das Feld haengt"},"fieldId":{"type":"string","description":"Spaltenname in der Tabelle — beim Anlegen auf den kunde_-Namensraum gehoben"},"fieldLabel":{"type":"string","description":"Anzeigename in der Oberflaeche"},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"],"description":"Datenbanktyp der Spalte"},"defaultValue":{"type":["string","null"],"description":"Vorbelegung; null wenn keine gesetzt ist"},"required":{"type":"boolean","description":"Ob das Feld beim Speichern verlangt wird"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"fieldIconType":{"type":["string","null"],"description":"Darstellungsart in der Maske; null wenn keine gewaehlt wurde"},"showInList":{"type":"boolean","description":"Ob das Feld als Spalte in der Liste erscheint"},"showOnDocuments":{"type":"boolean","description":"Ob der Wert im Kunden-Infoblock der Belege gedruckt wird"},"sortOrder":{"type":"number","description":"Position innerhalb der eigenen Felder"},"groupName":{"type":["string","null"],"description":"Gruppenueberschrift in der Maske; null wenn ungruppiert"},"helpText":{"type":["string","null"],"description":"Hilfetext am Feld; null wenn keiner gepflegt ist"},"scope":{"type":"string","enum":["tenant","user"],"description":"tenant = fuer alle sichtbar, user = nur fuer den Ersteller"},"ownerUserId":{"type":["string","null"],"description":"Ersteller bei einem persoenlichen Feld; sonst null"},"options":{"type":["array","null"],"items":{"type":"string"},"description":"Auswahlwerte bei einem Auswahlfeld; sonst null"},"anchorFieldId":{"type":["string","null"],"description":"Feld, unter dem es in der Maske erscheint; null heisst Block „Eigene Felder\""},"anchorColumn":{"type":["string","null"],"description":"Native Spalte, nach der es in der Liste einsortiert wird; null heisst ans Ende"}},"required":["id","tenantId","entity","fieldId","fieldLabel","pgType","defaultValue","required","createdAt","fieldIconType","showInList","showOnDocuments","sortOrder","groupName","helpText","scope","ownerUserId","options","anchorFieldId","anchorColumn"],"description":"Ein eigenes Feld, wie es in der Registry steht"},"example":{"id":"string","tenantId":"string","entity":"customers","fieldId":"string","fieldLabel":"string","pgType":"text","defaultValue":"string","required":true,"createdAt":"string","fieldIconType":"string","showInList":true,"showOnDocuments":true,"sortOrder":0,"groupName":"string","helpText":"string","scope":"tenant","ownerUserId":"string","options":["string"],"anchorFieldId":"string","anchorColumn":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}},"operationId":"patchApiV1Custom-fieldsByEntityByFieldIdMeta","tags":["custom-fields"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true},{"schema":{"type":"string"},"in":"path","name":"fieldId","required":true}],"summary":"Aktualisiert Anzeige-Metadaten eines Custom-Felds","description":"Aendert NUR den Registry-Eintrag — Anzeigename, Darstellungsart, Listen-Sichtbarkeit, Sortierung, Gruppe, Hilfetext, Auswahlwerte und Sichtbarkeits-Bereich. Es laeuft kein ALTER TABLE; Datenbanktyp und Spaltenname bleiben, dafuer sind POST und DELETE zustaendig. Uebernommen werden nur die mitgeschickten Felder. Ein persoenliches Feld darf nur sein Ersteller oder ein Admin aendern, sonst 403; ein unbekanntes Feld ergibt 404. Werden Auswahlwerte mitgeschickt, wird zusaetzlich die abgeleitete Rabatt-Tabelle nachgezogen. Die Antwort ist der geaenderte Eintrag ohne Umschlag.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"fieldLabel":{"type":"string","maxLength":255},"fieldIconType":{"type":["string","null"],"maxLength":40},"showInList":{"type":"boolean"},"showOnDocuments":{"type":"boolean"},"sortOrder":{"type":"integer"},"groupName":{"type":["string","null"],"maxLength":120},"helpText":{"type":["string","null"]},"scope":{"type":"string","enum":["tenant","user"]},"options":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":120},"maxItems":50},"anchorFieldId":{"type":["string","null"],"maxLength":64},"anchorColumn":{"type":["string","null"],"maxLength":64}}},"example":{"fieldLabel":"string","fieldIconType":"string","showInList":true,"showOnDocuments":true,"sortOrder":0,"groupName":"string","helpText":"string","scope":"tenant","options":["string"],"anchorFieldId":"string","anchorColumn":"string"}}}}}},"/api/v1/custom-fields/{entity}/{fieldId}/revert":{"post":{"responses":{"200":{"description":"Ergebnis der Ruecksetzung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Ruecksetzung lief durch"},"fieldId":{"type":"string","description":"Das zurueckgesetzte Feld"},"revertedToVersion":{"type":"integer","description":"Die Manifest-Fassung, auf die zurueckgesetzt wurde"},"pgTypeKept":{"type":"boolean","description":"true, wenn die alte Fassung einen anderen Datenbanktyp trug — der bleibt bestehen, umgestellt wird nur die Darstellung"},"registry":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Registry-Zeile"},"tenantId":{"type":"string","description":"Mandant, dem das Feld gehoert"},"entity":{"type":"string","enum":["customers","orders","invoices","products","contacts","employees","projects","quotes","deliveries","tickets","contracts","tasks","documents","timesheets","rma","assets","opportunities","purchase_orders","dunning","credit_notes","incoming_invoices","blanket_orders","nonconformance","aufmass"],"description":"Bereich, an dem das Feld haengt"},"fieldId":{"type":"string","description":"Spaltenname in der Tabelle — beim Anlegen auf den kunde_-Namensraum gehoben"},"fieldLabel":{"type":"string","description":"Anzeigename in der Oberflaeche"},"pgType":{"type":"string","enum":["text","integer","numeric","boolean","date","timestamptz","jsonb"],"description":"Datenbanktyp der Spalte"},"defaultValue":{"type":["string","null"],"description":"Vorbelegung; null wenn keine gesetzt ist"},"required":{"type":"boolean","description":"Ob das Feld beim Speichern verlangt wird"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"fieldIconType":{"type":["string","null"],"description":"Darstellungsart in der Maske; null wenn keine gewaehlt wurde"},"showInList":{"type":"boolean","description":"Ob das Feld als Spalte in der Liste erscheint"},"showOnDocuments":{"type":"boolean","description":"Ob der Wert im Kunden-Infoblock der Belege gedruckt wird"},"sortOrder":{"type":"number","description":"Position innerhalb der eigenen Felder"},"groupName":{"type":["string","null"],"description":"Gruppenueberschrift in der Maske; null wenn ungruppiert"},"helpText":{"type":["string","null"],"description":"Hilfetext am Feld; null wenn keiner gepflegt ist"},"scope":{"type":"string","enum":["tenant","user"],"description":"tenant = fuer alle sichtbar, user = nur fuer den Ersteller"},"ownerUserId":{"type":["string","null"],"description":"Ersteller bei einem persoenlichen Feld; sonst null"},"options":{"type":["array","null"],"items":{"type":"string"},"description":"Auswahlwerte bei einem Auswahlfeld; sonst null"},"anchorFieldId":{"type":["string","null"],"description":"Feld, unter dem es in der Maske erscheint; null heisst Block „Eigene Felder\""},"anchorColumn":{"type":["string","null"],"description":"Native Spalte, nach der es in der Liste einsortiert wird; null heisst ans Ende"}},"required":["id","tenantId","entity","fieldId","fieldLabel","pgType","defaultValue","required","createdAt","fieldIconType","showInList","showOnDocuments","sortOrder","groupName","helpText","scope","ownerUserId","options","anchorFieldId","anchorColumn"],"description":"Die Registry-Zeile nach der Ruecksetzung"}},"required":["ok","fieldId","revertedToVersion","pgTypeKept","registry"],"description":"Ergebnis der Ruecksetzung"},"example":{"ok":true,"fieldId":"string","revertedToVersion":0,"pgTypeKept":true,"registry":{"id":"string","tenantId":"string","entity":"customers","fieldId":"string","fieldLabel":"string","pgType":"text","defaultValue":"string","required":true,"createdAt":"string","fieldIconType":"string","showInList":true,"showOnDocuments":true,"sortOrder":0,"groupName":"string","helpText":"string","scope":"tenant","ownerUserId":"string","options":["string"],"anchorFieldId":"string","anchorColumn":"string"}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}},"operationId":"postApiV1Custom-fieldsByEntityByFieldIdRevert","tags":["custom-fields"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true},{"schema":{"type":"string"},"in":"path","name":"fieldId","required":true}],"summary":"Setzt die Anzeige-Angaben eines eigenen Feldes auf eine frühere Fassung zurück","description":"Liest die gewaehlte Fassung aus dem Aenderungs-Manifest und schreibt daraus Anzeigename, Darstellungsart, Listen-Sichtbarkeit und Auswahlwerte in den Registry-Eintrag zurueck. Der Datenbanktyp wird bewusst NICHT zurueckgesetzt, weil das die Spalte umbauen wuerde; wich er ab, meldet die Antwort `pgTypeKept: true`. Die Ruecksetzung wird selbst als neue Manifest-Fassung festgehalten — der Verlauf waechst also, statt zurueckzuspringen. Unbekannte Fassung oder unbekanntes Feld: 404; ein fremdes persoenliches Feld: 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"integer","exclusiveMinimum":0}},"required":["version"]},"example":{"version":1}}}}}},"/api/v1/custom-entities":{"get":{"responses":{"200":{"description":"Eigene Entitaeten des Mandanten samt Feldzahl. Sind die Registry-Tabellen noch nicht angelegt (frischer Mandant), kommt eine leere Liste — kein Fehler.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"icon":{"type":["string","null"]},"table_name":{"type":"string"},"status":{"type":"string","enum":["active","disabled","archived"]},"source":{"type":["string","null"],"enum":["user","industry_pack","migration",null]},"source_pack_slug":{"type":["string","null"]},"row_count":{"type":["number","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"field_count":{"type":"number"}},"required":["id","tenant_id","slug","name","description","icon","table_name","status","source","source_pack_slug","row_count","created_by","created_at","updated_at","field_count"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","tenant_id":"string","slug":"string","name":"string","description":"string","icon":"string","table_name":"string","status":"active","source":"user","source_pack_slug":"string","row_count":0,"created_by":"string","created_at":"string","updated_at":"string","field_count":0}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Custom-entities","tags":["custom-entities"],"parameters":[],"description":"Listet die Entitaeten des Mandanten samt Anzahl aktiver Felder. Ein frischer Mandant, dessen Registry-Tabellen noch fehlen, bekommt eine leere Liste statt eines Fehlers. Es wird nicht geblaettert und nicht gefiltert; die Felder selbst sind nicht dabei — die liefert `GET /custom-entities/{id}`.","summary":"Listet die Entitaeten des Mandanten samt Anzahl aktiver Felder","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Kennung der Entitaet und Name der angelegten Tabelle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der angelegten Entitaet"},"tableName":{"type":"string","description":"Name der physischen Tabelle im Mandanten-Schema"}},"required":["id","tableName"],"description":"Die Entitaet ist angelegt, die Tabelle steht"},"example":{"id":"string","tableName":"string"}}}},"400":{"description":"Slug entspricht nicht dem Muster oder die Pruefung schlug fehl"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Manager-Rolle erforderlich"},"409":{"description":"Quota or conflict"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"postApiV1Custom-entities","tags":["custom-entities"],"parameters":[],"summary":"Legt eine neue Custom-Entity an (inkl. Dynamic-Table)","description":"Traegt die Entitaet in `public.custom_entities` ein UND legt im Schema des Mandanten die physische Tabelle `custom_<slug>` an — mit `id`, `tenant_id`, `created_at`, `updated_at`, `created_by`, `deleted_at`, eingeschalteter Zeilensicherheit und einer Mandantenregel. Beides laeuft in EINER Transaktion: scheitert die Tabelle, bleibt auch der Eintrag weg. Fachliche Felder entstehen dabei nicht; die kommen ueber die Feldverwaltung dazu.\n\nDer Slug ist nach dem Anlegen fest, weil der Tabellenname daran haengt, und muss dem Muster Kleinbuchstabe gefolgt von 1–63 Kleinbuchstaben, Ziffern oder `_` entsprechen (400 sonst). Je Mandant sind hoechstens 50 aktive Entitaeten zulaessig — der 51. Versuch endet mit 409, bevor irgendetwas geschrieben wird.\n\nNebenwirkung: der Vorgang wird zusaetzlich im Anpassungs-Verzeichnis vermerkt (nachrangig — schlaegt das fehl, gilt das Anlegen trotzdem). Zurueck kommen nur Kennung und Tabellenname. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$"},"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":2000},"icon":{"type":"string","maxLength":120}},"required":["slug","name"]}}}}}},"/api/v1/custom-entities/{id}":{"get":{"responses":{"200":{"description":"Kopfdaten der Entitaet samt aktiver Felder","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"icon":{"type":["string","null"]},"table_name":{"type":"string"},"status":{"type":"string","enum":["active","disabled","archived"]},"source":{"type":["string","null"],"enum":["user","industry_pack","migration",null]},"source_pack_slug":{"type":["string","null"]},"row_count":{"type":["number","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"fields":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die aktiven Felder der Entitaet, nach Position und Beschriftung sortiert"}},"required":["id","tenant_id","slug","name","description","icon","table_name","status","source","source_pack_slug","row_count","created_by","created_at","updated_at","fields"],"additionalProperties":true,"description":"Kopfdaten der Entitaet samt ihrer aktiven Felder"},"example":{"id":"string","tenant_id":"string","slug":"string","name":"string","description":"string","icon":"string","table_name":"string","status":"active","source":"user","source_pack_slug":"string","row_count":0,"created_by":"string","created_at":"string","updated_at":"string","fields":[{}]}}}},"401":{"description":"Kein Mandantenkontext"},"404":{"description":"`entity_not_found` — auch wenn sie einem anderen Mandanten gehoert"}},"operationId":"getApiV1Custom-entitiesById","tags":["custom-entities"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine eigene Entitaet samt ihrer aktiven Felder lesen","description":"Liefert die Kopfdaten EINER Entitaet und darunter ihre AKTIVEN Felder, nach Position und Beschriftung sortiert. Deaktivierte Felder fehlen. Anders als in der Liste kommen die Kopfdaten hier als vollstaendige Tabellenzeile und ohne `field_count`. Gesucht wird ausschliesslich im eigenen Mandanten; auch eine archivierte Entitaet wird geliefert."},"put":{"responses":{"200":{"description":"Bestaetigung; die geaenderte Entitaet kommt NICHT zurueck","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der geaenderten Entitaet — der Wert aus dem Pfad"},"updated":{"type":"boolean","const":true}},"required":["id","updated"]},"example":{"id":"string","updated":true}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Manager-Rolle erforderlich"},"404":{"description":"`entity_not_found`"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"putApiV1Custom-entitiesById","tags":["custom-entities"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aendert AUSSCHLIESSLICH Name, Beschreibung und Symbol — jedes Feld freiwillig, nur mitgegebene werden gesetzt. Slug, Tabellenname, Status und Felder bleiben unberuehrt; der Slug ist nach dem Anlegen fest, weil die physische Tabelle daran haengt. Die Antwort bestaetigt nur — sie enthaelt NICHT die geaenderte Entitaet. Trotz PUT ist das ein Teil-Patch, kein Ersetzen. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":2000},"icon":{"type":"string","maxLength":120}}},"example":{"name":"string","description":"string","icon":"string"}}}},"summary":"Aendert AUSSCHLIESSLICH Name, Beschreibung und Symbol","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Bestaetigung; Tabelle und Datensaetze bleiben bestehen","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der archivierten Entitaet — der Wert aus dem Pfad"},"archived":{"type":"boolean","const":true}},"required":["id","archived"]},"example":{"id":"string","archived":true}}}},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Manager-Rolle erforderlich"},"404":{"description":"`entity_not_found`"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"deleteApiV1Custom-entitiesById","tags":["custom-entities"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt die Entitaet auf `status = archived`. Die physische Tabelle und alle Datensaetze BLEIBEN bestehen, es wird nichts geloescht — die Entitaet verschwindet nur aus der Liste. Eine Route zum Zurueckholen gibt es hier nicht. Die Antwort bestaetigt nur. Erfordert mindestens die Rolle `manager`.","summary":"Setzt die Entitaet auf `status = archived`","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/custom-entities/{id}/rows":{"get":{"responses":{"200":{"description":"Datensaetze der Seite plus Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Datensaetze der Seite, neueste zuerst. Die Spalten haengen an der Entitaet; zu jedem Beziehungsfeld kommt zusaetzlich `<feld>__label` mit der Beschriftung des Ziels"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller nicht geloeschten Datensaetze der Entitaet"}},"required":["rows","total"]},"example":{"rows":[{}],"total":0}}}},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"`tenant_mismatch` — die Entitaet gehoert einem anderen Mandanten"},"404":{"description":"`entity_not_found`"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"getApiV1Custom-entitiesByIdRows","tags":["custom-entities"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Datensaetze einer eigenen Entitaet blaettern","description":"Blaettert durch die Datensaetze EINER Entitaet, neueste zuerst; soft-geloeschte bleiben aussen vor. `limit` (Vorgabe 50, Obergrenze 200) und `offset` blaettern — unlesbare Werte fallen still auf die Vorgabe zurueck. Es gibt keinen Filter und keine Sortierung.\n\nDie Spalten haengen an der Entitaet. Zusaetzlich werden Beziehungs-, Nachschlage- und Summenfelder aufgeloest: zu jedem Beziehungsfeld kommt `<feld>__label` mit der Beschriftung des Ziels. Das Aufloesen ist nachrangig — ein fehlendes Ziel laesst die Beschriftung einfach weg."},"post":{"responses":{"201":{"description":"Kennung des angelegten Datensatzes","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des angelegten Datensatzes"}},"required":["id"]},"example":{"id":"string"}}}},"400":{"description":"Validation"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"`tenant_mismatch` — die Entitaet gehoert einem anderen Mandanten"},"404":{"description":"`entity_not_found`"},"422":{"description":"Eine Pflicht- oder Pruefregel der Entitaet ist verletzt"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"postApiV1Custom-entitiesByIdRows","tags":["custom-entities"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Legt einen Datensatz in der Tabelle der Entitaet an. Die Werte stehen unter `data`, geschluesselt nach Feld-Slug; unbekannte Schluessel und Regelverstoesse werden abgelehnt (422 bei einer verletzten Pflicht- oder Pruefregel, 400 bei unzulaessiger Form). Anders als das Anlegen der Entitaet selbst braucht das KEINE Manager-Rolle. Zurueck kommt nur die Kennung, nicht der Datensatz.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"]},"example":{"data":{}}}}},"summary":"Legt einen Datensatz in der Tabelle der Entitaet an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/custom-entities/{id}/rows/{rowId}":{"put":{"responses":{"200":{"description":"Bestaetigung; der geaenderte Datensatz kommt NICHT zurueck","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Datensatzes — der Wert aus dem Pfad"},"updated":{"type":"boolean","const":true}},"required":["id","updated"]},"example":{"id":"string","updated":true}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"`tenant_mismatch` — die Entitaet gehoert einem anderen Mandanten"},"404":{"description":"`row_not_found` oder `entity_not_found`"},"422":{"description":"Eine Pflicht- oder Pruefregel der Entitaet ist verletzt"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"putApiV1Custom-entitiesByIdRowsByRowId","tags":["custom-entities"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"rowId","required":true}],"description":"Aendert einen Datensatz. Gesetzt werden NUR die unter `data` mitgegebenen Felder; nicht genannte behalten ihren Wert. Es gelten dieselben Regeln wie beim Anlegen (422 bei einer verletzten Pflicht- oder Pruefregel). Die Antwort bestaetigt nur — sie enthaelt NICHT den geaenderten Datensatz. Ein soft-geloeschter Datensatz wird nicht getroffen und ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"]},"example":{"data":{}}}}},"summary":"Aendert einen Datensatz","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Bestaetigung; die Zeile bleibt bestehen und ist nur ausgeblendet","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Datensatzes — der Wert aus dem Pfad"},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"]},"example":{"id":"string","deleted":true}}}},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"`tenant_mismatch` — die Entitaet gehoert einem anderen Mandanten"},"404":{"description":"`row_not_found` — unbekannt ODER bereits geloescht"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"deleteApiV1Custom-entitiesByIdRowsByRowId","tags":["custom-entities"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"rowId","required":true}],"description":"Setzt `deleted_at` auf dem Datensatz — die Zeile bleibt in der Tabelle und verschwindet nur aus `GET /custom-entities/{id}/rows`. Eine Route zum Zurueckholen gibt es hier nicht. Der Aufruf ist NICHT wiederholbar: ein bereits geloeschter Datensatz wird nicht mehr getroffen und ergibt 404. Die Antwort bestaetigt nur.","summary":"Setzt `deleted_at` auf dem Datensatz","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/custom-entities/{entityId}/fields":{"post":{"responses":{"201":{"description":"Feld angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des neuen Feldes in public.custom_fields_v2"}},"required":["id"]},"example":{"id":"string"}}}},"400":{"description":"Ungültiger Slug, ungültiges Relationsziel oder ungültige Formel"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Entität gehört einem anderen Mandanten oder Rolle unter „manager\""},"404":{"description":"Entität nicht gefunden"},"409":{"description":"Obergrenze von 100 aktiven Feldern erreicht"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"postApiV1Custom-entitiesByEntityIdFields","tags":["custom-fields-v2"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entityId","required":true}],"summary":"Feld an eine Custom-Entität anhängen (legt auch die Spalte an)","description":"Trägt das Feld in `public.custom_fields_v2` ein und hängt in derselben Transaktion eine Spalte an die physische Tabelle der Entität an. Der Slug muss snake_case sein und die Entität dem aufrufenden Mandanten gehören — sonst 400 bzw. 403/404; je Entität sind höchstens 100 aktive Felder erlaubt, darüber kommt 409. Weil es eine Schemaänderung ist, verlangt der Aufruf mindestens die Rolle „manager\"; zurück kommt allein die Kennung des neuen Feldes.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$"},"label":{"type":"string","minLength":1,"maxLength":120},"fieldType":{"type":"string","enum":["text","longtext","number","integer","decimal","date","datetime","bool","select","multi_select","relation","computed","lookup","rollup","file","json","email","phone","url"]},"options":{"type":"object","additionalProperties":{}},"required":{"type":"boolean"},"defaultValue":{"type":"string","maxLength":2000},"validationRegex":{"type":"string","maxLength":500},"helpText":{"type":"string","maxLength":2000},"position":{"type":"integer","minimum":0,"maximum":10000},"targetEntitySlug":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$"},"formula":{"type":"string","maxLength":2000}},"required":["slug","label","fieldType"]}}}}}},"/api/v1/custom-entities/{entityId}/fields/{fieldId}":{"put":{"responses":{"200":{"description":"Metadaten geändert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"updated":{"type":"boolean","const":true}},"required":["id","updated"]},"example":{"id":"string","updated":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Rolle unter „manager\""},"404":{"description":"Feld nicht gefunden"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"putApiV1Custom-entitiesByEntityIdFieldsByFieldId","tags":["custom-fields-v2"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entityId","required":true},{"schema":{"type":"string"},"in":"path","name":"fieldId","required":true}],"description":"Ändert ausschließlich die Metadaten eines Feldes: Beschriftung, Pflichtangabe, Vorgabewert, Prüfmuster, Hilfetext, Position und `options`. Weder Spalte noch Feldtyp werden angefasst, es läuft kein ALTER TABLE. Nicht gesendete Felder bleiben stehen; passt die Feldkennung nicht zu Entität und Mandant, kommt 404. Zurück kommt nur eine Bestätigung mit der Kennung, nicht der geänderte Datensatz.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":120},"required":{"type":"boolean"},"defaultValue":{"type":"string","maxLength":2000},"validationRegex":{"type":"string","maxLength":500},"helpText":{"type":"string","maxLength":2000},"position":{"type":"integer","minimum":0,"maximum":10000},"options":{"type":"object","additionalProperties":{}}}},"example":{"label":"string","required":true,"defaultValue":"string","validationRegex":"string","helpText":"string","position":0,"options":{}}}}},"summary":"Ändert ausschließlich die Metadaten eines Feldes","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Feld deaktiviert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"disabled":{"type":"boolean","const":true}},"required":["id","disabled"]},"example":{"id":"string","disabled":true}}}},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Rolle unter „manager\""},"404":{"description":"Feld nicht gefunden"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"deleteApiV1Custom-entitiesByEntityIdFieldsByFieldId","tags":["custom-fields-v2"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entityId","required":true},{"schema":{"type":"string"},"in":"path","name":"fieldId","required":true}],"description":"Setzt `status = disabled` in public.custom_fields_v2. Die Spalte und alle darin gespeicherten Werte bleiben physisch erhalten — es wird nichts gelöscht und nichts migriert —, das Feld zählt aber nicht mehr gegen die Obergrenze von 100 aktiven Feldern je Entität. Passt die Kennung nicht zu Entität und Mandant, kommt 404.","summary":"Setzt `status = disabled` in public.custom_fields_v2","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/industry-packs":{"get":{"responses":{"200":{"description":"Alle Pakete der Registry","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"Kennung des Pakets"},"name":{"type":"string","description":"Anzeigename"},"description":{"type":["string","null"],"description":"Beschreibung; null wenn keine hinterlegt ist"},"icon":{"type":["string","null"],"description":"Symbolname; null wenn keiner hinterlegt ist"},"industry_category":{"type":["string","null"],"description":"Branchengruppe; null wenn keine gesetzt ist"},"custom_entities_json":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Entitaeten, die das Paket anlegt (slug, name, ggf. description und icon)"},"custom_fields_json":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Felder, die das Paket anlegt (entity, slug, label, fieldType, …)"},"version":{"type":["string","null"],"description":"Version des Pakets; null wenn keine gepflegt ist"}},"required":["slug","name","description","icon","industry_category","custom_entities_json","custom_fields_json","version"]},"description":"Alle Pakete der Registry, nach Namen sortiert"}},"required":["data"]},"example":{"data":[{"slug":"string","name":"string","description":"string","icon":"string","industry_category":"string","custom_entities_json":[{}],"custom_fields_json":[{}],"version":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen"}},"operationId":"getApiV1Industry-packs","tags":["industry-packs"],"parameters":[],"description":"Liest die Paket-Registry aus `public.industry_packs`, nach Namen sortiert. Der Katalog ist mandantenuebergreifend: was hier steht, gilt fuer alle. Ob der aufrufende Mandant ein Paket installiert hat, steht NICHT dabei — dafuer `GET /industry-packs/installed`. Fehlt die Registry-Tabelle, legt der Abruf sie leer an und antwortet mit einer leeren Liste. Ohne Datenbankverbindung kommt ebenfalls eine leere Liste, kein Fehler.","summary":"Liest die Paket-Registry aus `public.industry_packs`, nach Namen sortiert","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/industry-packs/installed":{"get":{"responses":{"200":{"description":"Installationszeilen des Mandanten, aktive und deaktivierte","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"pack_slug":{"type":"string","description":"Kennung des Pakets"},"installed_at":{"type":"string","description":"Zeitpunkt der Installation"},"installed_by":{"type":["string","null"],"description":"Anwender, der installiert hat; null wenn nicht erfasst"},"status":{"type":"string","description":"Zustand der Installation; nach dem Deinstallieren `disabled` — die Zeile bleibt stehen"}},"required":["pack_slug","installed_at","installed_by","status"]},"description":"Alle Installationszeilen des Mandanten, neueste zuerst — auch die auf `disabled` gesetzten"}},"required":["data"]},"example":{"data":[{"pack_slug":"string","installed_at":"string","installed_by":"string","status":"string"}]}}}},"401":{"description":"Kein Mandantenkontext"},"500":{"description":"Abfrage fehlgeschlagen"}},"operationId":"getApiV1Industry-packsInstalled","tags":["industry-packs"],"parameters":[],"summary":"Installierte Branchenpakete des Mandanten — auch die deaktivierten","description":"Liest die Installationszeilen des Mandanten aus `public.tenant_industry_pack_install`, neueste zuerst. ACHTUNG: die Liste ist NICHT auf aktive Pakete gefiltert — ein deinstalliertes Paket steht weiter drin, erkennbar an `status: \"disabled\"`. Wer nur die aktiven will, muss selbst filtern. Zu jedem Eintrag kommen nur Kennung, Zeitpunkt, Installierender und Zustand, nicht die Paketdaten selbst."}},"/api/v1/industry-packs/{slug}/install":{"post":{"responses":{"200":{"description":"Bilanz des Laufs — auch mit Fehlern in `errors`","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"entitiesCreated":{"type":"integer","minimum":0,"description":"Angelegte Entitaeten"},"fieldsCreated":{"type":"integer","minimum":0,"description":"Angelegte Felder"},"errors":{"type":"array","items":{"type":"object","properties":{"scope":{"type":"string","enum":["entity","field"],"description":"Woran es scheiterte"},"ref":{"type":"string","description":"Kennung der betroffenen Entitaet bzw. des Feldes"},"message":{"type":"string","description":"Grund im Klartext"}},"required":["scope","ref","message"]},"description":"Uebersprungene Teile. Nicht leer heisst: das Paket ist nur TEILWEISE eingerichtet"}},"required":["entitiesCreated","fieldsCreated","errors"],"description":"Bilanz des Installationslaufs"}},"required":["data"]},"example":{"data":{"entitiesCreated":0,"fieldsCreated":0,"errors":[{"scope":"entity","ref":"string","message":"string"}]}}}}},"401":{"description":"Kein Mandantenkontext"},"404":{"description":"Kein Paket mit diesem slug"},"500":{"description":"Installation fehlgeschlagen"}},"operationId":"postApiV1Industry-packsBySlugInstall","tags":["industry-packs"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"description":"Richtet ein Paket fuer den Mandanten ein: legt seine Entitaeten an und haengt seine Felder als echte Spalten an die Tabellen im Schema `tenant_<slug>` (ALTER TABLE ADD COLUMN IF NOT EXISTS), danach den Eintrag in der Feld-Registry. Der Lauf ist wiederholbar und laeuft OHNE Transaktion: was scheitert, wird uebersprungen und in `errors` genannt — die Antwort bleibt trotzdem 200. Eine nicht leere `errors`-Liste heisst also: das Paket ist nur teilweise eingerichtet. Fehlt die Zieltabelle einer Entitaet, wird das Feld ausgelassen, OHNE eine Registry-Zeile zu schreiben. Ein unbekanntes Paket ergibt 404.","summary":"Richtet ein Paket fuer den Mandanten ein","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Die Installationszeile steht auf disabled","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"description":"Die Installationszeile steht jetzt auf `disabled`"}},"required":["data"]},"example":{"data":{"ok":true}}}}},"401":{"description":"Kein Mandantenkontext"},"404":{"description":"Keine Installationszeile zu diesem Paket"},"500":{"description":"Deinstallation fehlgeschlagen"}},"operationId":"deleteApiV1Industry-packsBySlugInstall","tags":["industry-packs"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"description":"Setzt die Installationszeile des Mandanten auf `status = disabled`. Mehr passiert NICHT: die vom Paket angelegten Spalten, Entitaeten und Registry-Eintraege bleiben bestehen, und die vorhandenen Daten bleiben unangetastet. Auch die Zeile selbst bleibt stehen und erscheint weiter in `GET /industry-packs/installed`. Gibt es keine Zeile zu diesem Paket, kommt 404; ein zweiter Aufruf auf eine bereits deaktivierte Zeile ergibt wieder 200.","summary":"Setzt die Installationszeile des Mandanten auf `status = disabled`","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ui-configs/{page}":{"get":{"responses":{"200":{"description":"Gespeichertes Layout","content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"string"},"config":{"type":["object","null"],"additionalProperties":{}},"version":{"type":"number"},"updatedBy":{"type":["string","null"]},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["page","config","version","updatedBy","updatedAt"]},"example":{"page":"string","config":{},"version":0,"updatedBy":"string","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"getApiV1Ui-configsByPage","tags":["ui-configs"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"page","required":true}],"description":"Liefert das gespeicherte Layout einer Seite. Gelesen wird die Zeile mit passendem `page` aus `ui_configs` im Mandantenschema; Tabelle und Spalten werden dabei einmal je Schema und Prozess selbstheilend angelegt oder nachgeruestet. `page` muss aus Kleinbuchstaben, Ziffern, Bindestrich und Unterstrich bestehen, sonst 400. Existiert keine Zeile, antwortet die Route mit 404 und einem Rumpf aus Leerwerten (`config: null`, `version: 0`).","summary":"Liefert das gespeicherte Layout einer Seite","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Layout gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"string"},"version":{"type":"number"},"updatedBy":{"type":["string","null"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"widgetCount":{"type":"number"}},"required":["page","version","updatedBy","updatedAt","widgetCount"]},"example":{"page":"string","version":0,"updatedBy":"string","updatedAt":"2026-01-01T12:00:00.000Z","widgetCount":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}},"operationId":"putApiV1Ui-configsByPage","tags":["ui-configs"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"page","required":true}],"description":"Speichert das Layout einer Seite (Upsert). Geschrieben werden `config` und `config_jsonb` in `ui_configs`; `version` steigt bei jedem Schreibvorgang um eins und `updated_by` bekommt die Nutzerkennung aus dem Kontext. Wird `expectedVersion` mitgeschickt und passt nicht zur gespeicherten Version, antwortet die Route mit 409 und der tatsaechlichen Version, ohne zu schreiben. Ein ungueltiger Rumpf ergibt 422, ein ungueltiges `page` 400.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"config":{"type":"object","properties":{"layout":{"type":"string","enum":["grid","masonry"],"default":"grid"},"columns":{"type":"integer","minimum":1,"maximum":4,"default":4},"widgets":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"kpi"},"id":{"type":"string"},"title":{"type":"string"},"metric":{"type":"string","enum":["revenue","orders_count","customers_new","invoices_open","inventory_low","projects_active"]},"period":{"type":"string","enum":["today","week","month","quarter","year"]},"comparison":{"type":"boolean","default":true},"position":{"type":"object","properties":{"col":{"type":"integer","minimum":0,"maximum":3},"row":{"type":"integer","minimum":0}},"required":["col","row"]},"size":{"type":"string","enum":["sm","md","lg"],"default":"md"}},"required":["type","id","title","metric","period","position"]},{"type":"object","properties":{"type":{"type":"string","const":"table"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string","enum":["orders","customers","invoices","inventory","projects","leads"]},"columns":{"type":"array","items":{"type":"string"},"maxItems":8},"sort":{"type":"string"},"filter":{"type":"string"},"limit":{"type":"integer","minimum":5,"maximum":50,"default":10},"position":{"type":"object","properties":{"col":{"type":"integer","minimum":0},"row":{"type":"integer","minimum":0}},"required":["col","row"]}},"required":["type","id","title","source","columns","position"]},{"type":"object","properties":{"type":{"type":"string","const":"chart"},"id":{"type":"string"},"title":{"type":"string"},"chartType":{"type":"string","enum":["bar","line","pie","area","funnel"]},"metric":{"type":"string"},"groupBy":{"type":"string"},"period":{"type":"string","enum":["week","month","quarter","year"]},"position":{"type":"object","properties":{"col":{"type":"integer","minimum":0},"row":{"type":"integer","minimum":0}},"required":["col","row"]}},"required":["type","id","title","chartType","metric","period","position"]},{"type":"object","properties":{"type":{"type":"string","const":"alert"},"id":{"type":"string"},"title":{"type":"string"},"source":{"type":"string"},"condition":{"type":"string"},"severity":{"type":"string","enum":["info","warning","critical"]},"position":{"type":"object","properties":{"col":{"type":"integer","minimum":0},"row":{"type":"integer","minimum":0}},"required":["col","row"]}},"required":["type","id","title","source","condition","severity","position"]}]},"default":[]},"name":{"type":"string"}}},"expectedVersion":{"type":"integer","minimum":0}},"required":["config"]},"example":{"config":{"layout":"grid","columns":1,"widgets":[{"type":"kpi","id":"string","title":"string","metric":"revenue","period":"today","comparison":true,"position":{"col":0,"row":0},"size":"sm"}],"name":"string"},"expectedVersion":0}}}},"summary":"Speichert das Layout einer Seite (Upsert)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/list-configs/{entity}":{"get":{"responses":{"200":{"description":"Gespeicherte Spaltenkonfiguration, ggf. leer","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"filterable":{"type":"array","items":{"type":"string"},"description":"Spalten, die einen Filtertrichter bekommen"},"sortable":{"type":"array","items":{"type":"string"},"description":"Spalten, die sortierbar sein sollen"},"hidden":{"type":"array","items":{"type":"string"},"description":"Spalten, die die Liste nicht zeigt"},"order":{"type":"array","items":{"type":"string"},"description":"Spaltenreihenfolge von links nach rechts"}},"description":"Nur gesetzte Schluessel sind enthalten; ein fehlender heisst „nichts konfiguriert\""}},"required":["data"]},"example":{"data":{"filterable":["string"],"sortable":["string"],"hidden":["string"],"order":["string"]}}}}},"400":{"description":"Unbekannte Entität"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1List-configsByEntity","tags":["list-configs"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true}],"summary":"Spaltenkonfiguration einer Liste lesen","description":"Liest die Mandanten-Spaltenkonfiguration einer Liste aus `<mandant>.list_configs` — eine Zeile je Entität, die Konfiguration selbst liegt als JSONB. Die Entität im Pfad muss in derselben Whitelist stehen wie bei den Zusatzfeldern, sonst 400. Hat der Mandant nichts umgestellt — der Normalfall —, kommt ein leeres Objekt: dann gilt der Spaltenstand aus dem Code. Die Tabelle wird beim ersten Aufruf angelegt."},"put":{"responses":{"200":{"description":"Tatsächlich gespeicherte Spaltenkonfiguration","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"filterable":{"type":"array","items":{"type":"string"},"description":"Spalten, die einen Filtertrichter bekommen"},"sortable":{"type":"array","items":{"type":"string"},"description":"Spalten, die sortierbar sein sollen"},"hidden":{"type":"array","items":{"type":"string"},"description":"Spalten, die die Liste nicht zeigt"},"order":{"type":"array","items":{"type":"string"},"description":"Spaltenreihenfolge von links nach rechts"}},"description":"Nur gesetzte Schluessel sind enthalten; ein fehlender heisst „nichts konfiguriert\""}},"required":["data"]},"example":{"data":{"filterable":["string"],"sortable":["string"],"hidden":["string"],"order":["string"]}}}}},"400":{"description":"Unbekannte Entität"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Nicht gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer"}},"required":["error","retryAfter"]}}}}},"operationId":"putApiV1List-configsByEntity","tags":["list-configs"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true}],"description":"Ersetzt die Spaltenkonfiguration der Entität vollständig — `INSERT … ON CONFLICT (entity) DO UPDATE`, es wird nichts zusammengeführt. Der Rumpf wird nicht hart validiert, sondern normalisiert: nur die Schlüssel `filterable`, `sortable`, `hidden` und `order`, darin nur nicht-leere Zeichenketten bis 64 Zeichen, ohne Dubletten und höchstens 100 je Schlüssel. Alles andere fällt still weg, leere Listen werden gar nicht erst gespeichert. Die Antwort ist die tatsächlich abgelegte Konfiguration — sie kann kürzer sein als das Gesendete.","summary":"Ersetzt die Spaltenkonfiguration der Entität vollständig","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/imports/analyze-headers":{"post":{"responses":{"200":{"description":"Erkanntes Quellsystem inkl. Kandidatenliste","content":{"application/json":{"schema":{"type":"object","properties":{"sourceSystem":{"type":"string"},"label":{"type":"string"},"confidence":{"type":"number"},"hint":{"type":"string"},"candidates":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"confidence":{"type":"number"}},"required":["id","label","confidence"],"additionalProperties":false}}},"required":["sourceSystem","label","confidence","hint","candidates"],"additionalProperties":false},"example":{"sourceSystem":"string","label":"string","confidence":0,"hint":"string","candidates":[{"id":"string","label":"string","confidence":0}]}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1ImportsAnalyze-headers","tags":["imports"],"parameters":[],"summary":"Detect source system from headers","description":"Erkennt das Quell-ERP/CRM aus Spaltenüberschriften (stateless, für KI-Tools). Reine Mustererkennung ohne Upload-Job und ohne Datenbankzugriff — es wird nichts gespeichert. sample_rows wird entgegengenommen, aber nicht ausgewertet.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"headers":{"type":"array","items":{"type":"string"},"minItems":1},"sample_rows":{"type":"array","items":{"type":"array","items":{"type":"string"}}}},"required":["headers"]},"example":{"headers":["string"],"sample_rows":[["string"]]}}}}}},"/api/v1/imports/suggest-mapping":{"post":{"responses":{"200":{"description":"Mapping-Vorschlag inkl. Zielfeld-Metadaten","content":{"application/json":{"schema":{"type":"object","properties":{"mapping":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]}},"confidence":{"type":"number"},"issues":{"type":"array","items":{"type":"string"}},"targetFields":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"},"required":{"type":"boolean"}},"required":["value","label","required"],"additionalProperties":false}}},"required":["mapping","confidence","issues","targetFields"],"additionalProperties":false},"example":{"mapping":{"beispiel":"string"},"confidence":0,"issues":["string"],"targetFields":[{"value":"string","label":"string","required":true}]}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1ImportsSuggest-mapping","tags":["imports"],"parameters":[],"summary":"Suggest column mapping","description":"Schlägt ein Spalten-Mapping für eine Ziel-Entität vor (stateless, für KI-Tools). Nur ein Vorschlag: es wird nichts gespeichert und nichts importiert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"source_columns":{"type":"array","items":{"type":"string"},"minItems":1},"target_entity":{"type":"string","enum":["customers","orders","products","contacts","suppliers","invoices"]},"sample_rows":{"type":"array","items":{"type":"array","items":{"type":"string"}}}},"required":["source_columns","target_entity"]},"example":{"source_columns":["string"],"target_entity":"customers","sample_rows":[["string"]]}}}}}},"/api/v1/imports/presign":{"post":{"responses":{"200":{"description":"Job angelegt, Upload-Ziel + Grenzwerte","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"upload":{"type":"object","properties":{"url":{"type":"string"},"method":{"type":"string","const":"PUT"},"fallback":{"type":"boolean"}},"required":["url","method"],"additionalProperties":false},"storageKey":{"type":"string"},"maxFileSize":{"type":"number"},"maxRows":{"type":"number"}},"required":["jobId","upload","storageKey","maxFileSize","maxRows"],"additionalProperties":false},"example":{"jobId":"string","upload":{"url":"string","method":"PUT","fallback":true},"storageKey":"string","maxFileSize":0,"maxRows":0}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1ImportsPresign","tags":["imports"],"parameters":[],"summary":"Create import job and upload URL","description":"Legt einen Import-Job an und liefert die Upload-URL (S3-PUT oder Dev-Rückfall). Antwortet 200, nicht 201: der Job ist kein adressierbarer Datensatz, sondern ein kurzlebiger Vorgang IM ARBEITSSPEICHER des Prozesses. Er überlebt keinen Neustart, und bei mehreren API-Instanzen kann jede Folge-Anfrage auf einer Instanz landen, die den Job nicht kennt — dann antworten die /:id-Routen 404. Fehlen S3-Zugangsdaten, trägt upload.fallback=true und die URL zeigt auf PUT /import-uploads/:storageKey.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"contentType":{"type":"string","minLength":1,"maxLength":120},"entity":{"type":"string","enum":["customers","orders","products","contacts","suppliers","invoices"]}},"required":["filename","contentType","entity"]},"example":{"filename":"string","contentType":"string","entity":"customers"}}}}}},"/api/v1/imports/{id}/preview":{"get":{"responses":{"200":{"description":"Vorschau der Datei","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"entity":{"type":"string","enum":["customers","orders","products","contacts","suppliers","invoices"]},"headers":{"type":"array","items":{"type":"string"}},"rowCount":{"type":"number"},"preview":{"type":"array","items":{"type":"array","items":{"type":"string"}}},"encoding":{"type":"string"},"delimiter":{"type":"string"},"sheetName":{"type":"string"}},"required":["jobId","entity","headers","rowCount","preview"],"additionalProperties":false},"example":{"jobId":"string","entity":"customers","headers":["string"],"rowCount":0,"preview":[["string"]],"encoding":"string","delimiter":"string","sheetName":"string"}}}},"401":{"description":"Unauthorized"},"413":{"description":"Datei zu groß oder zu viele Zeilen","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"file_too_large"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"too_many_rows"},"maxRows":{"type":"number"},"received":{"type":"number"}},"required":["error","maxRows","received"],"additionalProperties":false}]}}}}},"operationId":"getApiV1ImportsByIdPreview","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Preview uploaded import file","description":"Überschriften, Zeilenzahl und die ersten 20 Zeilen der hochgeladenen Datei. Der Aufruf parst die Datei beim ersten Mal und merkt das Ergebnis am Job — alle weiteren Schritte (detect/infer/commit) setzen voraus, dass er einmal gelaufen ist, und antworten sonst 409. Solange nichts hochgeladen wurde: 409."}},"/api/v1/imports/{id}/detect-source":{"post":{"responses":{"200":{"description":"Erkanntes Quellsystem inkl. Kandidatenliste","content":{"application/json":{"schema":{"type":"object","properties":{"sourceSystem":{"type":"string"},"label":{"type":"string"},"confidence":{"type":"number"},"hint":{"type":"string"},"candidates":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"confidence":{"type":"number"}},"required":["id","label","confidence"],"additionalProperties":false}}},"required":["sourceSystem","label","confidence","hint","candidates"],"additionalProperties":false},"example":{"sourceSystem":"string","label":"string","confidence":0,"hint":"string","candidates":[{"id":"string","label":"string","confidence":0}]}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1ImportsByIdDetect-source","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Detect source system for import job","description":"Erkennt das Quell-ERP/CRM anhand der Spaltenüberschriften des Jobs. Reine Auskunft — am Job wird nichts geändert. Ohne vorherige Vorschau: 409."}},"/api/v1/imports/{id}/detect-entity":{"post":{"responses":{"200":{"description":"Erkannte Ziel-Entität inkl. Kandidatenliste","content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":["string","null"],"enum":["customers","orders","products","contacts","suppliers","invoices",null]},"label":{"type":"string"},"confidence":{"type":"number"},"hint":{"type":"string"},"candidates":{"type":"array","items":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","products","contacts","suppliers","invoices"]},"label":{"type":"string"},"confidence":{"type":"number"}},"required":["entity","label","confidence"],"additionalProperties":false}}},"required":["entity","label","confidence","hint","candidates"],"additionalProperties":false},"example":{"entity":"customers","label":"string","confidence":0,"hint":"string","candidates":[{"entity":"customers","label":"string","confidence":0}]}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1ImportsByIdDetect-entity","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Detect target entity for import job","description":"Erkennt die Ziel-Entität (Kunden/Artikel/Rechnungen …) anhand der Spaltenüberschriften. Anders als /detect-source ÄNDERT dieser Aufruf den Job: die erkannte Entität wird dort gespeichert und bestimmt den weiteren Ablauf (im Zuordnungs-Schritt noch überschreibbar). Erkennt er nichts, bleibt die bisherige Entität stehen. Ohne vorherige Vorschau: 409."}},"/api/v1/imports/{id}/infer-mapping":{"post":{"responses":{"200":{"description":"Mapping-Vorschlag inkl. Zielfeld-Metadaten","content":{"application/json":{"schema":{"type":"object","properties":{"mapping":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]}},"confidence":{"type":"number"},"issues":{"type":"array","items":{"type":"string"}},"targetFields":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"},"required":{"type":"boolean"}},"required":["value","label","required"],"additionalProperties":false}}},"required":["mapping","confidence","issues","targetFields"],"additionalProperties":false},"example":{"mapping":{"beispiel":"string"},"confidence":0,"issues":["string"],"targetFields":[{"value":"string","label":"string","required":true}]}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1ImportsByIdInfer-mapping","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Infer field mapping for import job","description":"Schlägt für den Job eine Spalten-Zuordnung vor und merkt sie am Job vor (Status \"mapped\"). Der Vorschlag ist noch keine Übernahme — importiert wird erst mit /commit, und dieses nimmt die Zuordnung aus dem Rumpf, nicht die hier gemerkte. Ohne vorherige Vorschau: 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","orders","products","contacts","suppliers","invoices"]}}},"example":{"entity":"customers"}}}}}},"/api/v1/imports/{id}/detect-logic":{"post":{"responses":{"200":{"description":"Erkannte Formel-Logik (bei CSV immer leer, mit Begründung)","content":{"application/json":{"schema":{"type":"object","properties":{"detectedLogic":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["calculated-column","aggregate","condition","lookup","unknown"]},"sourceCells":{"type":"array","items":{"type":"string"}},"sourceColumn":{"type":"string"},"formulaSample":{"type":"string"},"erpSuggestion":{"type":"string"}},"required":["kind","sourceCells","formulaSample","erpSuggestion"],"additionalProperties":false}},"reason":{"type":"string","const":"no_formulas_in_csv"}},"required":["detectedLogic"],"additionalProperties":false},"example":{"detectedLogic":[{"kind":"calculated-column","sourceCells":["string"],"sourceColumn":"string","formulaSample":"string","erpSuggestion":"string"}],"reason":"no_formulas_in_csv"}}}},"401":{"description":"Unauthorized"},"409":{"description":"Preview required / upload pending"}},"operationId":"postApiV1ImportsByIdDetect-logic","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Detect Excel formula logic","description":"Erkennt Excel-Formeln (berechnete Spalten/Aggregate/Bedingungen/Lookups) und schlägt ihre ERP-Nachbildung vor. NUR für xlsx: eine CSV trägt keine Formeln, dort antwortet der Aufruf 200 mit leerer Liste und reason=\"no_formulas_in_csv\" — das ist kein Fehler, aber auch kein Ergebnis. Der Vorschlag wird nirgends gespeichert und nichts davon gebaut."}},"/api/v1/imports/{id}/commit":{"post":{"responses":{"200":{"description":"Import übernommen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"imported":{"type":"number"},"durationMs":{"type":"number"},"batchCount":{"type":"number"}},"required":["ok","imported","durationMs","batchCount"],"additionalProperties":false},"example":{"ok":true,"imported":0,"durationMs":0,"batchCount":0}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — mindestens Rolle „manager\" nötig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"422":{"description":"Import abgelehnt — keine Zeile wurde übernommen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","enum":["validation_failed","db_unavailable","unsupported_entity"]},"failedRowIndex":{"type":"number"},"message":{"type":"string"},"details":{}},"required":["ok","error","message"],"additionalProperties":false}}}}},"operationId":"postApiV1ImportsByIdCommit","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Commit import job","description":"Übernimmt die zugeordneten Zeilen in 500er-Batches in die Ziel-Entität. Läuft synchron: die Antwort ist das ERGEBNIS, keine Quittung — 200 nennt die Zahl der übernommenen Zeilen. Alles-oder-nichts: schlägt die Prüfung fehl, kommt 422 mit dem Index der ersten fehlerhaften Zeile und es wurde KEINE Zeile geschrieben. Maßgeblich ist die Zuordnung aus dem Rumpf; die Ziel-Entität steht am Job. Ohne vorherige Vorschau: 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mapping":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]}}},"required":["mapping"]},"example":{"mapping":{"beispiel":"string"}}}}}}},"/api/v1/imports/analyze":{"post":{"responses":{"200":{"description":"Feldtyp-Vorschau","content":{"application/json":{"schema":{"type":"object","properties":{"columns":{"type":"array","items":{"type":"object","properties":{"header":{"type":"string"},"slug":{"type":"string"},"label":{"type":"string"},"fieldType":{"type":"string","enum":["text","number","date","select","email","phone","bool"]},"options":{"type":"array","items":{"type":"string"}},"confidence":{"type":"number"},"samples":{"type":"array","items":{"type":"string"}}},"required":["header","slug","label","fieldType","confidence","samples"],"additionalProperties":false}},"rowCount":{"type":"number"},"suggestedName":{"type":"string"}},"required":["columns","rowCount","suggestedName"],"additionalProperties":false},"example":{"columns":[{"header":"string","slug":"string","label":"string","fieldType":"text","options":["string"],"confidence":0,"samples":["string"]}],"rowCount":0,"suggestedName":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1ImportsAnalyze","tags":["imports"],"parameters":[],"summary":"Analyse columns and suggest field types","description":"Analysiert Header + Beispielzeilen und schlägt je Spalte einen Feldtyp vor (Text/Zahl/Datum/Auswahl/E-Mail/Telefon/Ja-Nein) — Basis für den Modul-Autobau. Reine Vorschau ohne Job und ohne Schreibvorgang: es entsteht kein Modul. suggestedName ist derzeit IMMER leer; den Modulnamen setzt der Aufrufer selbst.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"headers":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":200},"rows":{"type":"array","items":{"type":"array","items":{"type":"string"}},"maxItems":50000}},"required":["headers","rows"]},"example":{"headers":["string"],"rows":[["string"]]}}}}}},"/api/v1/imports/build-module":{"post":{"responses":{"201":{"description":"Modul erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"entityId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"fieldCount":{"type":"number"},"imported":{"type":"number"},"skipped":{"type":"number"},"rowCount":{"type":"number"},"moduleUrl":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}}},"required":["ok","entityId","slug","name","fieldCount","imported","skipped","rowCount","moduleUrl"],"additionalProperties":false},"example":{"ok":true,"entityId":"string","slug":"string","name":"string","fieldCount":0,"imported":0,"skipped":0,"rowCount":0,"moduleUrl":"string","warnings":["string"]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — mindestens Rolle „manager\" nötig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"409":{"description":"Slug-Kollision / Quota"}},"operationId":"postApiV1ImportsBuild-module","tags":["imports"],"parameters":[],"summary":"Build custom module from columns","description":"Baut aus Feld-Definitionen ein neues Custom-Modul: legt Entity + Felder an und importiert die Zeilen in einem Fluss. Teilerfolg ist möglich und wird mit 201 gemeldet: ein Feld, das nicht angelegt werden konnte, wird nur protokolliert — fieldCount nennt die ANGEFORDERTE Zahl, nicht die tatsächlich angelegte. Zeilen, die scheitern oder leer sind, zählen unter skipped; die ersten fünf Gründe stehen unter warnings. Ein bereits vergebenes Kürzel oder ein erschöpftes Kontingent: 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$"},"name":{"type":"string","minLength":1,"maxLength":120},"icon":{"type":"string","maxLength":120},"fields":{"type":"array","items":{"type":"object","properties":{"header":{"type":"string","minLength":1},"slug":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$"},"label":{"type":"string","minLength":1,"maxLength":120},"fieldType":{"type":"string","enum":["text","number","date","select","email","phone","bool"]},"options":{"type":"array","items":{"type":"string"},"maxItems":50}},"required":["header","slug","label","fieldType"]},"minItems":1,"maxItems":100},"rows":{"type":"array","items":{"type":"array","items":{"type":"string"}},"maxItems":50000}},"required":["slug","name","fields","rows"]}}}}}},"/api/v1/imports/{id}":{"get":{"responses":{"200":{"description":"Job-Status","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"entity":{"type":"string","enum":["customers","orders","products","contacts","suppliers","invoices"]},"status":{"type":"string","enum":["awaiting_upload","uploaded","mapped","committed","failed"]},"filename":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"rowCount":{"type":"number"},"headers":{"type":"array","items":{"type":"string"}},"mapping":{"type":["object","null"],"additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]}},"result":{"type":["object","null"],"properties":{"imported":{"type":"number"},"durationMs":{"type":"number"},"error":{"type":"string"},"failedRowIndex":{"type":"number"}},"required":["imported","durationMs"],"additionalProperties":false}},"required":["id","entity","status","filename","createdAt","updatedAt","rowCount","headers","mapping","result"],"additionalProperties":false},"example":{"id":"string","entity":"customers","status":"awaiting_upload","filename":"string","createdAt":"string","updatedAt":"string","rowCount":0,"headers":["string"],"mapping":{"beispiel":"string"},"result":{"imported":0,"durationMs":0,"error":"string","failedRowIndex":0}}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1ImportsById","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get import job status","description":"Status und Metadaten eines Import-Jobs. Der Job lebt im Arbeitsspeicher des Prozesses: nach Ablauf, nach einem Neustart oder wenn die Anfrage eine andere API-Instanz trifft, antwortet der Aufruf 404 — auch für einen fremden Mandanten (404 statt 403, damit die Existenz fremder Jobs nicht verraten wird)."},"delete":{"responses":{"200":{"description":"Job verworfen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Unauthorized"}},"operationId":"deleteApiV1ImportsById","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Discard import job","description":"Verwirft einen Import-Job samt zwischengespeicherter Datei. Betrifft nur den Vorgang im Arbeitsspeicher — bereits übernommene Zeilen bleiben in der Datenbank und werden NICHT zurückgenommen."}},"/api/v1/imports/{id}/match-columns":{"post":{"responses":{"200":{"description":"Je Spalte die beste Entität + Treffer-Rate sowie Bulk-Fix-Vorschläge","content":{"application/json":{"schema":{"type":"object","properties":{"columns":{"type":"array","items":{"type":"object","properties":{"header":{"type":"string"},"headerGuess":{"type":["string","null"],"enum":["customers","products","suppliers",null]},"best":{"type":["object","null"],"properties":{"entity":{"type":"string","enum":["customers","products","suppliers"]},"label":{"type":"string"},"field":{"type":"string"},"matchRate":{"type":"number"},"matchedCount":{"type":"number"},"sampleSize":{"type":"number"},"examples":{"type":"array","items":{"type":"string"}}},"required":["entity","label","field","matchRate","matchedCount","sampleSize","examples"],"additionalProperties":false},"matches":{"type":"array","items":{"type":"object","properties":{"entity":{"type":"string","enum":["customers","products","suppliers"]},"label":{"type":"string"},"field":{"type":"string"},"matchRate":{"type":"number"},"matchedCount":{"type":"number"},"sampleSize":{"type":"number"},"examples":{"type":"array","items":{"type":"string"}}},"required":["entity","label","field","matchRate","matchedCount","sampleSize","examples"],"additionalProperties":false}}},"required":["header","headerGuess","best","matches"],"additionalProperties":false}},"bulkFixes":{"type":"array","items":{"type":"object","properties":{"header":{"type":"string"},"fixes":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["de-date","de-number","trim"]},"label":{"type":"string"},"affected":{"type":"number"},"inspected":{"type":"number"},"examples":{"type":"array","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"],"additionalProperties":false}}},"required":["kind","label","affected","inspected","examples"],"additionalProperties":false}}},"required":["header","fixes"],"additionalProperties":false}}},"required":["columns","bulkFixes"],"additionalProperties":false},"example":{"columns":[{"header":"string","headerGuess":"customers","best":{"entity":"customers","label":"string","field":"string","matchRate":0,"matchedCount":0,"sampleSize":0,"examples":["string"]},"matches":[{"entity":"customers","label":"string","field":"string","matchRate":0,"matchedCount":0,"sampleSize":0,"examples":["string"]}]}],"bulkFixes":[{"header":"string","fixes":[{"kind":"de-date","label":"string","affected":0,"inspected":0,"examples":[]}]}]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — mindestens Rolle „manager\" nötig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"409":{"description":"Preview required"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1ImportsByIdMatch-columns","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Match columns against existing master data","description":"Gleicht jede Spalte gegen bestehende Stammdaten ab (Kunden/Artikel/Lieferanten) und meldet pro Spalte die beste Entität + Treffer-Rate sowie Bulk-Fix-Vorschläge. Reine Auskunft: weder die Stammdaten noch der Job werden geändert, und kein Vorschlag wird angewandt. Ohne vorherige Vorschau: 409."}},"/api/v1/imports/mapping-memory/recall":{"post":{"responses":{"200":{"description":"Gemerkte Zuordnung — `remembered` ist null, wenn es keine gibt","content":{"application/json":{"schema":{"type":"object","properties":{"remembered":{"type":["object","null"],"properties":{"entity":{"type":"string","enum":["customers","orders","products","contacts","suppliers","invoices"]},"mapping":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]}},"headers":{"type":"array","items":{"type":"string"}},"useCount":{"type":"number"}},"required":["entity","mapping","headers","useCount"],"additionalProperties":false}},"required":["remembered"],"additionalProperties":false},"example":{"remembered":{"entity":"customers","mapping":{"beispiel":"string"},"headers":["string"],"useCount":0}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — mindestens Rolle „manager\" nötig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1ImportsMapping-memoryRecall","tags":["imports"],"parameters":[],"summary":"Recall remembered column mapping","description":"Liest eine früher bestätigte Spalten-Zuordnung für genau dieses Header-Set (Vorbelegung). Trotz POST ein reiner Lesevorgang — der Rumpf trägt die Überschriften. Gibt es keine gemerkte Zuordnung, ist remembered null (200, kein 404). Jeder Fehler dieses Aufrufs wird als 503 database_unavailable gemeldet.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"headers":{"type":"array","items":{"type":"string","maxLength":255},"minItems":1,"maxItems":200}},"required":["headers"]},"example":{"headers":["string"]}}}}}},"/api/v1/imports/mapping-memory":{"put":{"responses":{"200":{"description":"Zuordnung gemerkt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — mindestens Rolle „manager\" nötig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1ImportsMapping-memory","tags":["imports"],"parameters":[],"summary":"Remember column mapping","description":"Merkt sich eine bestätigte Spalten-Zuordnung für dieses Header-Set (nach erfolgreichem Import). Gilt mandantenweit für jeden künftigen Import mit denselben Überschriften. Jeder Fehler dieses Aufrufs wird als 503 database_unavailable gemeldet.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"headers":{"type":"array","items":{"type":"string","maxLength":255},"minItems":1,"maxItems":200},"entity":{"type":"string","enum":["customers","orders","products","contacts","suppliers","invoices"]},"mapping":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]}}},"required":["headers","entity","mapping"]},"example":{"headers":["string"],"entity":"customers","mapping":{"beispiel":"string"}}}}}}},"/api/v1/import-uploads/{storageKey}":{"put":{"responses":{"200":{"description":"Datei übernommen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"jobId":{"type":"string"}},"required":["ok","jobId"],"additionalProperties":false},"example":{"ok":true,"jobId":"string"}}}},"401":{"description":"Unauthorized"},"413":{"description":"Datei größer als das Upload-Limit","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"file_too_large"},"maxBytes":{"type":"number"}},"required":["error","maxBytes"],"additionalProperties":false}}}}},"operationId":"putApiV1Import-uploadsByStorageKey","tags":["imports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"storageKey","required":true}],"summary":"Upload import file directly (dev fallback)","description":"Dev-Rückfall ohne S3: nimmt die Datei-Bytes direkt entgegen und hängt sie an den Job. Nur zu benutzen, wenn /presign upload.fallback=true geliefert hat — mit konfiguriertem S3 wird stattdessen gegen die signierte URL hochgeladen. Die Bytes landen im Arbeitsspeicher des Prozesses, nicht in einem Speicher."}},"/api/v1/search":{"get":{"responses":{"200":{"description":"Treffer nach Rang. `components` zerlegt den `score` in Bedeutung, Wortlaut und Alter; `backend` sagt, welche Suchmaschine geantwortet hat.","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"tenant":{"type":"string"},"backend":{"type":"string"},"count":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"entityType":{"type":"string"},"entityId":{"type":"string"},"content":{"type":"string"},"metadata":{},"indexedAt":{"type":"string"},"score":{"type":"number"},"components":{"type":"object","properties":{"cosine":{"type":"number"},"lexical":{"type":"number"},"recency":{"type":"number"}},"required":["cosine","lexical","recency"],"additionalProperties":false}},"required":["id","entityType","entityId","content","indexedAt","score","components"],"additionalProperties":false}}},"required":["query","tenant","backend","count","results"],"additionalProperties":false},"example":{"query":"string","tenant":"string","backend":"string","count":0,"results":[{"id":"string","entityType":"string","entityId":"string","content":"string","indexedAt":"string","score":0,"components":{"cosine":0,"lexical":0,"recency":0}}]}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Search","tags":["search"],"parameters":[],"summary":"Tenant-scoped Volltext-/Vektor-Suche ueber Kunden, Rechnungen und Auftraege","description":"Fragt ZWEI Quellen parallel ab und mischt die Treffer: die Direktsuche ueber die realen Mandanten-Tabellen (ILIKE) und den semantischen Index `search_index` (pgvector + tsvector). Faellt eine der beiden aus, liefert die andere weiter; ist gar keine Datenbank erreichbar, antwortet ein In-Memory-Rueckfall mit Beispieldaten. Welcher Weg gegriffen hat, steht in `backend` — ohne dieses Feld sieht ein Rueckfall aus wie ein Vektortreffer. `q` ist Pflicht (1 bis 500 Zeichen), `limit` liegt zwischen 1 und 100 (Vorgabe 20), `types` grenzt als kommagetrennte Liste auf Entitaetstypen ein. Ein nicht aufloesbarer Mandant und ein ungueltiger Parameter enden beide mit 400."}},"/api/v1/search/reindex":{"post":{"responses":{"200":{"description":"Reindex-Ergebnis je Entity-Typ. Auch ein Lauf mit gescheiterten Indexern kommt hier an — dann mit `ok: false` und einem `errors`-Feld.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"false, sobald mindestens ein Entity-Typ gescheitert ist"},"tenant":{"type":"string","description":"Slug des Mandanten, dessen Index neu gebaut wurde"},"indexed":{"type":"object","additionalProperties":{"type":"integer"},"description":"Entity-Typ auf Anzahl der in search_index geschriebenen Zeilen"},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Entity-Typ auf Fehlermeldung; fehlt vollstaendig, wenn nichts gescheitert ist"}},"required":["ok","tenant","indexed"]},"example":{"ok":true,"tenant":"string","indexed":{"beispiel":0},"errors":{"beispiel":"string"}}}}},"400":{"description":"Bad Request (unbekannter Entity-Typ)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden (Admin erforderlich)"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"postApiV1SearchReindex","tags":["search"],"parameters":[],"summary":"Baut den Vektor-Suchindex dieses Mandanten neu auf","description":"Synchron-gebatchter Backfill des Vektor-Suchindex (search_index) fuer den aktuellen Mandanten. Body optional: { types: [\"quote\",\"order\",...] } filtert auf einzelne Entity-Typen."}},"/api/v1/search/reindex-log":{"get":{"responses":{"200":{"description":"Verlauf der Reindex-Laeufe samt Alterswarnung","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Laufs"},"sourceTypes":{"type":["string","null"],"description":"Welche Quellen der Lauf umfasste; null wenn nicht vermerkt"},"chunksWritten":{"type":"integer","description":"Anzahl geschriebener Textabschnitte; 0 wenn nicht vermerkt"},"startedAt":{"type":["string","null"],"description":"Beginn des Laufs als ISO-8601-Zeitstempel"},"completedAt":{"type":["string","null"],"description":"Ende des Laufs; null bei einem Lauf, der nie fertig wurde"},"durationMs":{"type":["integer","null"],"description":"Dauer in Millisekunden; null wenn nicht vermerkt"}},"required":["id","sourceTypes","chunksWritten","startedAt","completedAt","durationMs"]},"description":"Die letzten 20 Laeufe, neueste zuerst"},"lastCompletedAt":{"type":["string","null"],"description":"Abschluss des juengsten Laufs; null, wenn es noch keinen gab"},"stale":{"type":"boolean","description":"true, wenn der letzte Lauf ueber 48 Stunden zurueckliegt — und ebenso, wenn es keinen gab"}},"required":["jobs","lastCompletedAt","stale"]},"example":{"jobs":[{"id":"string","sourceTypes":"string","chunksWritten":0,"startedAt":"string","completedAt":"string","durationMs":0}],"lastCompletedAt":"string","stale":true}}}},"400":{"description":"Mandant nicht aufloesbar"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden (Admin erforderlich)"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getApiV1SearchReindex-log","tags":["search"],"parameters":[],"summary":"Letzte RAG-Reindex-Läufe des Mandanten + >48h-Stale-Warnung.","description":"Liest die letzten 20 Zeilen aus `public.rag_reindex_log` fuer den aufrufenden Mandanten, neueste zuerst; geschrieben werden sie vom naechtlichen Reindex-Job. `stale` ist true, sobald der juengste abgeschlossene Lauf ueber 48 Stunden zurueckliegt — und ebenso, wenn es ueberhaupt noch keinen Lauf gab. Die Tabelle wird bei Bedarf selbst angelegt, ein Mandant ohne bisherigen Lauf bekommt deshalb `jobs: []` und `stale: true` statt eines Fehlers. Der Aufruf liest nur; er stoesst KEINEN Reindex an."}},"/api/v1/search/semantic":{"post":{"responses":{"200":{"description":"Treffer nach Rang, in der schlanken Form ohne `components`-Zerlegung. `backend` sagt, welche Suchmaschine geantwortet hat.","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","description":"Die ausgewertete Suchanfrage"},"tenant":{"type":"string","description":"Slug des Mandanten, in dem gesucht wurde"},"backend":{"type":"string","description":"Welche Suchmaschine geantwortet hat (Vektor oder Rueckfall)"},"count":{"type":"number","description":"Anzahl der Treffer in `results`"},"results":{"type":"array","items":{"type":"object","properties":{"entityType":{"type":"string","description":"Art des Treffers, z. B. customer oder invoice"},"entityId":{"type":"string","description":"Kennung des getroffenen Datensatzes"},"content":{"type":"string","description":"Der indizierte Text des Treffers"},"metadata":{"description":"Beim Indizieren mitgeschriebene Zusatzangaben"},"score":{"type":"number","description":"Rangwert, auf sechs Nachkommastellen gerundet"}},"required":["entityType","entityId","content","score"],"additionalProperties":false}}},"required":["query","tenant","backend","count","results"],"additionalProperties":false},"example":{"query":"string","tenant":"string","backend":"string","count":0,"results":[{"entityType":"string","entityId":"string","content":"string","score":0}]}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1SearchSemantic","tags":["search"],"parameters":[],"description":"Semantische/Volltext-Suche für Agenten/n8n/MCP. Body: { q, limit?, types? }. Antwort: Treffer mit entityType/entityId/content/score. Tenant-scoped; Auth via API-Key oder Session.","summary":"Semantische/Volltext-Suche für Agenten/n8n/MCP","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/activity":{"get":{"responses":{"200":{"description":"Die Eintraege des Verlaufs","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Eintrags (UUID)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Eintrag gehoert"},"entityType":{"type":"string","minLength":1,"description":"Art des Belegs oder Datensatzes, an dem der Eintrag haengt, z. B. `invoice` oder `customer`"},"entityId":{"type":"string","minLength":1,"description":"Kennung des Belegs oder Datensatzes"},"userId":{"type":["string","null"],"minLength":1,"description":"Kennung des Urhebers; `null` bei System-Eintraegen ohne Benutzerkontext"},"userName":{"type":["string","null"],"description":"Klarname des Urhebers, beim Lesen aus der Nutzertabelle aufgeloest; FEHLT, wenn nicht aufloesbar"},"userEmail":{"type":["string","null"],"format":"email","description":"E-Mail des Urhebers, beim Lesen aufgeloest; FEHLT, wenn nicht aufloesbar"},"action":{"type":"string","minLength":1,"maxLength":500,"description":"Was geschah. Der Wert `note` kennzeichnet eine von Hand geschriebene Notiz — nur die ist loeschbar"},"note":{"type":["string","null"],"maxLength":2000,"description":"Der Notiztext; `null` bei System-Eintraegen"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"canDelete":{"type":"boolean","description":"Darf DER ANFRAGENDE Benutzer diesen Eintrag entfernen? Stammt aus derselben Wache, die DELETE durchsetzt. Im Speicher-Rueckfall immer `false`, weil DELETE ohne Datenbank ablehnt."}},"required":["id","tenantId","entityType","entityId","userId","action","note","createdAt","canDelete"],"additionalProperties":false},"description":"Die Eintraege, neueste zuerst, hoechstens so viele wie `limit`"},"count":{"type":"integer","minimum":0,"description":"Laenge von `entries` — NICHT die Gesamtzahl im Verlauf. Wer alle will, erhoeht `limit` (max. 100)."}},"required":["entries","count"],"additionalProperties":false},"example":{"entries":[{"id":"string","tenantId":"string","entityType":"string","entityId":"string","userId":"string","userName":"string","userEmail":"beispiel@example.com","action":"string","note":"string","createdAt":"string","canDelete":true}],"count":0}}}},"400":{"description":"Query-Parameter abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"503":{"description":"Abfrage fehlgeschlagen; es wird KEINE leere Liste vorgetaeuscht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Activity","tags":["activity"],"parameters":[{"in":"query","name":"entity_type","schema":{"type":"string","minLength":1}},{"in":"query","name":"entity_id","schema":{"type":"string","minLength":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}}],"summary":"Aktivitäts-Verlauf lesen","description":"Liest den Aktivitaets-Verlauf. OHNE `entity_type`/`entity_id` kommt der mandantenweite Feed (Dashboard), MIT beiden der Verlauf eines einzelnen Datensatzes. Ist die Datenbank nicht erreichbar, antwortet ein Rueckfall aus dem Prozessspeicher mit 200 und `canDelete: false`. Scheitert dagegen die ABFRAGE bei erreichbarer Datenbank, kommt bewusst 503 statt einer leeren Liste — ein leerer Verlauf soll nie ein verschluckter Fehler sein."},"post":{"responses":{"201":{"description":"Der angelegte Eintrag — Form identisch, ob dauerhaft gespeichert oder nur im Speicher","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Eintrags (UUID)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Eintrag gehoert"},"entityType":{"type":"string","minLength":1,"description":"Art des Belegs oder Datensatzes, an dem der Eintrag haengt, z. B. `invoice` oder `customer`"},"entityId":{"type":"string","minLength":1,"description":"Kennung des Belegs oder Datensatzes"},"userId":{"type":["string","null"],"minLength":1,"description":"Kennung des Urhebers; `null` bei System-Eintraegen ohne Benutzerkontext"},"userName":{"type":["string","null"],"description":"Klarname des Urhebers, beim Lesen aus der Nutzertabelle aufgeloest; FEHLT, wenn nicht aufloesbar"},"userEmail":{"type":["string","null"],"format":"email","description":"E-Mail des Urhebers, beim Lesen aufgeloest; FEHLT, wenn nicht aufloesbar"},"action":{"type":"string","minLength":1,"maxLength":500,"description":"Was geschah. Der Wert `note` kennzeichnet eine von Hand geschriebene Notiz — nur die ist loeschbar"},"note":{"type":["string","null"],"maxLength":2000,"description":"Der Notiztext; `null` bei System-Eintraegen"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"canDelete":{"type":"boolean","description":"Darf DER ANFRAGENDE Benutzer diesen Eintrag entfernen? Stammt aus derselben Wache, die DELETE durchsetzt. Im Speicher-Rueckfall immer `false`, weil DELETE ohne Datenbank ablehnt."}},"required":["id","tenantId","entityType","entityId","userId","action","note","createdAt","canDelete"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","entityType":"string","entityId":"string","userId":"string","userName":"string","userEmail":"beispiel@example.com","action":"string","note":"string","createdAt":"string","canDelete":true}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"}},"operationId":"postApiV1Activity","tags":["activity"],"parameters":[],"summary":"Notiz zum Verlauf hinzufügen","description":"Haengt einen Eintrag an den Verlauf eines Datensatzes. Der Eintrag kommt OHNE Umschlag zurueck — die Felder stehen direkt im Wurzelobjekt, `canDelete` gleich mit, damit die Maske den frisch getippten Eintrag ohne Neuladen anzeigen kann. ACHTUNG: schlaegt der INSERT fehl oder fehlt die Datenbank, antwortet die Route TROTZDEM mit 201 — der Eintrag liegt dann nur im Prozessspeicher dieser Instanz. Wer `action` auf `note` setzt, kann den Eintrag spaeter selbst wieder loeschen; jeder andere Wert gilt als Protokoll und ist dauerhaft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity_type":{"type":"string","minLength":1},"entity_id":{"type":"string","minLength":1},"action":{"type":"string","minLength":1,"maxLength":500},"note":{"type":"string","maxLength":2000}},"required":["entity_type","entity_id","action"]},"example":{"entity_type":"string","entity_id":"string","action":"string","note":"string"}}}}}},"/api/v1/activity/{id}":{"delete":{"responses":{"200":{"description":"Der Eintrag ist entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true,"description":"Immer `true` — der Eintrag ist ab jetzt ausgeblendet"},"id":{"type":"string","minLength":1,"description":"Kennung des entfernten Eintrags, unveraendert aus dem Pfad"}},"required":["deleted","id"],"additionalProperties":false},"example":{"deleted":true,"id":"string"}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"System-Eintrag (fuer alle gesperrt) oder fremde Notiz ohne Adminrecht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["system_entry_protected","not_your_note"],"description":"`system_entry_protected` = kein Notiz-Eintrag, sondern Protokoll des Belegs (fuer alle gesperrt); `not_your_note` = fremde Notiz ohne Adminrecht"},"message":{"type":"string","description":"Deutscher Klartext mit dem Grund, direkt anzeigbar"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Kennung nicht in UUID-Form, nicht vorhanden, oder bereits entfernt — auch der zweite Klick landet hier","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Deutscher Klartext, direkt anzeigbar"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder Abfrage fehlgeschlagen — es wurde nichts entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1ActivityById","tags":["activity"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigene Notiz entfernen","description":"Entfernt eine selbst geschriebene Notiz (Administratoren: jede Notiz). Es wird `deleted_at` gesetzt, die Zeile bleibt also in der Datenbank stehen und ist nur nicht mehr sichtbar. Zwei Sperren davor: System-Eintraege (alles ausser `action` = `note`) sind fuer JEDEN gesperrt, fremde Notizen nur fuer Nicht-Administratoren. Ohne Datenbank wird kein Erfolg vorgetaeuscht, sondern 503 gesendet — der Prozessspeicher wird hier bewusst NICHT angefasst."}},"/api/v1/crm/leads":{"get":{"responses":{"200":{"description":"Leads unter `items`, dazu `total`, `page` und `limit`","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"anyOf":[{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `company_name`, `contact_name`, `email`, `phone`, `source`, `status`, `score`, `assigned_to`, `notes`, `estimated_value`, `ai_score_suggestion`, `ai_score_reason`, `created_at`, `updated_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. `estimated_value` ist NUMERIC und kommt bei postgres-js als Zeichenkette heraus, nicht als Zahl."},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Leads, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Lead gehoert"},"companyName":{"type":"string","minLength":1,"maxLength":200,"description":"Firmenname des Interessenten — das einzige Pflichtfeld"},"contactName":{"type":["string","null"],"maxLength":200,"description":"Ansprechpartner; `null`, wenn keiner erfasst ist"},"email":{"type":["string","null"],"format":"email","description":"E-Mail des Ansprechpartners; `null`, wenn keine oder eine leere angegeben wurde"},"phone":{"type":["string","null"],"maxLength":50,"description":"Telefonnummer; `null`, wenn keine erfasst ist"},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"description":"Woher der Lead kam; wirkt auf die Punktzahl von POST /leads/{id}/score"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"description":"Stufe im Vertriebsprozess. `converted` ist der Altbestand-Name fuer `won`"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Von Hand gesetzte Punktzahl; 0, wenn nie gesetzt"},"assignedTo":{"type":["string","null"],"format":"uuid","description":"Zustaendiger Benutzer; `null`, wenn niemand zugeordnet ist"},"notes":{"type":["string","null"],"description":"Freitext; `null`, wenn keiner erfasst ist"},"estimatedValue":{"type":["number","null"],"minimum":0,"description":"Geschaetzter Auftragswert in Euro; `null`, wenn nicht erfasst — NICHT 0"},"aiScoreSuggestion":{"type":["integer","null"],"minimum":0,"maximum":100,"description":"Zuletzt berechnete Punktzahl aus POST /leads/{id}/score; beim Anlegen `null`"},"aiScoreReason":{"type":["string","null"],"description":"Rechenweg dieser Punktzahl im Klartext; beim Anlegen `null`"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","companyName","contactName","email","phone","source","status","score","assignedTo","notes","estimatedValue","aiScoreSuggestion","aiScoreReason","createdAt","updatedAt"],"additionalProperties":false}]},"description":"Die Leads dieser Seite, neueste zuerst"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer fuer die gesetzten Filter — echtes COUNT, nicht die Laenge der Seite"},"page":{"type":"integer","minimum":1,"description":"Die zurueckgegebene Seite, gezaehlt ab 1"},"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Groesse der Seite, wie angefragt"}},"required":["items","total","page","limit"],"additionalProperties":false},"example":{"items":[{}],"total":0,"page":1,"limit":1}}}},"400":{"description":"Query-Parameter abgelehnt (rohes Zod-Ergebnis). Ein unzulaessiger Mandanten-Slug antwortet unter demselben Code mit Klartext `invalid_tenant_slug`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"500":{"description":"Abfrage gescheitert, aber NICHT an der Verbindung — ein Wiederholen hilft hier nicht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CrmLeads","tags":["CRM","Leads"],"parameters":[{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"source","schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"List leads","description":"Listet die Leads des Mandanten, seitenweise (`page`/`limit`) und filterbar nach `status`, `source` und `search` (Firma, Ansprechpartner, E-Mail). Ist die Datenbank nicht erreichbar, antwortet dieser Aufruf trotzdem mit 200 — dann aus einem prozesslokalen Zwischenspeicher, der nur enthält, was genau dieser Serverprozess selbst angelegt hat. Eine leere Liste heißt also nicht zwingend „keine Leads vorhanden\"."},"post":{"responses":{"201":{"description":"Der angelegte Lead, OHNE Umschlag. Immer die camelCase-Form — dieser Aufruf reicht keine Datenbankzeile durch, sondern gibt das Objekt zurueck, das er selbst gebaut hat","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Leads, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Lead gehoert"},"companyName":{"type":"string","minLength":1,"maxLength":200,"description":"Firmenname des Interessenten — das einzige Pflichtfeld"},"contactName":{"type":["string","null"],"maxLength":200,"description":"Ansprechpartner; `null`, wenn keiner erfasst ist"},"email":{"type":["string","null"],"format":"email","description":"E-Mail des Ansprechpartners; `null`, wenn keine oder eine leere angegeben wurde"},"phone":{"type":["string","null"],"maxLength":50,"description":"Telefonnummer; `null`, wenn keine erfasst ist"},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"description":"Woher der Lead kam; wirkt auf die Punktzahl von POST /leads/{id}/score"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"description":"Stufe im Vertriebsprozess. `converted` ist der Altbestand-Name fuer `won`"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Von Hand gesetzte Punktzahl; 0, wenn nie gesetzt"},"assignedTo":{"type":["string","null"],"format":"uuid","description":"Zustaendiger Benutzer; `null`, wenn niemand zugeordnet ist"},"notes":{"type":["string","null"],"description":"Freitext; `null`, wenn keiner erfasst ist"},"estimatedValue":{"type":["number","null"],"minimum":0,"description":"Geschaetzter Auftragswert in Euro; `null`, wenn nicht erfasst — NICHT 0"},"aiScoreSuggestion":{"type":["integer","null"],"minimum":0,"maximum":100,"description":"Zuletzt berechnete Punktzahl aus POST /leads/{id}/score; beim Anlegen `null`"},"aiScoreReason":{"type":["string","null"],"description":"Rechenweg dieser Punktzahl im Klartext; beim Anlegen `null`"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","companyName","contactName","email","phone","source","status","score","assignedTo","notes","estimatedValue","aiScoreSuggestion","aiScoreReason","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","companyName":"string","contactName":"string","email":"beispiel@example.com","phone":"string","source":"web","status":"new","score":0,"assignedTo":"00000000-0000-4000-8000-000000000000","notes":"string","estimatedValue":0,"aiScoreSuggestion":0,"aiScoreReason":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis). Ein unzulaessiger Mandanten-Slug antwortet unter demselben Code mit Klartext `invalid_tenant_slug`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"500":{"description":"Anlage gescheitert, aber NICHT an der Verbindung — es wurde nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — es wurde nichts gespeichert, wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1CrmLeads","tags":["CRM","Leads"],"parameters":[],"summary":"Create lead","description":"Legt einen Lead an. Die ID vergibt der Server. ACHTUNG — ist die Datenbank nicht erreichbar, kommt trotzdem 201: der Lead liegt dann ausschliesslich im Arbeitsspeicher DIESES Serverprozesses, ist für andere Instanzen unsichtbar und beim nächsten Neustart weg. Ein 201 ist hier also keine Zusage, dass der Lead dauerhaft gespeichert wurde. Scheitert dagegen ein INSERT bei erreichbarer Datenbank, kommt ehrlich ein 503 statt eines stillen 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string","minLength":1,"maxLength":200},"contactName":{"type":"string","maxLength":200},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string","maxLength":50},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"default":"manual"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"default":"new"},"score":{"type":"integer","minimum":0,"maximum":100},"assignedTo":{"type":"string","format":"uuid"},"notes":{"type":"string"},"estimatedValue":{"type":"number","minimum":0}},"required":["companyName"]},"example":{"companyName":"string","contactName":"string","email":"beispiel@example.com","phone":"string","source":"web","status":"new","score":0,"assignedTo":"00000000-0000-4000-8000-000000000000","notes":"string","estimatedValue":0}}}}}},"/api/v1/crm/leads/{id}":{"get":{"responses":{"200":{"description":"Der Lead, OHNE Umschlag — die Felder stehen direkt im Wurzelobjekt","content":{"application/json":{"schema":{"anyOf":[{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `company_name`, `contact_name`, `email`, `phone`, `source`, `status`, `score`, `assigned_to`, `notes`, `estimated_value`, `ai_score_suggestion`, `ai_score_reason`, `created_at`, `updated_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. `estimated_value` ist NUMERIC und kommt bei postgres-js als Zeichenkette heraus, nicht als Zahl."},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Leads, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Lead gehoert"},"companyName":{"type":"string","minLength":1,"maxLength":200,"description":"Firmenname des Interessenten — das einzige Pflichtfeld"},"contactName":{"type":["string","null"],"maxLength":200,"description":"Ansprechpartner; `null`, wenn keiner erfasst ist"},"email":{"type":["string","null"],"format":"email","description":"E-Mail des Ansprechpartners; `null`, wenn keine oder eine leere angegeben wurde"},"phone":{"type":["string","null"],"maxLength":50,"description":"Telefonnummer; `null`, wenn keine erfasst ist"},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"description":"Woher der Lead kam; wirkt auf die Punktzahl von POST /leads/{id}/score"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"description":"Stufe im Vertriebsprozess. `converted` ist der Altbestand-Name fuer `won`"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Von Hand gesetzte Punktzahl; 0, wenn nie gesetzt"},"assignedTo":{"type":["string","null"],"format":"uuid","description":"Zustaendiger Benutzer; `null`, wenn niemand zugeordnet ist"},"notes":{"type":["string","null"],"description":"Freitext; `null`, wenn keiner erfasst ist"},"estimatedValue":{"type":["number","null"],"minimum":0,"description":"Geschaetzter Auftragswert in Euro; `null`, wenn nicht erfasst — NICHT 0"},"aiScoreSuggestion":{"type":["integer","null"],"minimum":0,"maximum":100,"description":"Zuletzt berechnete Punktzahl aus POST /leads/{id}/score; beim Anlegen `null`"},"aiScoreReason":{"type":["string","null"],"description":"Rechenweg dieser Punktzahl im Klartext; beim Anlegen `null`"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","companyName","contactName","email","phone","source","status","score","assignedTo","notes","estimatedValue","aiScoreSuggestion","aiScoreReason","createdAt","updatedAt"],"additionalProperties":false}]},"example":{"id":"9a1f4c2e-6b7d-4e8f-9a0b-1c2d3e4f5a6b","tenant_id":"musterbau-gmbh","company_name":"Schreinerei Weber & Söhne","contact_name":"Thomas Weber","email":"info@example.com","phone":"+49 89 987654-0","source":"web","status":"qualified","score":65,"assigned_to":"e2d4c6a8-0b1c-4d2e-8f3a-4b5c6d7e8f9a","notes":"Anfrage über das Kontaktformular, Interesse an Werkstattausstattung.","estimated_value":"18500.00","ai_score_suggestion":72,"ai_score_reason":"E-Mail hinterlegt (+10), Telefon hinterlegt (+10), Quelle web (+5), Stufe qualified (+20)","created_at":"2026-05-04T10:12:45.000Z","updated_at":"2026-05-18T16:30:02.000Z"}}}},"400":{"description":"Unzulaessiger Mandanten-Slug — Klartext `invalid_tenant_slug`, kein JSON"},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Kein Lead mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Abfrage gescheitert, aber NICHT an der Verbindung — ein Wiederholen hilft hier nicht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CrmLeadsById","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get lead","description":"Liefert einen einzelnen Lead. Ohne erreichbare Datenbank wird im prozesslokalen Zwischenspeicher gesucht — ein 404 kann dann auch heißen, dass der Lead zwar existiert, aber nur in der Datenbank steht."},"patch":{"responses":{"200":{"description":"Der geaenderte Lead, OHNE Umschlag","content":{"application/json":{"schema":{"anyOf":[{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `company_name`, `contact_name`, `email`, `phone`, `source`, `status`, `score`, `assigned_to`, `notes`, `estimated_value`, `ai_score_suggestion`, `ai_score_reason`, `created_at`, `updated_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. `estimated_value` ist NUMERIC und kommt bei postgres-js als Zeichenkette heraus, nicht als Zahl."},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Leads, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Lead gehoert"},"companyName":{"type":"string","minLength":1,"maxLength":200,"description":"Firmenname des Interessenten — das einzige Pflichtfeld"},"contactName":{"type":["string","null"],"maxLength":200,"description":"Ansprechpartner; `null`, wenn keiner erfasst ist"},"email":{"type":["string","null"],"format":"email","description":"E-Mail des Ansprechpartners; `null`, wenn keine oder eine leere angegeben wurde"},"phone":{"type":["string","null"],"maxLength":50,"description":"Telefonnummer; `null`, wenn keine erfasst ist"},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"description":"Woher der Lead kam; wirkt auf die Punktzahl von POST /leads/{id}/score"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"description":"Stufe im Vertriebsprozess. `converted` ist der Altbestand-Name fuer `won`"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Von Hand gesetzte Punktzahl; 0, wenn nie gesetzt"},"assignedTo":{"type":["string","null"],"format":"uuid","description":"Zustaendiger Benutzer; `null`, wenn niemand zugeordnet ist"},"notes":{"type":["string","null"],"description":"Freitext; `null`, wenn keiner erfasst ist"},"estimatedValue":{"type":["number","null"],"minimum":0,"description":"Geschaetzter Auftragswert in Euro; `null`, wenn nicht erfasst — NICHT 0"},"aiScoreSuggestion":{"type":["integer","null"],"minimum":0,"maximum":100,"description":"Zuletzt berechnete Punktzahl aus POST /leads/{id}/score; beim Anlegen `null`"},"aiScoreReason":{"type":["string","null"],"description":"Rechenweg dieser Punktzahl im Klartext; beim Anlegen `null`"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","companyName","contactName","email","phone","source","status","score","assignedTo","notes","estimatedValue","aiScoreSuggestion","aiScoreReason","createdAt","updatedAt"],"additionalProperties":false}]}}}},"400":{"description":"ZWEI Formen unter demselben Code: das rohe Zod-Ergebnis bei unzulaessigem Rumpf, oder `{ \"error\": \"No fields to update\" }`, wenn der Rumpf kein aenderbares Feld enthaelt. Ein unzulaessiger Mandanten-Slug antwortet dagegen mit Klartext.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]},{"type":"object","properties":{"error":{"type":"string","const":"No fields to update","description":"Der Rumpf enthielt kein Feld, das geschrieben werden koennte"}},"required":["error"],"additionalProperties":false}]}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Lead mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Aenderung gescheitert, aber NICHT an der Verbindung — es wurde nichts geaendert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — es wurde nichts geaendert, wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1CrmLeadsById","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update lead","description":"Ändert einzelne Felder eines Leads. Ein Rumpf ohne bekanntes Feld wird mit 400 abgelehnt, statt nichts zu tun und Erfolg zu melden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string","minLength":1,"maxLength":200},"contactName":{"type":"string","maxLength":200},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string","maxLength":50},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"default":"manual"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"default":"new"},"score":{"type":"integer","minimum":0,"maximum":100},"assignedTo":{"type":"string","format":"uuid"},"notes":{"type":"string"},"estimatedValue":{"type":"number","minimum":0}}},"example":{"companyName":"string","contactName":"string","email":"beispiel@example.com","phone":"string","source":"web","status":"new","score":0,"assignedTo":"00000000-0000-4000-8000-000000000000","notes":"string","estimatedValue":0}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzlast — auch bei unbekannter Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Unzulaessiger Mandanten-Slug — Klartext `invalid_tenant_slug`, kein JSON"},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"500":{"description":"Loeschen gescheitert, aber NICHT an der Verbindung — es wurde nichts entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — es wurde nichts entfernt, wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1CrmLeadsById","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete lead","description":"Löscht einen Lead endgültig (kein Soft-Delete). Der Aufruf ist bewusst idempotent und meldet auch dann 200 `{ ok: true }`, wenn es zu dieser ID gar keinen Lead gab — ein 200 ist hier also KEIN Beleg dafür, dass etwas gelöscht wurde. Es gibt hier kein 404."}},"/api/v1/crm/leads/{id}/convert":{"post":{"responses":{"200":{"description":"Quittung mit Hinweistext — auch bei unbekannter Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` — auch dann, wenn es den Lead gar nicht gab"},"message":{"type":"string","description":"Fester englischer Hinweis, den Kunden ueber POST /customers anzulegen — das tut dieser Aufruf nicht"}},"required":["ok","message"],"additionalProperties":false},"example":{"ok":true,"message":"string"}}}},"400":{"description":"Unzulaessiger Mandanten-Slug — Klartext `invalid_tenant_slug`, kein JSON"},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"500":{"description":"Statuswechsel gescheitert, aber NICHT an der Verbindung — der Status blieb stehen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — der Status blieb stehen, wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1CrmLeadsByIdConvert","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mark lead as converted (no customer is created)","description":"ACHTUNG — trotz des Pfadnamens entsteht hier KEIN Kunde. Der Aufruf setzt nur `status = \"converted\"` und antwortet mit dem Hinweis, den Kunden über POST /customers anzulegen; die Verknüpfung zwischen Lead und Kunde stellt er ebenfalls nicht her. Er meldet ausserdem auch dann `{ ok: true }`, wenn es zu dieser ID keinen Lead gibt — die Antwort sagt nichts darüber aus, ob wirklich ein Datensatz geändert wurde."}},"/api/v1/crm/leads/{id}/score":{"post":{"responses":{"200":{"description":"Punktzahl, Rechenweg und die Einzelfaktoren","content":{"application/json":{"schema":{"type":"object","properties":{"suggestion":{"type":"integer","minimum":0,"maximum":100,"description":"Die Punktzahl, auf 0..100 gekappt. Trotz des Namens steckt KEIN Sprachmodell dahinter"},"reason":{"type":"string","minLength":1,"description":"Der Rechenweg als deutscher Satz, aus denselben Summanden gebildet"},"factors":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"description":"Deutscher Name des Summanden, z. B. `E-Mail hinterlegt`"},"punkte":{"type":"integer","description":"Sein Beitrag zur Summe — kann negativ sein, etwa −30 fuer die Stufe `Verloren`"}},"required":["label","punkte"],"additionalProperties":false},"description":"Die Summanden einzeln, damit die Zahl nachrechenbar ist. Summanden mit 0 Punkten fehlen"}},"required":["suggestion","reason","factors"],"additionalProperties":false},"example":{"suggestion":0,"reason":"string","factors":[{"label":"string","punkte":0}]}}}},"400":{"description":"Unzulaessiger Mandanten-Slug — Klartext `invalid_tenant_slug`, kein JSON"},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Lead mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Berechnung oder Speichern gescheitert, aber NICHT an der Verbindung. Es kommt bewusst KEINE Punktzahl zurueck, wenn das Speichern scheiterte","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1CrmLeadsByIdScore","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Compute lead score from the recorded fields","description":"Rechnet einen Punktwert (0–100) aus den am Lead erfassten Angaben — deterministisch, OHNE Sprachmodell. Jede Teilpunktzahl kommt in `factors` mit, die Zahl ist also nachrechenbar. Zwei Stolpersteine: (1) Das Ergebnis landet in der Spalte `ai_score_suggestion` und heisst in der Antwort `suggestion` — beides sagt „KI\", die Rechnung ist aber keine. (2) Dieser Wert ist NICHT derselbe wie der von GET /leads/{id}/score: der liest aus `public.lead_scores` und wird nur von POST /leads/{id}/score/recompute gefüllt. Wer hier schreibt und dort liest, bekommt 404."},"get":{"responses":{"200":{"description":"Die zuletzt gespeicherte Bewertung","content":{"application/json":{"schema":{"type":"object","properties":{"leadId":{"type":"string","format":"uuid","description":"Die Kennung aus dem Pfad, unveraendert zurueckgegeben"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Der Punktwert; die Datenbank erzwingt den Bereich 0..100"},"factors":{"type":"object","additionalProperties":{"type":"number"},"description":"Je Merkmal sein Punktbeitrag. Welche Merkmale vorkommen, entscheidet das KI-Werkzeug — die Schluessel sind NICHT fest und koennen sich zwischen zwei Bewertungen unterscheiden"},"reasoning":{"type":["string","null"],"description":"Begruendung des Werkzeugs im Klartext; `null`, wenn keine gespeichert wurde"},"computedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Berechnung (ISO 8601, UTC)"}},"required":["leadId","score","factors","reasoning","computedAt"],"additionalProperties":false},"example":{"leadId":"00000000-0000-4000-8000-000000000000","score":0,"factors":{"beispiel":0},"reasoning":"string","computedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Fuer diesen Lead wurde noch keine KI-Bewertung berechnet (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Abfrage fehlgeschlagen (`internal_error`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`) — hier OHNE `retryAfter`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1CrmLeadsByIdScore","tags":["CRM","Leads","Scoring"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Read latest AI lead score","description":"Liest die zuletzt gespeicherte KI-Bewertung eines Leads aus `public.lead_scores`. Diese Tabelle füllt AUSSCHLIESSLICH POST /leads/{id}/score/recompute. Der Punktwert aus POST /leads/{id}/score ist ein anderer (deterministische Rechnung, gespeichert am Lead selbst) und erscheint hier NICHT — ohne vorherigen `recompute` antwortet dieser Aufruf deshalb mit 404, auch wenn der Lead existiert."}},"/api/v1/crm/leads/{id}/score/recompute":{"post":{"responses":{"200":{"description":"Die eben berechnete Bewertung — OHNE `computedAt`, den liest erst GET","content":{"application/json":{"schema":{"type":"object","properties":{"leadId":{"type":"string","description":"Die Kennung aus dem Pfad, unveraendert zurueckgegeben"},"score":{"type":"number","description":"Der eben berechnete Punktwert, wie das Werkzeug ihn liefert"},"factors":{"type":"object","additionalProperties":{"type":"number"},"description":"Je Merkmal sein Punktbeitrag. Welche Merkmale vorkommen, entscheidet das KI-Werkzeug — die Schluessel sind NICHT fest und koennen sich zwischen zwei Bewertungen unterscheiden"},"reasoning":{"type":"string","description":"Begruendung des Werkzeugs im Klartext — hier immer vorhanden"}},"required":["leadId","score","factors","reasoning"],"additionalProperties":false},"example":{"leadId":"string","score":0,"factors":{"beispiel":0},"reasoning":"string"}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"500":{"description":"Bewertung fehlgeschlagen. `error` traegt `internal_error`, `compute_failed` — oder die Meldung des Werkzeugs, wenn es selbst einen Grund genannt hat","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"KI-Werkzeug in dieser Umgebung nicht geladen (`tool_unavailable`) — es wird nichts ersatzweise gerechnet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1CrmLeadsByIdScoreRecompute","tags":["CRM","Leads","Scoring"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Recompute AI lead score","description":"Stösst die KI-Bewertung eines Leads neu an und gibt das Ergebnis zurück. Ist das KI-Paket in dieser Umgebung nicht geladen, antwortet der Aufruf mit 503 `tool_unavailable` — er rechnet dann NICHT ersatzweise selbst. Erst dieser Aufruf befüllt, was GET /leads/{id}/score später liest."}},"/api/v1/crm/leads/{id}/enrich":{"post":{"responses":{"200":{"description":"ZWEI Bauformen: bei `enriched: false` fehlt `data` ganz, bei `enriched: true` ist es dabei. `fieldsAdded` nennt die geaenderten Spalten in Datenbank-Schreibweise.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"enriched":{"type":"boolean","const":false,"description":"Es wurde nichts an den Lead geschrieben"},"fieldsAdded":{"type":"array","items":{"type":"string"},"maxItems":0,"description":"Immer leer in dieser Bauform"},"reason":{"type":"string","enum":["no_company_name","not_found"],"description":"`no_company_name` = am Lead steht kein Firmenname, es wurde gar nicht gesucht; `not_found` = die Quelle kennt die Firma nicht. FEHLT im dritten Fall: gefunden, aber jedes brauchbare Feld war leer"}},"required":["enriched","fieldsAdded"],"additionalProperties":false},{"type":"object","properties":{"enriched":{"type":"boolean","const":true,"description":"Es wurde mindestens ein Feld am Lead geschrieben"},"fieldsAdded":{"type":"array","items":{"type":"string"},"minItems":1,"description":"Die geschriebenen SPALTEN in Datenbank-Schreibweise. `enrichment_data` darin heisst: fuer diesen Wert gab es keine eigene Spalte, er liegt im JSONB-Sammelfeld"},"data":{"type":"object","properties":{"found":{"type":"boolean","const":true,"description":"In dieser Antwort immer `true` — bei `false` fehlt `data` ganz"},"name":{"type":"string","minLength":1,"description":"Firmenname, wie die Quelle ihn fuehrt — nicht zwingend der gesuchte Wortlaut"},"registerNumber":{"type":"string","description":"Registernummer; FEHLT, wenn die Quelle keine kennt"},"address":{"type":"string","description":"Vollstaendige Anschrift; FEHLT, wenn die Quelle keine kennt"},"ceo":{"type":"string","description":"Erste als Geschaeftsfuehrung erkannte Person; FEHLT, wenn keine eindeutig zuzuordnen war"},"foundedAt":{"type":"string","description":"Gruendungsdatum, wie die Quelle es schreibt; FEHLT haeufig"},"capital":{"type":"string","description":"Stammkapital. Von DIESER Quelle nie geliefert — das Feld steht nur im Vertrag der Schnittstelle"}},"required":["found","name"],"additionalProperties":false,"description":"Der unveraenderte Befund der Quelle — auch die Felder, die nicht passten"}},"required":["enriched","fieldsAdded","data"],"additionalProperties":false}]},"example":{"enriched":false,"fieldsAdded":[],"reason":"no_company_name"}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Kein Lead mit dieser Kennung in diesem Mandanten (`Lead not found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["DB unavailable","Lead not found"],"description":"Der Grund als englischer Text, nicht als Kennung"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar (`DB unavailable`) — hier OHNE `retryAfter`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["DB unavailable","Lead not found"],"description":"Der Grund als englischer Text, nicht als Kennung"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1CrmLeadsByIdEnrich","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Enrich a lead with handelsregister data","description":"Schlägt den Firmennamen des Leads im Handelsregister nach und schreibt gefundene Angaben (Anschrift, Registernummer, Geschäftsführung) an den Lead. Fehlt eine passende Spalte, landet der Wert stattdessen im JSONB-Feld `enrichment_data` — welcher Weg genommen wurde, steht in `fieldsAdded`. Die Umsatzsteuer-ID liefert die Quelle grundsätzlich nicht. Ein erfolgloser Lauf ist KEIN Fehler: Antwort 200 mit `enriched: false` und `reason` `no_company_name` oder `not_found`."}},"/api/v1/leads":{"get":{"responses":{"200":{"description":"Leads unter `items`, dazu `total`, `page` und `limit`","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"anyOf":[{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `company_name`, `contact_name`, `email`, `phone`, `source`, `status`, `score`, `assigned_to`, `notes`, `estimated_value`, `ai_score_suggestion`, `ai_score_reason`, `created_at`, `updated_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. `estimated_value` ist NUMERIC und kommt bei postgres-js als Zeichenkette heraus, nicht als Zahl."},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Leads, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Lead gehoert"},"companyName":{"type":"string","minLength":1,"maxLength":200,"description":"Firmenname des Interessenten — das einzige Pflichtfeld"},"contactName":{"type":["string","null"],"maxLength":200,"description":"Ansprechpartner; `null`, wenn keiner erfasst ist"},"email":{"type":["string","null"],"format":"email","description":"E-Mail des Ansprechpartners; `null`, wenn keine oder eine leere angegeben wurde"},"phone":{"type":["string","null"],"maxLength":50,"description":"Telefonnummer; `null`, wenn keine erfasst ist"},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"description":"Woher der Lead kam; wirkt auf die Punktzahl von POST /leads/{id}/score"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"description":"Stufe im Vertriebsprozess. `converted` ist der Altbestand-Name fuer `won`"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Von Hand gesetzte Punktzahl; 0, wenn nie gesetzt"},"assignedTo":{"type":["string","null"],"format":"uuid","description":"Zustaendiger Benutzer; `null`, wenn niemand zugeordnet ist"},"notes":{"type":["string","null"],"description":"Freitext; `null`, wenn keiner erfasst ist"},"estimatedValue":{"type":["number","null"],"minimum":0,"description":"Geschaetzter Auftragswert in Euro; `null`, wenn nicht erfasst — NICHT 0"},"aiScoreSuggestion":{"type":["integer","null"],"minimum":0,"maximum":100,"description":"Zuletzt berechnete Punktzahl aus POST /leads/{id}/score; beim Anlegen `null`"},"aiScoreReason":{"type":["string","null"],"description":"Rechenweg dieser Punktzahl im Klartext; beim Anlegen `null`"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","companyName","contactName","email","phone","source","status","score","assignedTo","notes","estimatedValue","aiScoreSuggestion","aiScoreReason","createdAt","updatedAt"],"additionalProperties":false}]},"description":"Die Leads dieser Seite, neueste zuerst"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer fuer die gesetzten Filter — echtes COUNT, nicht die Laenge der Seite"},"page":{"type":"integer","minimum":1,"description":"Die zurueckgegebene Seite, gezaehlt ab 1"},"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Groesse der Seite, wie angefragt"}},"required":["items","total","page","limit"],"additionalProperties":false},"example":{"items":[{}],"total":0,"page":1,"limit":1}}}},"400":{"description":"Query-Parameter abgelehnt (rohes Zod-Ergebnis). Ein unzulaessiger Mandanten-Slug antwortet unter demselben Code mit Klartext `invalid_tenant_slug`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"500":{"description":"Abfrage gescheitert, aber NICHT an der Verbindung — ein Wiederholen hilft hier nicht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Leads","tags":["CRM","Leads"],"parameters":[{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"source","schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"List leads","description":"Listet die Leads des Mandanten, seitenweise (`page`/`limit`) und filterbar nach `status`, `source` und `search` (Firma, Ansprechpartner, E-Mail). Ist die Datenbank nicht erreichbar, antwortet dieser Aufruf trotzdem mit 200 — dann aus einem prozesslokalen Zwischenspeicher, der nur enthält, was genau dieser Serverprozess selbst angelegt hat. Eine leere Liste heißt also nicht zwingend „keine Leads vorhanden\"."},"post":{"responses":{"201":{"description":"Der angelegte Lead, OHNE Umschlag. Immer die camelCase-Form — dieser Aufruf reicht keine Datenbankzeile durch, sondern gibt das Objekt zurueck, das er selbst gebaut hat","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Leads, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Lead gehoert"},"companyName":{"type":"string","minLength":1,"maxLength":200,"description":"Firmenname des Interessenten — das einzige Pflichtfeld"},"contactName":{"type":["string","null"],"maxLength":200,"description":"Ansprechpartner; `null`, wenn keiner erfasst ist"},"email":{"type":["string","null"],"format":"email","description":"E-Mail des Ansprechpartners; `null`, wenn keine oder eine leere angegeben wurde"},"phone":{"type":["string","null"],"maxLength":50,"description":"Telefonnummer; `null`, wenn keine erfasst ist"},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"description":"Woher der Lead kam; wirkt auf die Punktzahl von POST /leads/{id}/score"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"description":"Stufe im Vertriebsprozess. `converted` ist der Altbestand-Name fuer `won`"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Von Hand gesetzte Punktzahl; 0, wenn nie gesetzt"},"assignedTo":{"type":["string","null"],"format":"uuid","description":"Zustaendiger Benutzer; `null`, wenn niemand zugeordnet ist"},"notes":{"type":["string","null"],"description":"Freitext; `null`, wenn keiner erfasst ist"},"estimatedValue":{"type":["number","null"],"minimum":0,"description":"Geschaetzter Auftragswert in Euro; `null`, wenn nicht erfasst — NICHT 0"},"aiScoreSuggestion":{"type":["integer","null"],"minimum":0,"maximum":100,"description":"Zuletzt berechnete Punktzahl aus POST /leads/{id}/score; beim Anlegen `null`"},"aiScoreReason":{"type":["string","null"],"description":"Rechenweg dieser Punktzahl im Klartext; beim Anlegen `null`"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","companyName","contactName","email","phone","source","status","score","assignedTo","notes","estimatedValue","aiScoreSuggestion","aiScoreReason","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","companyName":"string","contactName":"string","email":"beispiel@example.com","phone":"string","source":"web","status":"new","score":0,"assignedTo":"00000000-0000-4000-8000-000000000000","notes":"string","estimatedValue":0,"aiScoreSuggestion":0,"aiScoreReason":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis). Ein unzulaessiger Mandanten-Slug antwortet unter demselben Code mit Klartext `invalid_tenant_slug`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"500":{"description":"Anlage gescheitert, aber NICHT an der Verbindung — es wurde nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — es wurde nichts gespeichert, wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Leads","tags":["CRM","Leads"],"parameters":[],"summary":"Create lead","description":"Legt einen Lead an. Die ID vergibt der Server. ACHTUNG — ist die Datenbank nicht erreichbar, kommt trotzdem 201: der Lead liegt dann ausschliesslich im Arbeitsspeicher DIESES Serverprozesses, ist für andere Instanzen unsichtbar und beim nächsten Neustart weg. Ein 201 ist hier also keine Zusage, dass der Lead dauerhaft gespeichert wurde. Scheitert dagegen ein INSERT bei erreichbarer Datenbank, kommt ehrlich ein 503 statt eines stillen 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string","minLength":1,"maxLength":200},"contactName":{"type":"string","maxLength":200},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string","maxLength":50},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"default":"manual"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"default":"new"},"score":{"type":"integer","minimum":0,"maximum":100},"assignedTo":{"type":"string","format":"uuid"},"notes":{"type":"string"},"estimatedValue":{"type":"number","minimum":0}},"required":["companyName"]},"example":{"companyName":"string","contactName":"string","email":"beispiel@example.com","phone":"string","source":"web","status":"new","score":0,"assignedTo":"00000000-0000-4000-8000-000000000000","notes":"string","estimatedValue":0}}}}}},"/api/v1/leads/{id}":{"get":{"responses":{"200":{"description":"Der Lead, OHNE Umschlag — die Felder stehen direkt im Wurzelobjekt","content":{"application/json":{"schema":{"anyOf":[{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `company_name`, `contact_name`, `email`, `phone`, `source`, `status`, `score`, `assigned_to`, `notes`, `estimated_value`, `ai_score_suggestion`, `ai_score_reason`, `created_at`, `updated_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. `estimated_value` ist NUMERIC und kommt bei postgres-js als Zeichenkette heraus, nicht als Zahl."},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Leads, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Lead gehoert"},"companyName":{"type":"string","minLength":1,"maxLength":200,"description":"Firmenname des Interessenten — das einzige Pflichtfeld"},"contactName":{"type":["string","null"],"maxLength":200,"description":"Ansprechpartner; `null`, wenn keiner erfasst ist"},"email":{"type":["string","null"],"format":"email","description":"E-Mail des Ansprechpartners; `null`, wenn keine oder eine leere angegeben wurde"},"phone":{"type":["string","null"],"maxLength":50,"description":"Telefonnummer; `null`, wenn keine erfasst ist"},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"description":"Woher der Lead kam; wirkt auf die Punktzahl von POST /leads/{id}/score"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"description":"Stufe im Vertriebsprozess. `converted` ist der Altbestand-Name fuer `won`"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Von Hand gesetzte Punktzahl; 0, wenn nie gesetzt"},"assignedTo":{"type":["string","null"],"format":"uuid","description":"Zustaendiger Benutzer; `null`, wenn niemand zugeordnet ist"},"notes":{"type":["string","null"],"description":"Freitext; `null`, wenn keiner erfasst ist"},"estimatedValue":{"type":["number","null"],"minimum":0,"description":"Geschaetzter Auftragswert in Euro; `null`, wenn nicht erfasst — NICHT 0"},"aiScoreSuggestion":{"type":["integer","null"],"minimum":0,"maximum":100,"description":"Zuletzt berechnete Punktzahl aus POST /leads/{id}/score; beim Anlegen `null`"},"aiScoreReason":{"type":["string","null"],"description":"Rechenweg dieser Punktzahl im Klartext; beim Anlegen `null`"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","companyName","contactName","email","phone","source","status","score","assignedTo","notes","estimatedValue","aiScoreSuggestion","aiScoreReason","createdAt","updatedAt"],"additionalProperties":false}]},"example":{"id":"9a1f4c2e-6b7d-4e8f-9a0b-1c2d3e4f5a6b","tenant_id":"musterbau-gmbh","company_name":"Schreinerei Weber & Söhne","contact_name":"Thomas Weber","email":"info@example.com","phone":"+49 89 987654-0","source":"web","status":"qualified","score":65,"assigned_to":"e2d4c6a8-0b1c-4d2e-8f3a-4b5c6d7e8f9a","notes":"Anfrage über das Kontaktformular, Interesse an Werkstattausstattung.","estimated_value":"18500.00","ai_score_suggestion":72,"ai_score_reason":"E-Mail hinterlegt (+10), Telefon hinterlegt (+10), Quelle web (+5), Stufe qualified (+20)","created_at":"2026-05-04T10:12:45.000Z","updated_at":"2026-05-18T16:30:02.000Z"}}}},"400":{"description":"Unzulaessiger Mandanten-Slug — Klartext `invalid_tenant_slug`, kein JSON"},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Kein Lead mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Abfrage gescheitert, aber NICHT an der Verbindung — ein Wiederholen hilft hier nicht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1LeadsById","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get lead","description":"Liefert einen einzelnen Lead. Ohne erreichbare Datenbank wird im prozesslokalen Zwischenspeicher gesucht — ein 404 kann dann auch heißen, dass der Lead zwar existiert, aber nur in der Datenbank steht."},"patch":{"responses":{"200":{"description":"Der geaenderte Lead, OHNE Umschlag","content":{"application/json":{"schema":{"anyOf":[{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `company_name`, `contact_name`, `email`, `phone`, `source`, `status`, `score`, `assigned_to`, `notes`, `estimated_value`, `ai_score_suggestion`, `ai_score_reason`, `created_at`, `updated_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. `estimated_value` ist NUMERIC und kommt bei postgres-js als Zeichenkette heraus, nicht als Zahl."},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Leads, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem der Lead gehoert"},"companyName":{"type":"string","minLength":1,"maxLength":200,"description":"Firmenname des Interessenten — das einzige Pflichtfeld"},"contactName":{"type":["string","null"],"maxLength":200,"description":"Ansprechpartner; `null`, wenn keiner erfasst ist"},"email":{"type":["string","null"],"format":"email","description":"E-Mail des Ansprechpartners; `null`, wenn keine oder eine leere angegeben wurde"},"phone":{"type":["string","null"],"maxLength":50,"description":"Telefonnummer; `null`, wenn keine erfasst ist"},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"description":"Woher der Lead kam; wirkt auf die Punktzahl von POST /leads/{id}/score"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"description":"Stufe im Vertriebsprozess. `converted` ist der Altbestand-Name fuer `won`"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Von Hand gesetzte Punktzahl; 0, wenn nie gesetzt"},"assignedTo":{"type":["string","null"],"format":"uuid","description":"Zustaendiger Benutzer; `null`, wenn niemand zugeordnet ist"},"notes":{"type":["string","null"],"description":"Freitext; `null`, wenn keiner erfasst ist"},"estimatedValue":{"type":["number","null"],"minimum":0,"description":"Geschaetzter Auftragswert in Euro; `null`, wenn nicht erfasst — NICHT 0"},"aiScoreSuggestion":{"type":["integer","null"],"minimum":0,"maximum":100,"description":"Zuletzt berechnete Punktzahl aus POST /leads/{id}/score; beim Anlegen `null`"},"aiScoreReason":{"type":["string","null"],"description":"Rechenweg dieser Punktzahl im Klartext; beim Anlegen `null`"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","companyName","contactName","email","phone","source","status","score","assignedTo","notes","estimatedValue","aiScoreSuggestion","aiScoreReason","createdAt","updatedAt"],"additionalProperties":false}]}}}},"400":{"description":"ZWEI Formen unter demselben Code: das rohe Zod-Ergebnis bei unzulaessigem Rumpf, oder `{ \"error\": \"No fields to update\" }`, wenn der Rumpf kein aenderbares Feld enthaelt. Ein unzulaessiger Mandanten-Slug antwortet dagegen mit Klartext.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]},{"type":"object","properties":{"error":{"type":"string","const":"No fields to update","description":"Der Rumpf enthielt kein Feld, das geschrieben werden koennte"}},"required":["error"],"additionalProperties":false}]}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Lead mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Aenderung gescheitert, aber NICHT an der Verbindung — es wurde nichts geaendert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — es wurde nichts geaendert, wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1LeadsById","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update lead","description":"Ändert einzelne Felder eines Leads. Ein Rumpf ohne bekanntes Feld wird mit 400 abgelehnt, statt nichts zu tun und Erfolg zu melden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string","minLength":1,"maxLength":200},"contactName":{"type":"string","maxLength":200},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string","maxLength":50},"source":{"type":"string","enum":["web","referral","campaign","manual","ai","import"],"default":"manual"},"status":{"type":"string","enum":["new","contacted","qualified","meeting_scheduled","proposal_sent","negotiation","won","lost","converted","on_hold","nurture"],"default":"new"},"score":{"type":"integer","minimum":0,"maximum":100},"assignedTo":{"type":"string","format":"uuid"},"notes":{"type":"string"},"estimatedValue":{"type":"number","minimum":0}}},"example":{"companyName":"string","contactName":"string","email":"beispiel@example.com","phone":"string","source":"web","status":"new","score":0,"assignedTo":"00000000-0000-4000-8000-000000000000","notes":"string","estimatedValue":0}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzlast — auch bei unbekannter Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Unzulaessiger Mandanten-Slug — Klartext `invalid_tenant_slug`, kein JSON"},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"500":{"description":"Loeschen gescheitert, aber NICHT an der Verbindung — es wurde nichts entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — es wurde nichts entfernt, wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1LeadsById","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete lead","description":"Löscht einen Lead endgültig (kein Soft-Delete). Der Aufruf ist bewusst idempotent und meldet auch dann 200 `{ ok: true }`, wenn es zu dieser ID gar keinen Lead gab — ein 200 ist hier also KEIN Beleg dafür, dass etwas gelöscht wurde. Es gibt hier kein 404."}},"/api/v1/leads/{id}/convert":{"post":{"responses":{"200":{"description":"Quittung mit Hinweistext — auch bei unbekannter Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` — auch dann, wenn es den Lead gar nicht gab"},"message":{"type":"string","description":"Fester englischer Hinweis, den Kunden ueber POST /customers anzulegen — das tut dieser Aufruf nicht"}},"required":["ok","message"],"additionalProperties":false},"example":{"ok":true,"message":"string"}}}},"400":{"description":"Unzulaessiger Mandanten-Slug — Klartext `invalid_tenant_slug`, kein JSON"},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"500":{"description":"Statuswechsel gescheitert, aber NICHT an der Verbindung — der Status blieb stehen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — der Status blieb stehen, wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1LeadsByIdConvert","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mark lead as converted (no customer is created)","description":"ACHTUNG — trotz des Pfadnamens entsteht hier KEIN Kunde. Der Aufruf setzt nur `status = \"converted\"` und antwortet mit dem Hinweis, den Kunden über POST /customers anzulegen; die Verknüpfung zwischen Lead und Kunde stellt er ebenfalls nicht her. Er meldet ausserdem auch dann `{ ok: true }`, wenn es zu dieser ID keinen Lead gibt — die Antwort sagt nichts darüber aus, ob wirklich ein Datensatz geändert wurde."}},"/api/v1/leads/{id}/score":{"post":{"responses":{"200":{"description":"Punktzahl, Rechenweg und die Einzelfaktoren","content":{"application/json":{"schema":{"type":"object","properties":{"suggestion":{"type":"integer","minimum":0,"maximum":100,"description":"Die Punktzahl, auf 0..100 gekappt. Trotz des Namens steckt KEIN Sprachmodell dahinter"},"reason":{"type":"string","minLength":1,"description":"Der Rechenweg als deutscher Satz, aus denselben Summanden gebildet"},"factors":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"description":"Deutscher Name des Summanden, z. B. `E-Mail hinterlegt`"},"punkte":{"type":"integer","description":"Sein Beitrag zur Summe — kann negativ sein, etwa −30 fuer die Stufe `Verloren`"}},"required":["label","punkte"],"additionalProperties":false},"description":"Die Summanden einzeln, damit die Zahl nachrechenbar ist. Summanden mit 0 Punkten fehlen"}},"required":["suggestion","reason","factors"],"additionalProperties":false},"example":{"suggestion":0,"reason":"string","factors":[{"label":"string","punkte":0}]}}}},"400":{"description":"Unzulaessiger Mandanten-Slug — Klartext `invalid_tenant_slug`, kein JSON"},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Lead mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Berechnung oder Speichern gescheitert, aber NICHT an der Verbindung. Es kommt bewusst KEINE Punktzahl zurueck, wenn das Speichern scheiterte","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Fester deutscher Satz `Unerwarteter Serverfehler` — ohne Einzelheiten"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Verbindung zur Datenbank verloren — wiederholbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1LeadsByIdScore","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Compute lead score from the recorded fields","description":"Rechnet einen Punktwert (0–100) aus den am Lead erfassten Angaben — deterministisch, OHNE Sprachmodell. Jede Teilpunktzahl kommt in `factors` mit, die Zahl ist also nachrechenbar. Zwei Stolpersteine: (1) Das Ergebnis landet in der Spalte `ai_score_suggestion` und heisst in der Antwort `suggestion` — beides sagt „KI\", die Rechnung ist aber keine. (2) Dieser Wert ist NICHT derselbe wie der von GET /leads/{id}/score: der liest aus `public.lead_scores` und wird nur von POST /leads/{id}/score/recompute gefüllt. Wer hier schreibt und dort liest, bekommt 404."},"get":{"responses":{"200":{"description":"Die zuletzt gespeicherte Bewertung","content":{"application/json":{"schema":{"type":"object","properties":{"leadId":{"type":"string","format":"uuid","description":"Die Kennung aus dem Pfad, unveraendert zurueckgegeben"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Der Punktwert; die Datenbank erzwingt den Bereich 0..100"},"factors":{"type":"object","additionalProperties":{"type":"number"},"description":"Je Merkmal sein Punktbeitrag. Welche Merkmale vorkommen, entscheidet das KI-Werkzeug — die Schluessel sind NICHT fest und koennen sich zwischen zwei Bewertungen unterscheiden"},"reasoning":{"type":["string","null"],"description":"Begruendung des Werkzeugs im Klartext; `null`, wenn keine gespeichert wurde"},"computedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Berechnung (ISO 8601, UTC)"}},"required":["leadId","score","factors","reasoning","computedAt"],"additionalProperties":false},"example":{"leadId":"00000000-0000-4000-8000-000000000000","score":0,"factors":{"beispiel":0},"reasoning":"string","computedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Fuer diesen Lead wurde noch keine KI-Bewertung berechnet (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Abfrage fehlgeschlagen (`internal_error`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar (`database_unavailable`) — hier OHNE `retryAfter`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1LeadsByIdScore","tags":["CRM","Leads","Scoring"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Read latest AI lead score","description":"Liest die zuletzt gespeicherte KI-Bewertung eines Leads aus `public.lead_scores`. Diese Tabelle füllt AUSSCHLIESSLICH POST /leads/{id}/score/recompute. Der Punktwert aus POST /leads/{id}/score ist ein anderer (deterministische Rechnung, gespeichert am Lead selbst) und erscheint hier NICHT — ohne vorherigen `recompute` antwortet dieser Aufruf deshalb mit 404, auch wenn der Lead existiert."}},"/api/v1/leads/{id}/score/recompute":{"post":{"responses":{"200":{"description":"Die eben berechnete Bewertung — OHNE `computedAt`, den liest erst GET","content":{"application/json":{"schema":{"type":"object","properties":{"leadId":{"type":"string","description":"Die Kennung aus dem Pfad, unveraendert zurueckgegeben"},"score":{"type":"number","description":"Der eben berechnete Punktwert, wie das Werkzeug ihn liefert"},"factors":{"type":"object","additionalProperties":{"type":"number"},"description":"Je Merkmal sein Punktbeitrag. Welche Merkmale vorkommen, entscheidet das KI-Werkzeug — die Schluessel sind NICHT fest und koennen sich zwischen zwei Bewertungen unterscheiden"},"reasoning":{"type":"string","description":"Begruendung des Werkzeugs im Klartext — hier immer vorhanden"}},"required":["leadId","score","factors","reasoning"],"additionalProperties":false},"example":{"leadId":"string","score":0,"factors":{"beispiel":0},"reasoning":"string"}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"500":{"description":"Bewertung fehlgeschlagen. `error` traegt `internal_error`, `compute_failed` — oder die Meldung des Werkzeugs, wenn es selbst einen Grund genannt hat","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"KI-Werkzeug in dieser Umgebung nicht geladen (`tool_unavailable`) — es wird nichts ersatzweise gerechnet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","minLength":1,"description":"Feste Kennung: `not_found`, `database_unavailable`, `tool_unavailable`, `internal_error` — oder die Meldung des Werkzeugs, wenn die Bewertung selbst scheiterte"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1LeadsByIdScoreRecompute","tags":["CRM","Leads","Scoring"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Recompute AI lead score","description":"Stösst die KI-Bewertung eines Leads neu an und gibt das Ergebnis zurück. Ist das KI-Paket in dieser Umgebung nicht geladen, antwortet der Aufruf mit 503 `tool_unavailable` — er rechnet dann NICHT ersatzweise selbst. Erst dieser Aufruf befüllt, was GET /leads/{id}/score später liest."}},"/api/v1/leads/{id}/enrich":{"post":{"responses":{"200":{"description":"ZWEI Bauformen: bei `enriched: false` fehlt `data` ganz, bei `enriched: true` ist es dabei. `fieldsAdded` nennt die geaenderten Spalten in Datenbank-Schreibweise.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"enriched":{"type":"boolean","const":false,"description":"Es wurde nichts an den Lead geschrieben"},"fieldsAdded":{"type":"array","items":{"type":"string"},"maxItems":0,"description":"Immer leer in dieser Bauform"},"reason":{"type":"string","enum":["no_company_name","not_found"],"description":"`no_company_name` = am Lead steht kein Firmenname, es wurde gar nicht gesucht; `not_found` = die Quelle kennt die Firma nicht. FEHLT im dritten Fall: gefunden, aber jedes brauchbare Feld war leer"}},"required":["enriched","fieldsAdded"],"additionalProperties":false},{"type":"object","properties":{"enriched":{"type":"boolean","const":true,"description":"Es wurde mindestens ein Feld am Lead geschrieben"},"fieldsAdded":{"type":"array","items":{"type":"string"},"minItems":1,"description":"Die geschriebenen SPALTEN in Datenbank-Schreibweise. `enrichment_data` darin heisst: fuer diesen Wert gab es keine eigene Spalte, er liegt im JSONB-Sammelfeld"},"data":{"type":"object","properties":{"found":{"type":"boolean","const":true,"description":"In dieser Antwort immer `true` — bei `false` fehlt `data` ganz"},"name":{"type":"string","minLength":1,"description":"Firmenname, wie die Quelle ihn fuehrt — nicht zwingend der gesuchte Wortlaut"},"registerNumber":{"type":"string","description":"Registernummer; FEHLT, wenn die Quelle keine kennt"},"address":{"type":"string","description":"Vollstaendige Anschrift; FEHLT, wenn die Quelle keine kennt"},"ceo":{"type":"string","description":"Erste als Geschaeftsfuehrung erkannte Person; FEHLT, wenn keine eindeutig zuzuordnen war"},"foundedAt":{"type":"string","description":"Gruendungsdatum, wie die Quelle es schreibt; FEHLT haeufig"},"capital":{"type":"string","description":"Stammkapital. Von DIESER Quelle nie geliefert — das Feld steht nur im Vertrag der Schnittstelle"}},"required":["found","name"],"additionalProperties":false,"description":"Der unveraenderte Befund der Quelle — auch die Felder, die nicht passten"}},"required":["enriched","fieldsAdded","data"],"additionalProperties":false}]},"example":{"enriched":false,"fieldsAdded":[],"reason":"no_company_name"}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Kein Lead mit dieser Kennung in diesem Mandanten (`Lead not found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["DB unavailable","Lead not found"],"description":"Der Grund als englischer Text, nicht als Kennung"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar (`DB unavailable`) — hier OHNE `retryAfter`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["DB unavailable","Lead not found"],"description":"Der Grund als englischer Text, nicht als Kennung"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1LeadsByIdEnrich","tags":["CRM","Leads"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Enrich a lead with handelsregister data","description":"Schlägt den Firmennamen des Leads im Handelsregister nach und schreibt gefundene Angaben (Anschrift, Registernummer, Geschäftsführung) an den Lead. Fehlt eine passende Spalte, landet der Wert stattdessen im JSONB-Feld `enrichment_data` — welcher Weg genommen wurde, steht in `fieldsAdded`. Die Umsatzsteuer-ID liefert die Quelle grundsätzlich nicht. Ein erfolgloser Lauf ist KEIN Fehler: Antwort 200 mit `enriched: false` und `reason` `no_company_name` oder `not_found`."}},"/api/v1/crm/contacts":{"get":{"responses":{"200":{"description":"Die Kontakte dieser Seite samt Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{},"description":"Der Kontakt in Datenbank-Schreibweise. Der Handler reicht `SELECT c.*` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. Beim Lesen kommen zwei Felder AUS DER KUNDENTABELLE hinzu, die es in `contacts` gar nicht gibt: `customer_name` und `customer_number`. Beim Anlegen und Aendern fehlen genau diese beiden."},"description":"Die Kontakte dieser Seite, aufsteigend nach Nachname und Vorname"},"meta":{"type":"object","properties":{"page":{"type":"integer","minimum":1,"description":"Die zurueckgegebene Seite, gezaehlt ab 1"},"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Groesse der Seite, wie angefragt"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer fuer die gesetzten Filter"},"pages":{"type":"integer","minimum":0,"description":"Zahl der Seiten insgesamt — `total` geteilt durch `limit`, aufgerundet"}},"required":["page","limit","total","pages"],"additionalProperties":false,"description":"Seitenangaben. Achtung: hier heisst der Block `meta`, nicht `pagination` wie anderswo"}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{}],"meta":{"page":1,"limit":1,"total":0,"pages":0}}}}},"400":{"description":"Query-Parameter abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"ZWEI Formen: `Database unavailable`, wenn Datenbank oder Mandantenkontext ganz fehlen — oder `contacts_query_failed` MIT der Datenbankmeldung, wenn die Abfrage selbst scheiterte.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable","description":"Kein Datenbank-Client ODER kein Mandantenkontext — die Antwort unterscheidet das nicht"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["contacts_query_failed","pipeline_query_failed"],"description":"Welche Abfrage scheiterte"},"message":{"type":"string","description":"Die Meldung der Datenbank im Wortlaut — anders als bei den Schreibfehlern wird sie hier durchgereicht"}},"required":["error","message"],"additionalProperties":false}]}}}}},"operationId":"getApiV1CrmContacts","tags":["CRM"],"parameters":[{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive","lead",""]}},{"in":"query","name":"customerId","schema":{"type":"string","format":"uuid"}}],"summary":"CRM-Kontakte auflisten","description":"Listet die Kontakte des Mandanten, seitenweise und filterbar nach Suchtext, Status und Kunde. Die Suche trifft Vorname, Nachname, E-Mail und Funktion. Geloeschte Kontakte bleiben aussen vor. Bei einem Fehler kommt bewusst KEINE leere Liste mit 200 zurueck, sondern 503 — eine leere Liste waere von „dieser Mandant hat keine Kontakte\" nicht zu unterscheiden."},"post":{"responses":{"201":{"description":"Der angelegte Kontakt in Datenbank-Schreibweise","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Der Kontakt in Datenbank-Schreibweise. Der Handler reicht `SELECT c.*` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. Beim Lesen kommen zwei Felder AUS DER KUNDENTABELLE hinzu, die es in `contacts` gar nicht gibt: `customer_name` und `customer_number`. Beim Anlegen und Aendern fehlen genau diese beiden."}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"ZWEI Formen: `Database unavailable`, wenn Datenbank oder Mandantenkontext ganz fehlen — oder `Create failed`, wenn das Anlegen scheiterte. Der Grund steht dann NUR im Server-Protokoll.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable","description":"Kein Datenbank-Client ODER kein Mandantenkontext — die Antwort unterscheidet das nicht"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["Create failed","Update failed","Delete failed"],"description":"Die Abfrage scheiterte. Der Grund steht NUR im Server-Protokoll, nicht in der Antwort"}},"required":["error"],"additionalProperties":false}]}}}}},"operationId":"postApiV1CrmContacts","tags":["CRM"],"parameters":[],"summary":"CRM-Kontakt anlegen","description":"Legt einen Kontakt an und gibt die Datenbankzeile OHNE Umschlag zurueck. Die Personennummer (`P-NNNNNN`, beginnend bei `P-500001`) vergibt der Server. Ist `isPrimary` gesetzt UND ein Kunde angegeben, verlieren alle anderen Kontakte dieses Kunden ihre Hauptkontakt-Markierung — der Aufruf aendert dann also mehr als eine Zeile. Die beiden Join-Felder `customer_name` und `customer_number` fehlen hier, anders als beim Lesen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"salutation":{"type":"string","enum":["Herr","Frau","Divers"]},"firstName":{"type":"string","maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string","maxLength":50},"mobile":{"type":"string","maxLength":50},"role":{"type":"string","maxLength":100},"department":{"type":"string","maxLength":100},"company":{"type":"string","maxLength":255},"status":{"type":"string","enum":["active","inactive","lead"],"default":"active"},"isPrimary":{"type":"boolean","default":false},"notes":{"type":"string"}},"required":["lastName"]},"example":{"customerId":"00000000-0000-4000-8000-000000000000","salutation":"Herr","firstName":"string","lastName":"string","email":"beispiel@example.com","phone":"string","mobile":"string","role":"string","department":"string","company":"string","status":"active","isPrimary":true,"notes":"string"}}}}}},"/api/v1/crm/contacts/{id}":{"get":{"responses":{"200":{"description":"Der Kontakt in Datenbank-Schreibweise","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Der Kontakt in Datenbank-Schreibweise. Der Handler reicht `SELECT c.*` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. Beim Lesen kommen zwei Felder AUS DER KUNDENTABELLE hinzu, die es in `contacts` gar nicht gibt: `customer_name` und `customer_number`. Beim Anlegen und Aendern fehlen genau diese beiden."},"example":{"id":"3c9e7a10-5b2d-4e8f-a1c3-7d6e5f4a3b2c","customer_id":"b7d2e4f6-1a3c-4d5e-8f9a-0b1c2d3e4f5a","person_number":"AP-0042","salutation":"Frau","title":"Dipl.-Ing.","first_name":"Sabine","last_name":"Krüger","email":"sabine.krueger@example.com","phone":"+49 30 1234567-0","mobile":"+49 170 1234567","role":"Einkaufsleitung","department":"Einkauf","is_primary":true,"opt_in":true,"notes":"Bevorzugt Rückruf vormittags.","custom_fields":{},"deleted_at":null,"created_at":"2026-02-03T08:40:12.000Z","updated_at":"2026-04-21T14:05:37.000Z","customer_name":"Musterbau GmbH","customer_number":"K-10023"}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht vorhanden, geloescht — ODER die Abfrage ist gescheitert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank oder Mandantenkontext fehlen ganz","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable","description":"Kein Datenbank-Client ODER kein Mandantenkontext — die Antwort unterscheidet das nicht"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1CrmContactsById","tags":["CRM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelnen CRM-Kontakt abrufen","description":"Liefert einen Kontakt OHNE Umschlag — die Felder stehen direkt im Wurzelobjekt, dazu `customer_name` und `customer_number` aus der Kundentabelle. ACHTUNG: scheitert die ABFRAGE, antwortet dieser Handler ebenfalls mit 404 statt mit 503. Ein 404 heisst hier also „nicht gefunden ODER nicht lesbar\"."},"patch":{"responses":{"200":{"description":"Der geaenderte Kontakt in Datenbank-Schreibweise","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Der Kontakt in Datenbank-Schreibweise. Der Handler reicht `SELECT c.*` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu. Beim Lesen kommen zwei Felder AUS DER KUNDENTABELLE hinzu, die es in `contacts` gar nicht gibt: `customer_name` und `customer_number`. Beim Anlegen und Aendern fehlen genau diese beiden."}}}},"400":{"description":"ZWEI Formen unter demselben Code: das rohe Zod-Ergebnis bei unzulaessigem Rumpf, oder `{ \"error\": \"No fields to update\" }`, wenn der Rumpf kein aenderbares Feld enthaelt.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]},{"type":"object","properties":{"error":{"type":"string","const":"No fields to update","description":"Der Rumpf enthielt kein Feld, das geschrieben werden koennte"}},"required":["error"],"additionalProperties":false}]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Kontakt mit dieser Kennung, oder er ist bereits geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"ZWEI Formen: `Database unavailable` bei fehlender Datenbank oder fehlendem Mandantenkontext, sonst `Update failed` — der Grund steht dann NUR im Server-Protokoll.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable","description":"Kein Datenbank-Client ODER kein Mandantenkontext — die Antwort unterscheidet das nicht"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["Create failed","Update failed","Delete failed"],"description":"Die Abfrage scheiterte. Der Grund steht NUR im Server-Protokoll, nicht in der Antwort"}},"required":["error"],"additionalProperties":false}]}}}}},"operationId":"patchApiV1CrmContactsById","tags":["CRM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"CRM-Kontakt bearbeiten","description":"Aendert einzelne Felder eines Kontakts und gibt die Zeile OHNE Umschlag zurueck. Leere Texte werden zu `null` gespeichert, nicht als leerer Text. `customerId` laesst sich hier NICHT umhaengen — das Feld wird stillschweigend uebergangen, obwohl der Rumpf es annimmt. Die beiden Join-Felder fehlen in der Antwort, anders als beim Lesen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"salutation":{"type":"string","enum":["Herr","Frau","Divers"]},"firstName":{"type":"string","maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string","maxLength":50},"mobile":{"type":"string","maxLength":50},"role":{"type":"string","maxLength":100},"department":{"type":"string","maxLength":100},"company":{"type":"string","maxLength":255},"status":{"type":"string","enum":["active","inactive","lead"],"default":"active"},"isPrimary":{"type":"boolean","default":false},"notes":{"type":"string"}}},"example":{"customerId":"00000000-0000-4000-8000-000000000000","salutation":"Herr","firstName":"string","lastName":"string","email":"beispiel@example.com","phone":"string","mobile":"string","role":"string","department":"string","company":"string","status":"active","isPrimary":true,"notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzlast — auch bei unbekannter Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"ZWEI Formen: `Database unavailable` bei fehlender Datenbank oder fehlendem Mandantenkontext, sonst `Delete failed` — der Grund steht dann NUR im Server-Protokoll.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable","description":"Kein Datenbank-Client ODER kein Mandantenkontext — die Antwort unterscheidet das nicht"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["Create failed","Update failed","Delete failed"],"description":"Die Abfrage scheiterte. Der Grund steht NUR im Server-Protokoll, nicht in der Antwort"}},"required":["error"],"additionalProperties":false}]}}}}},"operationId":"deleteApiV1CrmContactsById","tags":["CRM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"CRM-Kontakt soft-löschen","description":"Setzt `deleted_at` — die Zeile bleibt in der Datenbank stehen und ist nur nicht mehr sichtbar. Die Antwort ist auch dann 200, wenn es die Kennung gar nicht gibt oder der Kontakt schon geloescht war: das UPDATE trifft dann null Zeilen, und der Handler prueft das nicht nach. Ein 200 belegt hier also nicht, dass etwas geloescht wurde."}},"/api/v1/crm/pipeline":{"get":{"responses":{"200":{"description":"Der Trichter mit genau sechs Stufen","content":{"application/json":{"schema":{"type":"object","properties":{"stages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["neu","qualifiziert","angebot","verhandlung","gewonnen","verloren"],"description":"Kennung der Stufe"},"count":{"type":"integer","minimum":0,"description":"Zahl der Chancen in dieser Stufe"},"totalValue":{"type":"number","description":"Summe der erwarteten Umsaetze dieser Stufe in Euro"},"deals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Verkaufschance (UUID)"},"bezeichnung":{"type":"string","description":"Bezeichnung der Chance"},"kundeName":{"type":"string","description":"Name des Kunden als Text — nicht zwingend ein angelegter Kunde"},"stage":{"type":"string","enum":["neu","qualifiziert","angebot","verhandlung","gewonnen","verloren"],"description":"Stufe der Chance; entspricht der `id` der umgebenden Stufe"},"wahrscheinlichkeit":{"type":"integer","minimum":0,"maximum":100,"description":"Abschlusswahrscheinlichkeit in Prozent"},"erwarteteEinnahmen":{"type":"number","description":"Erwarteter Umsatz in Euro; 0, wenn nichts erfasst ist"},"erwartetesAbschlussdatum":{"type":["string","null"],"format":"date-time","description":"Erwarteter Abschluss. In der Datenbank ein reines DATUM, das der Treiber zu einem vollen Zeitstempel um Mitternacht UTC macht; `null`, wenn keines gesetzt ist"}},"required":["id","bezeichnung","kundeName","stage","wahrscheinlichkeit","erwarteteEinnahmen","erwartetesAbschlussdatum"],"additionalProperties":false},"description":"Die Chancen dieser Stufe, neueste zuerst"}},"required":["id","count","totalValue","deals"],"additionalProperties":false},"minItems":6,"maxItems":6,"description":"Immer genau sechs Stufen in fester Reihenfolge; eine Stufe mit `count` 0 heisst „keine Chance darin\", nicht „nicht erhoben\". ACHTUNG: es werden hoechstens 200 Chancen insgesamt gelesen — bei mehr sind `count` und `totalValue` zu niedrig, ohne dass die Antwort das anzeigt"}},"required":["stages"],"additionalProperties":false},"example":{"stages":[{"id":"neu","count":0,"totalValue":0,"deals":[{"id":"00000000-0000-4000-8000-000000000000","bezeichnung":"string","kundeName":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01T12:00:00.000Z"}]},{"id":"neu","count":0,"totalValue":0,"deals":[{"id":"00000000-0000-4000-8000-000000000000","bezeichnung":"string","kundeName":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01T12:00:00.000Z"}]},{"id":"neu","count":0,"totalValue":0,"deals":[{"id":"00000000-0000-4000-8000-000000000000","bezeichnung":"string","kundeName":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01T12:00:00.000Z"}]},{"id":"neu","count":0,"totalValue":0,"deals":[{"id":"00000000-0000-4000-8000-000000000000","bezeichnung":"string","kundeName":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01T12:00:00.000Z"}]},{"id":"neu","count":0,"totalValue":0,"deals":[{"id":"00000000-0000-4000-8000-000000000000","bezeichnung":"string","kundeName":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01T12:00:00.000Z"}]},{"id":"neu","count":0,"totalValue":0,"deals":[{"id":"00000000-0000-4000-8000-000000000000","bezeichnung":"string","kundeName":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01T12:00:00.000Z"}]}]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"ZWEI Formen: `Database unavailable`, wenn Datenbank oder Mandantenkontext ganz fehlen — oder `pipeline_query_failed` MIT der Datenbankmeldung, wenn die Abfrage selbst scheiterte.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable","description":"Kein Datenbank-Client ODER kein Mandantenkontext — die Antwort unterscheidet das nicht"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["contacts_query_failed","pipeline_query_failed"],"description":"Welche Abfrage scheiterte"},"message":{"type":"string","description":"Die Meldung der Datenbank im Wortlaut — anders als bei den Schreibfehlern wird sie hier durchgereicht"}},"required":["error","message"],"additionalProperties":false}]}}}}},"operationId":"getApiV1CrmPipeline","tags":["CRM"],"parameters":[],"summary":"Pipeline-Stufen mit Chancen-Übersicht","description":"Liefert den Vertriebstrichter: sechs feste Stufen, je mit Anzahl, Summe und den Chancen selbst. Gelesen wird dieselbe Tabelle wie unter `/api/v1/pipeline` — beide Wege sehen also dieselben Daten. WICHTIG: es werden hoechstens 200 Chancen gelesen, und die Grenze wirkt VOR der Gruppierung. Hat ein Mandant mehr, sind `count` und `totalValue` einzelner Stufen zu niedrig, ohne dass die Antwort das kenntlich macht. Bei einem Fehler kommt bewusst KEIN leerer Trichter mit 200 zurueck."}},"/api/v1/crm/deals":{"post":{"responses":{"201":{"description":"Die angelegte Verkaufschance in Datenbank-Schreibweise","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Die Verkaufschance in Datenbank-Schreibweise, aus der Tabelle `verkaufschancen` — derselben, die `/api/v1/pipeline` bedient. Der Handler reicht `RETURNING *` unveraendert durch, deshalb sagt die Spezifikation die Feldliste nicht zu. `erwartete_einnahmen` ist NUMERIC und kommt als Zeichenkette heraus."}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"ZWEI Formen: `Database unavailable` bei fehlender Datenbank oder fehlendem Mandantenkontext, sonst `Create failed` — der Grund steht dann NUR im Server-Protokoll.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable","description":"Kein Datenbank-Client ODER kein Mandantenkontext — die Antwort unterscheidet das nicht"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["Create failed","Update failed","Delete failed"],"description":"Die Abfrage scheiterte. Der Grund steht NUR im Server-Protokoll, nicht in der Antwort"}},"required":["error"],"additionalProperties":false}]}}}}},"operationId":"postApiV1CrmDeals","tags":["CRM"],"parameters":[],"summary":"Deal / Verkaufschance anlegen","description":"Legt eine Verkaufschance an und gibt die Datenbankzeile OHNE Umschlag zurueck. Geschrieben wird in dieselbe Tabelle, die `/api/v1/pipeline` liest — die Chance ist dort also sofort sichtbar. Ein `contactId` nimmt dieser Aufruf bewusst NICHT an: die Zieltabelle hat keine solche Spalte, und ein stillschweigend verworfener Wert waere ein Schein-Erfolg.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"kundeName":{"type":"string","minLength":1,"maxLength":255},"kundeId":{"type":"string","format":"uuid"},"ansprechpartner":{"type":"string","maxLength":255},"stage":{"type":"string","enum":["neu","qualifiziert","angebot","verhandlung","gewonnen","verloren"],"default":"neu"},"wahrscheinlichkeit":{"type":"integer","minimum":0,"maximum":100,"default":25},"erwarteteEinnahmen":{"type":"number","minimum":0,"default":0},"erwartetesAbschlussdatum":{"type":"string","format":"date"},"notizen":{"type":"string"}},"required":["bezeichnung","kundeName"]},"example":{"bezeichnung":"string","kundeName":"string","kundeId":"00000000-0000-4000-8000-000000000000","ansprechpartner":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01","notizen":"string"}}}}}},"/api/v1/crm/deals/{id}":{"patch":{"responses":{"200":{"description":"Die geaenderte Verkaufschance in Datenbank-Schreibweise","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Die Verkaufschance in Datenbank-Schreibweise, aus der Tabelle `verkaufschancen` — derselben, die `/api/v1/pipeline` bedient. Der Handler reicht `RETURNING *` unveraendert durch, deshalb sagt die Spezifikation die Feldliste nicht zu. `erwartete_einnahmen` ist NUMERIC und kommt als Zeichenkette heraus."}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Keine Verkaufschance mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"ZWEI Formen: `Database unavailable` bei fehlender Datenbank oder fehlendem Mandantenkontext, sonst `Update failed` — der Grund steht dann NUR im Server-Protokoll.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Database unavailable","description":"Kein Datenbank-Client ODER kein Mandantenkontext — die Antwort unterscheidet das nicht"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["Create failed","Update failed","Delete failed"],"description":"Die Abfrage scheiterte. Der Grund steht NUR im Server-Protokoll, nicht in der Antwort"}},"required":["error"],"additionalProperties":false}]}}}}},"operationId":"patchApiV1CrmDealsById","tags":["CRM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Deal bearbeiten (Stufe, Wert, Status)","description":"Aendert einzelne Felder einer Verkaufschance und gibt die Zeile OHNE Umschlag zurueck. Beim Wechsel der Stufe setzt der Server zusaetzlich den Zeitpunkt des Stufenwechsels — daran haengt die Liegezeit-Anzeige der Pipeline-Oberflaeche. Bei `gewonnen` oder `verloren` wird ausserdem der Abschlusszeitpunkt gesetzt. ACHTUNG: `bezeichnung` und `kundeName` werden nur bei WAHRHEITSWERTIGEM Inhalt geschrieben — ein leerer Text laesst das Feld stillschweigend unveraendert, statt ihn zu speichern.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"kundeName":{"type":"string","minLength":1,"maxLength":255},"kundeId":{"type":"string","format":"uuid"},"ansprechpartner":{"type":"string","maxLength":255},"stage":{"type":"string","enum":["neu","qualifiziert","angebot","verhandlung","gewonnen","verloren"],"default":"neu"},"wahrscheinlichkeit":{"type":"integer","minimum":0,"maximum":100,"default":25},"erwarteteEinnahmen":{"type":"number","minimum":0,"default":0},"erwartetesAbschlussdatum":{"type":"string","format":"date"},"notizen":{"type":"string"}}},"example":{"bezeichnung":"string","kundeName":"string","kundeId":"00000000-0000-4000-8000-000000000000","ansprechpartner":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01","notizen":"string"}}}}}},"/api/v1/crm/activities":{"get":{"responses":{"200":{"description":"Die Aktivitaeten dieser Seite samt Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"anyOf":[{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `entity_type`, `entity_id`, `type`, `subject`, `body`, `user_id`, `due_at`, `completed_at`, `created_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu."},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Aktivitaet, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem die Aktivitaet gehoert"},"entityType":{"type":"string","enum":["contact","customer","lead"],"description":"Art des Bezugsobjekts — Kontakt, Kunde oder Interessent"},"entityId":{"type":"string","format":"uuid","description":"Kennung des Bezugsobjekts (UUID)"},"type":{"type":"string","enum":["call","email","meeting","note","task"],"description":"Art der Aktivitaet: Anruf, Mail, Termin, Notiz oder Aufgabe"},"subject":{"type":"string","minLength":1,"maxLength":200,"description":"Betreff — die Zeile, die in der Zeitleiste steht"},"body":{"type":["string","null"],"description":"Ausfuehrlicher Text; `null`, wenn keiner erfasst wurde"},"userId":{"type":["string","null"],"minLength":1,"description":"Kennung des anlegenden Benutzers; `null`, wenn die Anfrage keinen Benutzerkontext trug"},"dueAt":{"type":["string","null"],"format":"date-time","description":"Faelligkeit (ISO 8601); `null` ohne Termin"},"completedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Erledigung (ISO 8601); `null`, solange offen"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"}},"required":["id","tenantId","entityType","entityId","type","subject","body","userId","dueAt","completedAt","createdAt"],"additionalProperties":false}]},"description":"Die Aktivitaeten dieser Seite, absteigend nach Anlagezeitpunkt"},"page":{"type":"integer","minimum":1,"description":"Die zurueckgegebene Seite, gezaehlt ab 1"},"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Groesse der Seite, wie angefragt"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer fuer die gesetzten Filter — echtes COUNT, nicht die Laenge der Seite"}},"required":["items","page","limit","total"],"additionalProperties":false},"example":{"items":[{}],"page":1,"limit":1,"total":0}}}},"400":{"description":"Query-Parameter abgelehnt (rohes Zod-Ergebnis). Ein unzulaessiger Mandanten-Slug antwortet dagegen mit Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"}},"operationId":"getApiV1CrmActivities","tags":["CRM","Activities"],"parameters":[{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":50}},{"in":"query","name":"entityType","schema":{"type":"string"}},{"in":"query","name":"entityId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"type","schema":{"type":"string"}}],"summary":"List CRM activities","description":"Listet die CRM-Zeitleiste (Anrufe, Mails, Termine, Notizen, Aufgaben), wahlweise auf ein Bezugsobjekt oder eine Art eingeschraenkt. ACHTUNG: schlaegt die Datenbankabfrage fehl, wird das nur protokolliert; die Antwort ist dann trotzdem 200 — beantwortet aus einem Speicher IM PROZESS, der nur enthaelt, was dieselbe Instanz seit ihrem Start selbst angelegt hat. Ein leeres Ergebnis heisst hier also nicht sicher „keine Aktivitaeten\"."},"post":{"responses":{"201":{"description":"Die angelegte Aktivitaet — Form identisch, ob dauerhaft gespeichert oder nur im Speicher","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Aktivitaet, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandanten-Slug, zu dem die Aktivitaet gehoert"},"entityType":{"type":"string","enum":["contact","customer","lead"],"description":"Art des Bezugsobjekts — Kontakt, Kunde oder Interessent"},"entityId":{"type":"string","format":"uuid","description":"Kennung des Bezugsobjekts (UUID)"},"type":{"type":"string","enum":["call","email","meeting","note","task"],"description":"Art der Aktivitaet: Anruf, Mail, Termin, Notiz oder Aufgabe"},"subject":{"type":"string","minLength":1,"maxLength":200,"description":"Betreff — die Zeile, die in der Zeitleiste steht"},"body":{"type":["string","null"],"description":"Ausfuehrlicher Text; `null`, wenn keiner erfasst wurde"},"userId":{"type":["string","null"],"minLength":1,"description":"Kennung des anlegenden Benutzers; `null`, wenn die Anfrage keinen Benutzerkontext trug"},"dueAt":{"type":["string","null"],"format":"date-time","description":"Faelligkeit (ISO 8601); `null` ohne Termin"},"completedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Erledigung (ISO 8601); `null`, solange offen"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"}},"required":["id","tenantId","entityType","entityId","type","subject","body","userId","dueAt","completedAt","createdAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","entityType":"contact","entityId":"00000000-0000-4000-8000-000000000000","type":"call","subject":"string","body":"string","userId":"string","dueAt":"2026-01-01T12:00:00.000Z","completedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis). Ein unzulaessiger Mandanten-Slug antwortet dagegen mit Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"}},"operationId":"postApiV1CrmActivities","tags":["CRM","Activities"],"parameters":[],"summary":"Create CRM activity","description":"Legt einen Eintrag in der CRM-Zeitleiste an. ACHTUNG: schlaegt der INSERT fehl oder fehlt die Datenbank, antwortet die Route TROTZDEM mit 201 — der Eintrag liegt dann nur im Prozessspeicher dieser Instanz und ist beim naechsten Neustart verloren. Die 201 ist hier also keine Zusage, dass gespeichert wurde. `entityId` wird nicht gegen die Zieltabelle geprueft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entityType":{"type":"string","enum":["contact","customer","lead"]},"entityId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["call","email","meeting","note","task"]},"subject":{"type":"string","minLength":1,"maxLength":200},"body":{"type":"string"},"dueAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"}},"required":["entityType","entityId","type","subject"]},"example":{"entityType":"contact","entityId":"00000000-0000-4000-8000-000000000000","type":"call","subject":"string","body":"string","dueAt":"2026-01-01T12:00:00.000Z","completedAt":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/crm/activities/{id}/complete":{"patch":{"responses":{"200":{"description":"Quittung mit dem gesetzten Zeitpunkt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`"},"completedAt":{"type":"string","format":"date-time","description":"Der gesetzte Erledigt-Zeitpunkt (ISO 8601, UTC) — vom Server erzeugt, nicht aus der Anfrage"}},"required":["ok","completedAt"],"additionalProperties":false},"example":{"ok":true,"completedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"}},"operationId":"patchApiV1CrmActivitiesByIdComplete","tags":["CRM","Activities"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mark activity complete","description":"Setzt den Erledigt-Zeitpunkt auf JETZT. Der Aufruf antwortet immer mit 200 — auch wenn es die Kennung gar nicht gibt (das UPDATE trifft dann null Zeilen) und auch, wenn die Datenbank nicht erreichbar ist. Ein 200 belegt hier also nicht, dass etwas geaendert wurde. Der Zeitpunkt laesst sich nicht mitgeben und nicht zuruecknehmen."}},"/api/v1/crm/activities/{id}":{"delete":{"responses":{"200":{"description":"Quittung ohne Nutzlast","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"}},"operationId":"deleteApiV1CrmActivitiesById","tags":["CRM","Activities"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete activity","description":"Entfernt den Zeitleisten-Eintrag endgueltig (kein Soft-Delete). Der Aufruf antwortet immer mit 200 — auch bei unbekannter Kennung und auch, wenn die Datenbank nicht erreichbar ist. Ein 200 belegt hier also nicht, dass etwas entfernt wurde."}},"/api/v1/tasks":{"get":{"responses":{"200":{"description":"Aufgabenliste des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"assigneeUserId":{"type":["string","null"]},"createdByUserId":{"type":"string"},"status":{"type":"string","enum":["offen","in_arbeit","erledigt","abgebrochen"]},"priority":{"type":"string","enum":["niedrig","normal","hoch","dringend"]},"dueDate":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"parentEntityType":{"type":["string","null"]},"parentEntityId":{"type":["string","null"]},"parentTaskId":{"type":["string","null"]},"sectionId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{},"assigneeName":{"type":["string","null"]},"assigneeFirstName":{"type":["string","null"]},"assigneeLastName":{"type":["string","null"]}},"required":["id","tenantId","title","description","assigneeUserId","createdByUserId","status","priority","dueDate","completedAt","parentEntityType","parentEntityId","parentTaskId","sectionId","projectId","customFields","assigneeName","assigneeFirstName","assigneeLastName"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","tenantId":"string","title":"string","description":"string","assigneeUserId":"string","createdByUserId":"string","status":"offen","priority":"niedrig","dueDate":"string","completedAt":"string","parentEntityType":"string","parentEntityId":"string","parentTaskId":"string","sectionId":"string","projectId":"string","customFields":{},"assigneeName":"string","assigneeFirstName":"string","assigneeLastName":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Tasks","tags":["Tasks"],"parameters":[{"in":"query","name":"assignee_id","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["offen","in_arbeit","erledigt","abgebrochen"]}},{"in":"query","name":"priority","schema":{"type":"string","enum":["niedrig","normal","hoch","dringend"]}},{"in":"query","name":"parent_entity_type","schema":{"type":"string"}},{"in":"query","name":"parent_entity_id","schema":{"type":"string"}},{"in":"query","name":"due_before","schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}]}},{"in":"query","name":"due_after","schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":500,"default":200}}],"summary":"List tasks","description":"Liest die Tabelle `tasks` des Mandanten; weich geloeschte Zeilen (`deleted_at`) bleiben aussen vor. Filterbar ueber assignee_id, status, priority, parent_entity_type/-id sowie due_before/due_after; `limit` begrenzt die Menge auf 1–500 (Vorgabe 200), eine Blaetterung ueber offset gibt es nicht. Sortiert nach Status, Prioritaet, Faelligkeit und Anlagedatum; zu jeder Aufgabe wird der Name der zustaendigen Person nachgeladen."},"post":{"responses":{"201":{"description":"Die angelegte Aufgabe","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"assigneeUserId":{"type":["string","null"]},"createdByUserId":{"type":"string"},"status":{"type":"string","enum":["offen","in_arbeit","erledigt","abgebrochen"]},"priority":{"type":"string","enum":["niedrig","normal","hoch","dringend"]},"dueDate":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"parentEntityType":{"type":["string","null"]},"parentEntityId":{"type":["string","null"]},"parentTaskId":{"type":["string","null"]},"sectionId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","title","description","assigneeUserId","createdByUserId","status","priority","dueDate","completedAt","parentEntityType","parentEntityId","parentTaskId","sectionId","projectId","customFields"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","title":"string","description":"string","assigneeUserId":"string","createdByUserId":"string","status":"offen","priority":"niedrig","dueDate":"string","completedAt":"string","parentEntityType":"string","parentEntityId":"string","parentTaskId":"string","sectionId":"string","projectId":"string","customFields":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Tasks","tags":["Tasks"],"parameters":[],"summary":"Create task","description":"Legt eine Zeile in `tasks` an. Ohne Angabe startet sie mit Status `offen` und Prioritaet `normal`; Ersteller ist der angemeldete Nutzer. Optional laesst sie sich an eine andere Entitaet (parent_entity_type/-id), eine Eltern-Aufgabe, einen Abschnitt oder ein Projekt binden. Schreibt einen Audit-Eintrag `task.create`. Scheitert der Schreibvorgang, antwortet die Route 503 statt einen Erfolg zu melden, den die Datenbank nicht kennt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":500},"description":{"type":"string","maxLength":10000},"assignee_user_id":{"type":"string","minLength":1},"priority":{"type":"string","enum":["niedrig","normal","hoch","dringend"]},"due_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}]},"parent_entity_type":{"type":"string","maxLength":64},"parent_entity_id":{"type":"string","maxLength":128},"parent_task_id":{"type":"string","format":"uuid"},"section_id":{"type":"string","format":"uuid"},"project_id":{"type":"string","format":"uuid"}},"required":["title"]},"example":{"title":"string","description":"string","assignee_user_id":"string","priority":"niedrig","due_date":"2026-01-01T12:00:00.000Z","parent_entity_type":"string","parent_entity_id":"string","parent_task_id":"00000000-0000-4000-8000-000000000000","section_id":"00000000-0000-4000-8000-000000000000","project_id":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/tasks/my":{"get":{"responses":{"200":{"description":"Aufgaben des angemeldeten Nutzers","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"assigneeUserId":{"type":["string","null"]},"createdByUserId":{"type":"string"},"status":{"type":"string","enum":["offen","in_arbeit","erledigt","abgebrochen"]},"priority":{"type":"string","enum":["niedrig","normal","hoch","dringend"]},"dueDate":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"parentEntityType":{"type":["string","null"]},"parentEntityId":{"type":["string","null"]},"parentTaskId":{"type":["string","null"]},"sectionId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{},"assigneeName":{"type":["string","null"]},"assigneeFirstName":{"type":["string","null"]},"assigneeLastName":{"type":["string","null"]}},"required":["id","tenantId","title","description","assigneeUserId","createdByUserId","status","priority","dueDate","completedAt","parentEntityType","parentEntityId","parentTaskId","sectionId","projectId","customFields","assigneeName","assigneeFirstName","assigneeLastName"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","tenantId":"string","title":"string","description":"string","assigneeUserId":"string","createdByUserId":"string","status":"offen","priority":"niedrig","dueDate":"string","completedAt":"string","parentEntityType":"string","parentEntityId":"string","parentTaskId":"string","sectionId":"string","projectId":"string","customFields":{},"assigneeName":"string","assigneeFirstName":"string","assigneeLastName":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TasksMy","tags":["Tasks"],"parameters":[],"summary":"My tasks","description":"Dieselbe Tabelle wie die Aufgabenliste, aber fest auf `assignee_user_id` = angemeldeter Nutzer eingeschraenkt und ohne Filterparameter. Geloeschte Aufgaben bleiben aussen vor, die Menge ist fest auf 200 Zeilen begrenzt; sortiert nach offen/in Arbeit, dann Faelligkeit und Anlagedatum."}},"/api/v1/tasks/my/widget":{"get":{"responses":{"200":{"description":"Kachelform fuer das Dashboard — NICHT die Aufgabenform","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"href":{"type":"string"},"meta":{"type":"string"}},"required":["id","title","href","meta"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","title":"string","subtitle":"string","href":"string","meta":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TasksMyWidget","tags":["Tasks"],"parameters":[],"summary":"My tasks (dashboard widget shape)","description":"Liefert nur die offenen und in Arbeit befindlichen Aufgaben des angemeldeten Nutzers, und zwar in der Kachelform `{ id, title, subtitle, href, meta }` — nicht in der Aufgabenform der uebrigen Routen. `limit` begrenzt auf 1–20 Eintraege (Vorgabe 8). Untertitel und Meta sind fertig formatierte deutsche Texte aus Faelligkeit (mit Hinweis „ueberfaellig\"), Prioritaet und Status."}},"/api/v1/tasks/stats":{"get":{"responses":{"200":{"description":"Zaehler fuer das Aufgaben-Abzeichen. `degraded: true` heisst: die Zahlen stammen NICHT aus der Datenbank — eine Null ist dann kein „alles erledigt\".","content":{"application/json":{"schema":{"type":"object","properties":{"openCount":{"type":"number"},"inProgressCount":{"type":"number"},"overdueCount":{"type":"number"},"degraded":{"type":"boolean","const":true}},"required":["openCount","inProgressCount","overdueCount"],"additionalProperties":false},"example":{"openCount":0,"inProgressCount":0,"overdueCount":0,"degraded":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TasksStats","tags":["Tasks"],"parameters":[],"summary":"Task stats for current user","description":"Zaehlt in einer Abfrage die offenen, die in Arbeit befindlichen und die ueberfaelligen Aufgaben, die dem angemeldeten Nutzer zugewiesen sind. Ueberfaellig heisst: Faelligkeit liegt in der Vergangenheit und der Status ist `offen` oder `in_arbeit`. Weich geloeschte Aufgaben zaehlen nicht mit."}},"/api/v1/tasks/{id}":{"patch":{"responses":{"200":{"description":"Die geaenderte Aufgabe — oder die Leerlauf-Quittung","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"assigneeUserId":{"type":["string","null"]},"createdByUserId":{"type":"string"},"status":{"type":"string","enum":["offen","in_arbeit","erledigt","abgebrochen"]},"priority":{"type":"string","enum":["niedrig","normal","hoch","dringend"]},"dueDate":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"parentEntityType":{"type":["string","null"]},"parentEntityId":{"type":["string","null"]},"parentTaskId":{"type":["string","null"]},"sectionId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","title","description","assigneeUserId","createdByUserId","status","priority","dueDate","completedAt","parentEntityType","parentEntityId","parentTaskId","sectionId","projectId","customFields"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}]},"example":{"id":"string","tenantId":"string","title":"string","description":"string","assigneeUserId":"string","createdByUserId":"string","status":"offen","priority":"niedrig","dueDate":"string","completedAt":"string","parentEntityType":"string","parentEntityId":"string","parentTaskId":"string","sectionId":"string","projectId":"string","customFields":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"patchApiV1TasksById","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update task","description":"Aendert einzelne Felder; nicht mitgeschickte bleiben unberuehrt. Ein Wechsel auf `erledigt` setzt `completed_at`, jeder andere Status loescht den Zeitpunkt wieder. `custom_fields` wird in das bestehende JSONB gemerged — ein `null` entfernt genau diesen Schluessel, die uebrigen bleiben stehen. Verletzt der Patch eine hinterlegte Entity-Regel, antwortet die Route 422; enthaelt der Rumpf kein aenderbares Feld, `{ ok: true, noop: true }`. Unbekannte oder geloeschte Aufgabe: 404. Schreibt einen Audit-Eintrag `task.update`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":500},"description":{"type":["string","null"],"maxLength":10000},"assignee_user_id":{"type":["string","null"]},"status":{"type":"string","enum":["offen","in_arbeit","erledigt","abgebrochen"]},"priority":{"type":"string","enum":["niedrig","normal","hoch","dringend"]},"due_date":{"type":["string","null"],"format":"date-time"},"section_id":{"type":["string","null"],"format":"uuid"},"project_id":{"type":["string","null"],"format":"uuid"},"custom_fields":{"type":"object","additionalProperties":{}}}},"example":{"title":"string","description":"string","assignee_user_id":"string","status":"offen","priority":"niedrig","due_date":"2026-01-01T12:00:00.000Z","section_id":"00000000-0000-4000-8000-000000000000","project_id":"00000000-0000-4000-8000-000000000000","custom_fields":{}}}}}},"delete":{"responses":{"200":{"description":"Quittung des Soft-Deletes","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true,"noop":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1TasksById","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete task","description":"Loescht weich: die Zeile bleibt erhalten und bekommt nur `deleted_at` gesetzt, zurueckholen laesst sie sich ueber `POST /tasks/{id}/restore`. Ein zweiter Loeschversuch trifft nichts mehr und antwortet 404 — der Zeitpunkt der ersten Loeschung bleibt damit erhalten. Schreibt einen Audit-Eintrag `task.delete`."},"get":{"responses":{"200":{"description":"Eine Aufgabe — als einzige Aufgaben-Route im data-Umschlag","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"assigneeUserId":{"type":["string","null"]},"createdByUserId":{"type":"string"},"status":{"type":"string","enum":["offen","in_arbeit","erledigt","abgebrochen"]},"priority":{"type":"string","enum":["niedrig","normal","hoch","dringend"]},"dueDate":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"parentEntityType":{"type":["string","null"]},"parentEntityId":{"type":["string","null"]},"parentTaskId":{"type":["string","null"]},"sectionId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","title","description","assigneeUserId","createdByUserId","status","priority","dueDate","completedAt","parentEntityType","parentEntityId","parentTaskId","sectionId","projectId","customFields"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"id":"string","tenantId":"string","title":"string","description":"string","assigneeUserId":"string","createdByUserId":"string","status":"offen","priority":"niedrig","dueDate":"string","completedAt":"string","parentEntityType":"string","parentEntityId":"string","parentTaskId":"string","sectionId":"string","projectId":"string","customFields":{}}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TasksById","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get a single task","description":"Liest genau eine Aufgabe des Mandanten. Weich geloeschte Aufgaben sind hier nicht auffindbar, und eine ID ohne UUID-Form ergibt 404 statt eines Serverfehlers. Als einzige Aufgaben-Route antwortet sie im `data`-Umschlag statt mit dem Objekt selbst."}},"/api/v1/tasks/{id}/restore":{"post":{"responses":{"200":{"description":"Die wiederhergestellte Aufgabe","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"assigneeUserId":{"type":["string","null"]},"createdByUserId":{"type":"string"},"status":{"type":"string","enum":["offen","in_arbeit","erledigt","abgebrochen"]},"priority":{"type":"string","enum":["niedrig","normal","hoch","dringend"]},"dueDate":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"parentEntityType":{"type":["string","null"]},"parentEntityId":{"type":["string","null"]},"parentTaskId":{"type":["string","null"]},"sectionId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","title","description","assigneeUserId","createdByUserId","status","priority","dueDate","completedAt","parentEntityType","parentEntityId","parentTaskId","sectionId","projectId","customFields"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","title":"string","description":"string","assigneeUserId":"string","createdByUserId":"string","status":"offen","priority":"niedrig","dueDate":"string","completedAt":"string","parentEntityType":"string","parentEntityId":"string","parentTaskId":"string","sectionId":"string","projectId":"string","customFields":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1TasksByIdRestore","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Restore deleted task","description":"Setzt `deleted_at` wieder auf NULL und macht die Aufgabe damit in allen Listen sichtbar. Die Abfrage trifft ausschliesslich Zeilen, die tatsaechlich geloescht sind — fuer eine nie geloeschte oder unbekannte Aufgabe kommt 404. Schreibt einen Audit-Eintrag `task.restore`."}},"/api/v1/tasks/bulk-assign":{"post":{"responses":{"200":{"description":"Wie viele zugewiesen wurden und welche fehlten","content":{"application/json":{"schema":{"type":"object","properties":{"assigned":{"type":"integer"},"assignedIds":{"type":"array","items":{"type":"string"}},"notFound":{"type":"array","items":{"type":"string"}}},"required":["assigned","assignedIds","notFound"],"additionalProperties":false},"example":{"assigned":0,"assignedIds":["string"],"notFound":["string"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1TasksBulk-assign","tags":["Tasks"],"parameters":[],"summary":"Assign many tasks to one user","description":"Setzt `assignee_user_id` fuer mehrere Aufgaben in einem Durchgang. Doppelte IDs werden vorher zusammengefasst, IDs ohne UUID-Form gar nicht erst abgefragt. Die Antwort nennt die tatsaechlich getroffenen IDs und unter `notFound` alle uebrigen — eine fremde, unbekannte oder geloeschte Aufgabe wird nie zugewiesen und nie als Erfolg gemeldet. Je zugewiesener Aufgabe entsteht ein Audit-Eintrag `task.update`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"taskIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"assigneeId":{"type":"string","minLength":1}},"required":["taskIds","assigneeId"]},"example":{"taskIds":["string"],"assigneeId":"string"}}}}}},"/api/v1/tasks/{id}/comments":{"get":{"responses":{"200":{"description":"Kommentare der Aufgabe, mit Autorenname","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"taskId":{"type":"string"},"authorUserId":{"type":"string"},"body":{"type":"string"},"createdAt":{},"updatedAt":{},"authorName":{"type":["string","null"]}},"required":["id","taskId","authorUserId","body"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","taskId":"string","authorUserId":"string","body":"string","authorName":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TasksByIdComments","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List task comments","description":"Liest `task_comments` zur Aufgabe, chronologisch aufsteigend, und ergaenzt je Eintrag den Namen des Autors zur reinen Nutzer-ID. Eine Aufgabe ohne Kommentare ergibt eine leere Liste, kein 404."},"post":{"responses":{"201":{"description":"Der angelegte Kommentar","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"taskId":{"type":"string"},"authorUserId":{"type":"string"},"body":{"type":"string"},"createdAt":{},"updatedAt":{},"authorName":{"type":["string","null"]}},"required":["id","taskId","authorUserId","body"],"additionalProperties":false},"example":{"id":"string","taskId":"string","authorUserId":"string","body":"string","authorName":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1TasksByIdComments","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Add task comment","description":"Legt eine Zeile in `task_comments` an. Vorher wird geprueft, ob die Aufgabe im Mandanten ueberhaupt existiert — sonst 404, es entsteht kein Kommentar ins Leere. Autor ist der angemeldete Nutzer; die Antwort traegt den Autorennamen bereits mit, damit die Oberflaeche sie direkt anhaengen kann. Schreibt einen Audit-Eintrag `task.comment.create`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":10000}},"required":["body"]},"example":{"body":"string"}}}}}},"/api/v1/tasks/{id}/comments/{commentId}":{"delete":{"responses":{"200":{"description":"Quittung der Loeschung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true,"noop":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1TasksByIdCommentsByCommentId","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"commentId","required":true}],"summary":"Delete task comment","description":"Entfernt den Kommentar endgueltig aus `task_comments` — es gibt keinen Soft-Delete und kein Zurueckholen. Nur der Autor selbst darf loeschen; ein fremder oder unbekannter Kommentar ergibt gleichermassen 404 (`comment_not_found_or_forbidden`). Schreibt einen Audit-Eintrag `task.comment.delete`."}},"/api/v1/tasks/{id}/subtasks":{"get":{"responses":{"200":{"description":"Kind-Aufgaben und Fortschritt","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"assigneeUserId":{"type":["string","null"]},"createdByUserId":{"type":"string"},"status":{"type":"string","enum":["offen","in_arbeit","erledigt","abgebrochen"]},"priority":{"type":"string","enum":["niedrig","normal","hoch","dringend"]},"dueDate":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"parentEntityType":{"type":["string","null"]},"parentEntityId":{"type":["string","null"]},"parentTaskId":{"type":["string","null"]},"sectionId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","title","description","assigneeUserId","createdByUserId","status","priority","dueDate","completedAt","parentEntityType","parentEntityId","parentTaskId","sectionId","projectId","customFields"],"additionalProperties":false}},"stats":{"type":"object","properties":{"total":{"type":"integer"},"done":{"type":"integer"}},"required":["total","done"],"additionalProperties":false}},"required":["items","stats"],"additionalProperties":false},"example":{"items":[{"id":"string","tenantId":"string","title":"string","description":"string","assigneeUserId":"string","createdByUserId":"string","status":"offen","priority":"niedrig","dueDate":"string","completedAt":"string","parentEntityType":"string","parentEntityId":"string","parentTaskId":"string","sectionId":"string","projectId":"string","customFields":{}}],"stats":{"total":0,"done":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TasksByIdSubtasks","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List sub-tasks of a task","description":"Liest die Aufgaben, deren `parent_task_id` auf diese Aufgabe zeigt (geloeschte ausgenommen), sortiert nach Status und Anlagedatum. Zusaetzlich zur Liste kommt `stats` mit der Gesamtzahl und der Anzahl der bereits erledigten Kind-Aufgaben — daraus baut die Oberflaeche den Fortschritt."}},"/api/v1/tasks/{id}/attachments":{"get":{"responses":{"200":{"description":"Anhaenge mit praesignierter Download-Adresse","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"taskId":{"type":"string"},"filename":{"type":"string"},"contentType":{"type":"string"},"sizeBytes":{"type":"number"},"uploadedByUserId":{"type":"string"},"createdAt":{},"downloadUrl":{"type":"string"}},"required":["id","taskId","filename","contentType","sizeBytes","uploadedByUserId"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","taskId":"string","filename":"string","contentType":"string","sizeBytes":0,"uploadedByUserId":"string","downloadUrl":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TasksByIdAttachments","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List task attachments","description":"Liest die Metadaten aus `task_attachments` (aeltester zuerst) und haengt je Anhang eine 15 Minuten gueltige, praesignierte Download-Adresse an. Ist der Dateispeicher nicht erreichbar oder fehlt der Speicher-Schluessel, kommen die Metadaten trotzdem — dann ohne `downloadUrl`."},"post":{"responses":{"201":{"description":"Der angelegte Anhang","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"taskId":{"type":"string"},"filename":{"type":"string"},"contentType":{"type":"string"},"sizeBytes":{"type":"number"},"uploadedByUserId":{"type":"string"},"createdAt":{},"downloadUrl":{"type":"string"}},"required":["id","taskId","filename","contentType","sizeBytes","uploadedByUserId"],"additionalProperties":false},"example":{"id":"string","taskId":"string","filename":"string","contentType":"string","sizeBytes":0,"uploadedByUserId":"string","downloadUrl":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1TasksByIdAttachments","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Attach a file to a task","description":"Nimmt eine Datei als multipart/form-data im Feld `file` entgegen, legt sie im Dateispeicher unter `tasks/<taskId>/<dateiname>` ab und schreibt die Metadaten nach `task_attachments`. Groesser als 10 MB wird mit 413 abgelehnt; erlaubt sind PDF, gaengige Bild-, Office- und Textformate sowie ZIP, alles andere endet in 400. Unbekannte Aufgabe: 404, fehlendes Multipart oder fehlende Datei: 400. Schreibt einen Audit-Eintrag `task.attachment.create`."}},"/api/v1/tasks/{id}/attachments/{attachmentId}":{"delete":{"responses":{"200":{"description":"Quittung der Loeschung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true,"noop":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1TasksByIdAttachmentsByAttachmentId","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"attachmentId","required":true}],"summary":"Delete a task attachment","description":"Entfernt die Zeile endgueltig aus `task_attachments` — nur die hochladende Person selbst darf das, sonst 404 (`attachment_not_found_or_forbidden`). Anschliessend wird das Objekt im Dateispeicher nach Moeglichkeit mitgeloescht; scheitert das, bleibt die Datei als Waise liegen, ohne den Aufruf zu stoeren. Schreibt einen Audit-Eintrag `task.attachment.delete`."}},"/api/v1/tasks/sections":{"get":{"responses":{"200":{"description":"Abschnitte des Aufgabenboards","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sortOrder":{"type":"number"},"createdAt":{}},"required":["id","name","sortOrder"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","name":"string","sortOrder":0}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TasksSections","tags":["Tasks"],"parameters":[],"summary":"List task sections","description":"Liest `task_sections` des Mandanten, sortiert nach `sort_order`, bei Gleichstand nach Anlagedatum. Abschnitte sind die frei benennbaren Spalten des Aufgabenboards; eine Aufgabe verweist ueber `section_id` auf ihren Abschnitt."},"post":{"responses":{"201":{"description":"Der angelegte Abschnitt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sortOrder":{"type":"number"},"createdAt":{}},"required":["id","name","sortOrder"],"additionalProperties":false},"example":{"id":"string","name":"string","sortOrder":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1TasksSections","tags":["Tasks"],"parameters":[],"summary":"Create task section","description":"Legt eine Zeile in `task_sections` an. Ohne `sort_order` steht der Abschnitt mit 0 ganz vorn; der Name wird getrimmt und darf hoechstens 200 Zeichen lang sein. Schreibt einen Audit-Eintrag `task.section.create`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"sort_order":{"type":"integer"}},"required":["name"]},"example":{"name":"string","sort_order":0}}}}}},"/api/v1/tasks/sections/{sectionId}":{"patch":{"responses":{"200":{"description":"Der geaenderte Abschnitt — oder die Leerlauf-Quittung","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sortOrder":{"type":"number"},"createdAt":{}},"required":["id","name","sortOrder"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}]},"example":{"id":"string","name":"string","sortOrder":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"patchApiV1TasksSectionsBySectionId","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"sectionId","required":true}],"summary":"Update task section","description":"Aendert Name und/oder Sortierung eines Abschnitts; nicht mitgeschickte Felder bleiben unberuehrt. Enthaelt der Rumpf kein aenderbares Feld, antwortet die Route `{ ok: true, noop: true }` und fasst nichts an; ein unbekannter Abschnitt ergibt 404. Schreibt einen Audit-Eintrag `task.section.update`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"sort_order":{"type":"integer"}}},"example":{"name":"string","sort_order":0}}}}},"delete":{"responses":{"200":{"description":"Quittung der Loeschung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true,"noop":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1TasksSectionsBySectionId","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"sectionId","required":true}],"summary":"Delete task section","description":"Loescht den Abschnitt endgueltig aus `task_sections`. Zuvor wird `section_id` aller zugeordneten Aufgaben auf NULL gesetzt — die Aufgaben selbst bleiben erhalten und sichtbar, sie stehen danach nur in keiner Spalte mehr. Schreibt einen Audit-Eintrag `task.section.delete`."}},"/api/v1/tasks/{id}/assignees":{"get":{"responses":{"200":{"description":"Zusaetzliche Zustaendige der Aufgabe","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"taskId":{"type":"string"},"userId":{"type":"string"},"createdAt":{}},"required":["id","taskId","userId"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","taskId":"string","userId":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TasksByIdAssignees","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List task co-assignees","description":"Liest `task_assignees` — die zusaetzlich zustaendigen Personen NEBEN der Hauptzuweisung `assigneeUserId` der Aufgabe selbst. Chronologisch aufsteigend; die Eintraege tragen nur die Nutzer-ID, keinen Namen."},"post":{"responses":{"200":{"description":"Die Zustaendigkeit bestand bereits — idempotent, gleicher Koerper","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"taskId":{"type":"string"},"userId":{"type":"string"},"createdAt":{}},"required":["id","taskId","userId"],"additionalProperties":false},"example":{"id":"string","taskId":"string","userId":"string"}}}},"201":{"description":"Die neu angelegte Zustaendigkeit","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"taskId":{"type":"string"},"userId":{"type":"string"},"createdAt":{}},"required":["id","taskId","userId"],"additionalProperties":false},"example":{"id":"string","taskId":"string","userId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1TasksByIdAssignees","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Add a co-assignee to a task","description":"Traegt eine weitere zustaendige Person in `task_assignees` ein. Der Aufruf ist idempotent: bestand die Zuordnung bereits, kommt 200 mit der vorhandenen Zeile statt 201, und es entsteht kein zweiter Eintrag. Existiert die Aufgabe im Mandanten nicht, antwortet die Route 404. Nur ein wirklich neuer Eintrag erzeugt einen Audit-Eintrag `task.assignee.add`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"maxLength":128}},"required":["user_id"]},"example":{"user_id":"string"}}}}}},"/api/v1/tasks/{id}/assignees/{userId}":{"delete":{"responses":{"200":{"description":"Quittung der Loeschung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true,"noop":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1TasksByIdAssigneesByUserId","tags":["Tasks"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Remove a co-assignee from a task","description":"Entfernt die zusaetzliche Zustaendigkeit endgueltig aus `task_assignees`. Die Hauptzuweisung der Aufgabe bleibt unberuehrt. Der Aufruf quittiert auch dann mit `ok`, wenn es die Zuordnung gar nicht gab. Schreibt einen Audit-Eintrag `task.assignee.remove`."}},"/api/v1/email-templates":{"get":{"responses":{"200":{"description":"Vorlagen des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"docType":{"type":"string","enum":["quote","order","delivery","invoice","abschlag","credit_note","dunning","recurring"]},"subject":{"type":"string"},"body":{"type":"string"},"isDefault":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","name","docType","subject","body","isDefault","createdAt","updatedAt"]}}},"required":["items"]},"example":{"items":[{"id":"string","name":"string","docType":"quote","subject":"string","body":"string","isDefault":true,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Email-templates","tags":["EmailTemplates"],"parameters":[{"in":"query","name":"docType","schema":{"type":"string","enum":["quote","order","delivery","invoice","abschlag","credit_note","dunning","recurring"]}}],"summary":"List email text templates","description":"Gibt alle Vorlagen des Mandanten in EINER Antwort zurueck — ohne Blaetterung —, sortiert nach Belegart, dann Standardvorlage zuerst, dann Name. Mit `docType` laesst sich auf eine Belegart einschraenken. Der Umschlag heiszt `items`, nicht `data`. Platzhalter wie {{doc_number}} bleiben unersetzt; sie werden erst beim Auswaehlen im Sende-Dialog eingesetzt. Ist die Datenbank nicht erreichbar oder scheitert die Abfrage, antwortet der Endpunkt 200 aus einem fluechtigen Zwischenspeicher — eine leere Liste heiszt dann nicht „keine Vorlagen vorhanden\"."},"post":{"responses":{"201":{"description":"Vorlage angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"docType":{"type":"string","enum":["quote","order","delivery","invoice","abschlag","credit_note","dunning","recurring"]},"subject":{"type":"string"},"body":{"type":"string"},"isDefault":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","name","docType","subject","body","isDefault","createdAt","updatedAt"]},"example":{"id":"string","name":"string","docType":"quote","subject":"string","body":"string","isDefault":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Email-templates","tags":["EmailTemplates"],"parameters":[],"summary":"Create email text template","description":"Legt eine benannte Vorlage fuer eine Belegart an. Pflicht sind `name` und `doc_type`; Betreff und Text fallen auf einen leeren Wert zurueck, `is_default` auf false. Die Kennung vergibt der Server. Namen sind NICHT eindeutig — dieselbe Bezeichnung laesst sich mehrfach anlegen. Mit `is_default: true` verliert die bisherige Standardvorlage derselben Belegart ihr Kennzeichen; andere Belegarten bleiben unberuehrt. Der Vorgang wird best-effort im Audit-Log vermerkt. Ist die Datenbank nicht erreichbar, landet die Vorlage nur in einem fluechtigen Zwischenspeicher und ist nach einem Neustart weg — die Antwort bleibt trotzdem 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"doc_type":{"type":"string","enum":["quote","order","delivery","invoice","abschlag","credit_note","dunning","recurring"]},"subject":{"type":"string","maxLength":500,"default":""},"body":{"type":"string","maxLength":20000,"default":""},"is_default":{"type":"boolean","default":false}},"required":["name","doc_type"]},"example":{"name":"string","doc_type":"quote","subject":"string","body":"string","is_default":true}}}}}},"/api/v1/email-templates/{id}":{"put":{"responses":{"200":{"description":"Vorlage nach der Aenderung — oder noop bei leerem Rumpf","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"docType":{"type":"string","enum":["quote","order","delivery","invoice","abschlag","credit_note","dunning","recurring"]},"subject":{"type":"string"},"body":{"type":"string"},"isDefault":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","name","docType","subject","body","isDefault","createdAt","updatedAt"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"noop":{"type":"boolean","const":true}},"required":["ok","noop"]}]},"example":{"id":"string","name":"string","docType":"quote","subject":"string","body":"string","isDefault":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Vorlage nicht gefunden"}},"operationId":"putApiV1Email-templatesById","tags":["EmailTemplates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update email text template","description":"Teil-Update trotz PUT: geschrieben werden nur die gesendeten Felder, die uebrigen bleiben stehen. Die Belegart laesst sich dabei umhaengen. Mit `is_default: true` verliert die bisherige Standardvorlage der NEUEN Belegart ihr Kennzeichen. Ein leerer Rumpf aendert nichts und antwortet 200 mit noop=true — ohne zu pruefen, ob es die Vorlage ueberhaupt gibt; sonst ergibt eine unbekannte Kennung 404. Der Vorgang wird best-effort im Audit-Log vermerkt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"doc_type":{"type":"string","enum":["quote","order","delivery","invoice","abschlag","credit_note","dunning","recurring"]},"subject":{"type":"string","maxLength":500},"body":{"type":"string","maxLength":20000},"is_default":{"type":"boolean"}}},"example":{"name":"string","doc_type":"quote","subject":"string","body":"string","is_default":true}}}}},"delete":{"responses":{"200":{"description":"Aufruf angenommen — es kann auch nichts getroffen worden sein","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1Email-templatesById","tags":["EmailTemplates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete email text template","description":"Entfernt die Vorlage ENDGUELTIG — kein Soft-Delete, kein Rueckgaengig. Bereits verschickte Mails sind nicht betroffen, sie haengen nicht an der Vorlage. War es die Standardvorlage ihrer Belegart, rueckt KEINE andere nach: die Belegart hat danach keine Standardvorlage mehr. Der Aufruf trifft nur Vorlagen des eigenen Mandanten und antwortet auch dann 200, wenn nichts geloescht wurde — die Antwort beweist also keine Loeschung. Der Vorgang wird best-effort im Audit-Log vermerkt."}},"/api/v1/crm/email-sync/config":{"get":{"responses":{"200":{"description":"Die hinterlegten Verbindungen. Ohne Datenbank die Speicher-Form.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"provider":{"type":"string","description":"imap | gmail | outlook"},"email":{"type":"string"},"imap_host":{"type":["string","null"]},"imap_port":{"type":["integer","null"]},"filter_customer_only":{"type":"boolean"},"last_sync_at":{"type":["string","null"],"description":"ISO-Zeichenkette; null, solange nie abgerufen wurde"},"status":{"type":"string","description":"active | error | disconnected"},"created_at":{"type":"string"}},"required":["id","provider","email","imap_host","imap_port","filter_customer_only","last_sync_at","status","created_at"],"description":"Mit Datenbank: die Spalten von `email_sync_config` — ohne das Zugriffstoken"},{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"provider":{"type":"string"},"email":{"type":"string"},"imapHost":{"type":["string","null"]},"imapPort":{"type":["integer","null"]},"oauthRefreshToken":{"type":["string","null"]},"filterCustomerOnly":{"type":"boolean"},"lastSyncAt":{"type":["string","null"]},"status":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","tenantId","userId","provider","email","imapHost","imapPort","oauthRefreshToken","filterCustomerOnly","lastSyncAt","status","createdAt"]}]}}},"required":["items"]},"example":{"items":[{"id":"string","provider":"string","email":"string","imap_host":"string","imap_port":0,"filter_customer_only":true,"last_sync_at":"string","status":"string","created_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1CrmEmail-syncConfig","tags":["CRM","EmailSync"],"parameters":[],"summary":"Get email-sync config","description":"Liest die hinterlegten Postfach-Verbindungen des Mandanten. Das Zugriffstoken (`oauth_refresh_token`) bleibt draussen — die Abfrage waehlt es gar nicht erst aus. Die Tabellen werden bei Bedarf angelegt, ein leeres `items` heisst also „nichts eingerichtet\", nicht „Tabelle fehlt\". Ohne Datenbankverbindung kommen die Eintraege aus einem Zwischenspeicher im Arbeitsspeicher und tragen dann ANDERE Feldnamen (camelCase) — siehe Schema."},"post":{"responses":{"201":{"description":"Die angelegte Verbindung — ohne das Zugriffstoken.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string","description":"Das Mandanten-Kuerzel, nicht die Mandanten-UUID"},"userId":{"type":"string","description":"Leer, wenn kein Nutzer im Kontext stand"},"provider":{"type":"string"},"email":{"type":"string"},"imapHost":{"type":["string","null"]},"imapPort":{"type":["integer","null"]},"filterCustomerOnly":{"type":"boolean"},"lastSyncAt":{"type":"null","description":"Beim Anlegen immer null"},"status":{"type":"string","const":"active"},"createdAt":{"type":"string"}},"required":["id","tenantId","userId","provider","email","imapHost","imapPort","filterCustomerOnly","lastSyncAt","status","createdAt"]},"example":{"id":"string","tenantId":"string","userId":"string","provider":"string","email":"string","imapHost":"string","imapPort":0,"filterCustomerOnly":true,"lastSyncAt":null,"status":"active","createdAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1CrmEmail-syncConfig","tags":["CRM","EmailSync"],"parameters":[],"summary":"Create email-sync config","description":"Hinterlegt eine Postfach-Verbindung (`imap`, `gmail` oder `outlook`). Ein mitgeschicktes `oauthRefreshToken` wird gespeichert, aber NICHT zurueckgegeben. Es gibt keine Pruefung auf Eindeutigkeit — dieselbe Adresse mehrfach anzulegen erzeugt mehrere Eintraege, und die Verbindung wird beim Anlegen nicht getestet: `status` steht immer auf `active`. Ein Schreibfehler wird als Fehler gemeldet und nicht still in den Arbeitsspeicher umgeleitet.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["imap","gmail","outlook"]},"email":{"type":"string","format":"email"},"imapHost":{"type":"string"},"imapPort":{"type":"integer"},"oauthRefreshToken":{"type":"string"},"filterCustomerOnly":{"type":"boolean","default":true}},"required":["provider","email"]},"example":{"provider":"imap","email":"beispiel@example.com","imapHost":"string","imapPort":0,"oauthRefreshToken":"string","filterCustomerOnly":true}}}}}},"/api/v1/crm/email-sync/config/{id}":{"delete":{"responses":{"200":{"description":"Geloescht — die Antwort traegt nur die Bestaetigung.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Es gibt keine solche Verbindung in diesem Mandanten"}},"operationId":"deleteApiV1CrmEmail-syncConfigById","tags":["CRM","EmailSync"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete email-sync config","description":"Entfernt eine Postfach-Verbindung endgueltig — kein Soft-Delete, kein „Rueckgaengig\". Geloescht wird nur, was auch dem eigenen Mandanten gehoert; trifft das Loeschen keine Zeile, kommt 404 statt eines stillen 200. Bereits abgerufene Mails im Posteingang bleiben stehen."}},"/api/v1/crm/email-sync/inbox":{"get":{"responses":{"200":{"description":"Die gefilterten Mails. Ohne Datenbank die Speicher-Form.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"from_email":{"type":"string"},"from_name":{"type":["string","null"]},"subject":{"type":"string"},"snippet":{"type":["string","null"]},"received_at":{"type":"string"},"matched_customer_id":{"type":["string","null"]},"matched_contact_id":{"type":["string","null"]},"is_read":{"type":"boolean"}},"required":["id","tenant_id","from_email","from_name","subject","snippet","received_at","matched_customer_id","matched_contact_id","is_read"],"description":"Mit Datenbank: alle Spalten von `email_inbox`"},{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"fromEmail":{"type":"string"},"fromName":{"type":["string","null"]},"subject":{"type":"string"},"snippet":{"type":"string"},"receivedAt":{"type":"string"},"matchedCustomerId":{"type":["string","null"]},"matchedContactId":{"type":["string","null"]},"isRead":{"type":"boolean"}},"required":["id","tenantId","fromEmail","fromName","subject","snippet","receivedAt","matchedCustomerId","matchedContactId","isRead"]}]}}},"required":["items"]},"example":{"items":[{"id":"string","tenant_id":"string","from_email":"string","from_name":"string","subject":"string","snippet":"string","received_at":"string","matched_customer_id":"string","matched_contact_id":"string","is_read":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1CrmEmail-syncInbox","tags":["CRM","EmailSync"],"parameters":[],"summary":"Filtered inbox (customer-relevant)","description":"Listet die eingegangenen Mails des Mandanten, neueste zuerst, HOECHSTENS 100 — eine Blaetterung gibt es nicht, aeltere Mails sind ueber diesen Aufruf nicht erreichbar. Der Abfrageparameter `filter` steht ohne Angabe auf `matched` und zeigt dann nur Mails mit zugeordnetem Kunden; jeder andere Wert zeigt alles. Ohne Datenbankverbindung kommen die Eintraege aus einem Zwischenspeicher im Arbeitsspeicher und tragen dann ANDERE Feldnamen (camelCase) — siehe Schema."}},"/api/v1/crm/email-sync/sync":{"post":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"501":{"description":"Der einzige Ausgang dieses Aufrufs. Es gibt KEINEN Erfolgsfall und damit auch keine 2xx-Form — `ok` ist immer `false`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","const":"not_implemented"},"message":{"type":"string"},"tenantSlug":{"type":"string"}},"required":["ok","error","message","tenantSlug"]}}}}},"operationId":"postApiV1CrmEmail-syncSync","tags":["CRM","EmailSync"],"parameters":[],"summary":"Abruf jetzt anstossen — derzeit NICHT verfuegbar (501)","description":"Es gibt keinen Postfach-Abruf: `configStore`/`inboxStore` in dieser Datei sind Arrays im Arbeitsspeicher, es existiert kein IMAP-Anschluss und kein Zeitgeber. Die Route antwortet deshalb 501 mit Begruendung."}},"/api/v1/crm/email-sync/inbox/{id}/read":{"patch":{"responses":{"200":{"description":"Als gelesen markiert — die Antwort traegt nur die Bestaetigung.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Es gibt keine solche Mail in diesem Mandanten"}},"operationId":"patchApiV1CrmEmail-syncInboxByIdRead","tags":["CRM","EmailSync"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mark inbox item as read","description":"Setzt eine Mail des Posteingangs auf gelesen. Der Aufruf kennt nur diese eine Richtung — auf ungelesen zuruecksetzen laesst sich eine Mail hier nicht. Betroffen ist nur, was dem eigenen Mandanten gehoert; trifft die Aenderung keine Zeile, kommt 404 statt eines stillen 200."}},"/api/v1/crm/email-sync/send":{"post":{"responses":{"200":{"description":"Angenommen vom Versanddienst — kein Zustellnachweis.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"to":{"type":"string","description":"Der Hauptempfaenger aus der Anfrage"},"subject":{"type":"string"}},"required":["ok","to","subject"]},"example":{"ok":true,"to":"string","subject":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"502":{"description":"Der Versanddienst hat abgelehnt"}},"operationId":"postApiV1CrmEmail-syncSend","tags":["CRM","EmailSync"],"parameters":[],"summary":"Reply / send an email from the CRM inbox","description":"Verschickt eine Mail an `to` (mit optionalem `cc`). `body` ist reiner Text und wird fuer den HTML-Teil sicher umgewandelt; der Text geht unveraendert mit. Versendet wird ueber den geprueften Absender des Mandanten, sofern einer eingerichtet ist, sonst ueber den Systemabsender. Der Aufruf haengt an KEINER Mail des Posteingangs — es entsteht kein Bezug zu einem Eintrag und nichts wird gespeichert. Lehnt der Anbieter ab, kommt 502 mit einer verstaendlichen Meldung; die Rohmeldung bleibt auf dem Server.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","format":"email"},"subject":{"type":"string","minLength":1,"maxLength":500},"body":{"type":"string","minLength":1,"maxLength":50000},"cc":{"type":"array","items":{"type":"string","format":"email"}}},"required":["to","subject","body"]},"example":{"to":"beispiel@example.com","subject":"string","body":"string","cc":["beispiel@example.com"]}}}}}},"/api/v1/email-inbox/config":{"get":{"responses":{"200":{"description":"Konfiguration — configured=false heisst: noch nichts hinterlegt","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"configured":{"type":"boolean","const":true},"inboxAddress":{"type":["string","null"]},"imapHost":{"type":["string","null"]},"imapPort":{"type":["integer","null"]},"imapUser":{"type":["string","null"]},"useSesInbound":{"type":"boolean"},"allowedSenderDomains":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"lastPolledAt":{"type":["string","null"]}},"required":["configured","inboxAddress","imapHost","imapPort","imapUser","useSesInbound","allowedSenderDomains","enabled","lastPolledAt"]},{"type":"object","properties":{"configured":{"type":"boolean","const":false},"inboxAddress":{"type":"string","description":"Vorschlag: die Standardadresse des Mandanten"},"enabled":{"type":"boolean","const":false},"useSesInbound":{"type":"boolean","const":false},"allowedSenderDomains":{"type":"array","items":{"type":"string"}},"lastPolledAt":{"type":"null"}},"required":["configured","inboxAddress","enabled","useSesInbound","allowedSenderDomains","lastPolledAt"]}]},"example":{"configured":true,"inboxAddress":"string","imapHost":"string","imapPort":0,"imapUser":"string","useSesInbound":true,"allowedSenderDomains":["string"],"enabled":true,"lastPolledAt":"string"}}}},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"DB unavailable"}},"operationId":"getApiV1Email-inboxConfig","tags":["email-inbox"],"parameters":[],"description":"Aktuelle E-Mail-Inbox-Konfiguration des Tenants lesen. Gelesen wird public.email_inbox_config; fehlt die Tabelle, legt der Aufruf sie zuvor an. Ist fuer den Mandanten noch nichts hinterlegt, kommt trotzdem 200 mit configured=false und der vorgeschlagenen Standardadresse — die Felder imapHost, imapPort und imapUser fehlen dann ganz. Das IMAP-Passwort liegt verschluesselt in der Tabelle und wird hier weder entschluesselt noch mitgeschickt.","summary":"Aktuelle E-Mail-Inbox-Konfiguration des Tenants lesen","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Gespeichert — quittiert nur, gibt die Konfiguration nicht zurueck","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"DB nicht verfuegbar oder Hauptschluessel fehlt"}},"operationId":"putApiV1Email-inboxConfig","tags":["email-inbox"],"parameters":[],"description":"E-Mail-Inbox-Konfiguration des Tenants anlegen oder aktualisieren. Der Schreibvorgang ist ein Upsert auf den Mandanten: nicht gesendete Felder behalten ihren gespeicherten Wert. Die Eingangsadresse ist die Ausnahme — sie wird immer geschrieben und faellt ohne Angabe auf die Standardadresse des Mandanten zurueck. Ein mitgeschicktes imapPassword wird vor dem Speichern mit AES-256-GCM verschluesselt; fehlt der Hauptschluessel, bricht der Aufruf mit 503 ab, BEVOR etwas geschrieben wird. Die Antwort ist eine reine Quittung ohne die neue Konfiguration.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"inboxAddress":{"type":"string","format":"email"},"imapHost":{"type":["string","null"]},"imapPort":{"type":["integer","null"],"minimum":1,"maximum":65535},"imapUser":{"type":["string","null"]},"imapPassword":{"type":["string","null"],"minLength":1,"maxLength":512},"useSesInbound":{"type":"boolean"},"allowedSenderDomains":{"type":"array","items":{"type":"string","minLength":1}},"enabled":{"type":"boolean"}}},"example":{"inboxAddress":"beispiel@example.com","imapHost":"string","imapPort":1,"imapUser":"string","imapPassword":"string","useSesInbound":true,"allowedSenderDomains":["string"],"enabled":true}}}},"summary":"E-Mail-Inbox-Konfiguration des Tenants anlegen oder aktualisieren","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/email-inbox/imports":{"get":{"responses":{"200":{"description":"Die letzten Importe, neueste zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"senderEmail":{"type":["string","null"]},"subject":{"type":["string","null"]},"receivedAt":{"type":"string"},"processedAt":{"type":["string","null"]},"status":{"type":["string","null"]},"errorMessage":{"type":["string","null"]},"documentCount":{"type":"integer","minimum":0,"description":"Laenge von document_ids — die Dokumente selbst fehlen hier"}},"required":["id","senderEmail","subject","receivedAt","processedAt","status","errorMessage","documentCount"]}}},"required":["items"]},"example":{"items":[{"id":"string","senderEmail":"string","subject":"string","receivedAt":"string","processedAt":"string","status":"string","errorMessage":"string","documentCount":0}]}}}},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"DB unavailable"}},"operationId":"getApiV1Email-inboxImports","tags":["email-inbox"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":10}}],"description":"Letzte E-Mail-Imports (Status, Sender, Anzahl Dokumente). Gelesen wird public.email_imports des Mandanten, neueste Zustellung zuerst; fehlt die Tabelle, legt der Aufruf sie zuvor an. limit nimmt 1 bis 100 an (Vorgabe 10), blaettern laesst sich nicht. documentCount ist nur die Laenge des Feldes document_ids — die Dokumente selbst stehen nicht in der Antwort.","summary":"Letzte E-Mail-Imports (Status, Sender, Anzahl Dokumente)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/email-inbox/test-connection":{"post":{"responses":{"200":{"description":"Verbindung erfolgreich — reine Quittung, keine Postfachdaten","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"Kein Mandantenkontext"},"422":{"description":"Login fehlgeschlagen / unvollständige Daten"},"503":{"description":"DB / Krypto nicht verfügbar"}},"operationId":"postApiV1Email-inboxTest-connection","tags":["email-inbox"],"parameters":[],"summary":"Testet die IMAP-Verbindung mit den hinterlegten Zugangsdaten","description":"IMAP-Verbindung testen: Login-Versuch mit gespeicherten oder übergebenen Zugangsdaten (zeitlich begrenzt, ohne Passwort-Logging).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"imapHost":{"type":["string","null"]},"imapPort":{"type":["integer","null"],"minimum":1,"maximum":65535},"imapUser":{"type":["string","null"]},"imapPassword":{"type":["string","null"],"minLength":1,"maxLength":512}}},"example":{"imapHost":"string","imapPort":1,"imapUser":"string","imapPassword":"string"}}}}}},"/api/v1/inbox-addresses":{"get":{"responses":{"200":{"description":"Adressen des Mandanten samt Bausteinen für das Anlege-Feld","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"label":{"type":"string"},"tag":{"type":"string","description":"Aus dem Label abgeleiteter vorderer Teil der Adresse"},"address":{"type":"string","description":"Vollständige Empfangsadresse in der Form <tag>@nemix.email"},"enabled":{"type":"boolean"},"box":{"type":"string","description":"Box der Adresse. Neu angelegt wird ausschließlich \"inbox\"."},"folderId":{"type":["string","null"],"format":"uuid","description":"Gebundener Eingang-Ordner, sonst null"},"lastReceivedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"totalMails":{"type":"integer","description":"Alle je an diese Adresse gegangenen Mails"},"totalDocuments":{"type":"integer","description":"Summe der daraus entstandenen Dokumente"}},"required":["id","label","tag","address","enabled","box","folderId","lastReceivedAt","createdAt","totalMails","totalDocuments"]}},"baseHint":{"type":"string","description":"Beispieladresse mit Platzhalter, etwa <name>@nemix.email"},"addressSuffix":{"type":"string","description":"Fester hinterer Teil jeder Adresse, @nemix.email"},"companyExample":{"type":"string","description":"Firmenname als Slug, nur Platzhalter fürs Eingabefeld. Leer, wenn kein Name gepflegt ist."}},"required":["data","baseHint","addressSuffix","companyExample"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","label":"string","tag":"string","address":"string","enabled":true,"box":"string","folderId":"00000000-0000-4000-8000-000000000000","lastReceivedAt":"string","createdAt":"string","totalMails":0,"totalDocuments":0}],"baseHint":"string","addressSuffix":"string","companyExample":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"DB unavailable"}},"operationId":"getApiV1Inbox-addresses","tags":["inbox-addresses"],"parameters":[],"summary":"Empfangsadressen des Mandanten inkl. Gesamt-Zähler","description":"Liest die nicht geloeschten Empfangsadressen des Mandanten aus `public.inbox_addresses`, neueste zuerst, und haengt an jede zwei Zaehler: wie viele Mails ueber sie ankamen und wie viele Belege daraus entstanden. Die Zaehler stammen aus `public.email_imports` und sind Gesamtwerte SEIT BEGINN — kein Zeitraum, keine Angabe fuer die letzten Tage. Adressen ohne Eingang stehen mit 0 darin.\n\nEs wird nicht geblaettert, nicht gefiltert und nicht gesucht; die Antwort enthaelt immer alle Adressen. Abgeschaltete Adressen (`enabled: false`) bleiben dabei — wer nur die aktiven will, muss selbst filtern.\n\nNeben `data` kommen drei Bausteine fuer das Anlege-Feld der Oberflaeche: `baseHint` und `addressSuffix` zeigen den Aufbau einer Adresse, `companyExample` ist NUR ein Platzhalter-Vorschlag und keine bestehende Adresse."},"post":{"responses":{"201":{"description":"Die angelegte Empfangsadresse","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"label":{"type":"string"},"tag":{"type":"string","description":"Aus dem Label abgeleiteter vorderer Teil der Adresse"},"address":{"type":"string","description":"Vollständige Empfangsadresse in der Form <tag>@nemix.email"},"enabled":{"type":"boolean"},"box":{"type":"string","description":"Box der Adresse. Neu angelegt wird ausschließlich \"inbox\"."},"folderId":{"type":["string","null"],"format":"uuid","description":"Gebundener Eingang-Ordner, sonst null"},"lastReceivedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"totalMails":{"type":"integer","description":"Alle je an diese Adresse gegangenen Mails"},"totalDocuments":{"type":"integer","description":"Summe der daraus entstandenen Dokumente"}},"required":["id","label","tag","address","enabled","box","folderId","lastReceivedAt","createdAt","totalMails","totalDocuments"]},"example":{"id":"00000000-0000-4000-8000-000000000000","label":"string","tag":"string","address":"string","enabled":true,"box":"string","folderId":"00000000-0000-4000-8000-000000000000","lastReceivedAt":"string","createdAt":"string","totalMails":0,"totalDocuments":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Tag belegt oder bereits eine Eingang-Adresse vorhanden"}},"operationId":"postApiV1Inbox-addresses","tags":["inbox-addresses"],"parameters":[],"description":"Legt eine Zeile in `public.inbox_addresses` an. Der Tag entsteht aus dem Label und bekommt bei Kollision innerhalb des Mandanten einen Zähler (eingang, eingang-2), die Adresse ist dann <tag>@nemix.email. Pro Mandant ist genau eine Eingang-Adresse erlaubt: existiert bereits eine, kommt 409 mit `inbox_address_exists`, bei einer global schon vergebenen Adresse 409 mit `address_taken`, und ein übergebenes `folderId` muss zu Mandant und Box passen, sonst 404. Die Zähler `totalMails` und `totalDocuments` stehen in dieser Antwort immer auf 0.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":80},"box":{"type":"string","enum":["inbox"]},"folderId":{"type":["string","null"],"format":"uuid"}},"required":["label"]},"example":{"label":"string","box":"inbox","folderId":"00000000-0000-4000-8000-000000000000"}}}},"summary":"Legt eine Zeile in `public.inbox_addresses` an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inbox-addresses/check":{"get":{"responses":{"200":{"description":"Normalisierte Adresse und ob sie noch frei ist","content":{"application/json":{"schema":{"type":"object","properties":{"front":{"type":"string","description":"Die angefragte Eingabe, unverändert zurückgegeben"},"tag":{"type":"string","description":"Normalisierte Form der Eingabe, leer wenn nichts Gültiges übrig bleibt"},"address":{"type":"string","description":"Die geprüfte Adresse, leer bei leerem tag"},"valid":{"type":"boolean","description":"false, wenn aus der Eingabe kein Tag gebildet werden konnte"},"taken":{"type":"boolean","description":"true, wenn die Adresse mandantenübergreifend bereits existiert"}},"required":["front","tag","address","valid","taken"]},"example":{"front":"string","tag":"string","address":"string","valid":true,"taken":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Inbox-addressesCheck","tags":["inbox-addresses"],"parameters":[{"in":"query","name":"front","schema":{"type":"string","maxLength":80,"default":""}}],"description":"Prüft ohne Mandantenfilter, ob eine Adresse schon existiert. Die Spalte `address` in `public.inbox_addresses` ist über alle Mandanten hinweg eindeutig, deshalb ist auch die Prüfung global. Der Parameter `front` wird zuerst normalisiert (Kleinbuchstaben, nur a-z, 0-9 und Bindestriche, höchstens 40 Zeichen); bleibt dabei nichts übrig, antwortet die Route ohne Datenbankzugriff mit `valid: false`. Soft-gelöschte Adressen zählen als vergeben, damit die Prüfung dasselbe Ergebnis liefert wie das spätere Anlegen.","summary":"Prüft ohne Mandantenfilter, ob eine Adresse schon existiert","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inbox-addresses/{id}":{"patch":{"responses":{"200":{"description":"Die geänderte Empfangsadresse","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"label":{"type":"string"},"tag":{"type":"string","description":"Aus dem Label abgeleiteter vorderer Teil der Adresse"},"address":{"type":"string","description":"Vollständige Empfangsadresse in der Form <tag>@nemix.email"},"enabled":{"type":"boolean"},"box":{"type":"string","description":"Box der Adresse. Neu angelegt wird ausschließlich \"inbox\"."},"folderId":{"type":["string","null"],"format":"uuid","description":"Gebundener Eingang-Ordner, sonst null"},"lastReceivedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"totalMails":{"type":"integer","description":"Alle je an diese Adresse gegangenen Mails"},"totalDocuments":{"type":"integer","description":"Summe der daraus entstandenen Dokumente"}},"required":["id","label","tag","address","enabled","box","folderId","lastReceivedAt","createdAt","totalMails","totalDocuments"]},"example":{"id":"00000000-0000-4000-8000-000000000000","label":"string","tag":"string","address":"string","enabled":true,"box":"string","folderId":"00000000-0000-4000-8000-000000000000","lastReceivedAt":"string","createdAt":"string","totalMails":0,"totalDocuments":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"patchApiV1Inbox-addressesById","tags":["inbox-addresses"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Label und Aktiv-Schalter einer Empfangsadresse ändern","description":"Ändert Label und Aktiv-Schalter einer Adresse des eigenen Mandanten und setzt `updated_at` neu; nicht übergebene Felder bleiben stehen. Tag und Adresse selbst ändern sich nie mit, eine bereits verteilte Adresse bleibt also gültig. Trifft die id keine ungelöschte Zeile des Mandanten, kommt 404; die Zähler `totalMails` und `totalDocuments` stehen in dieser Antwort immer auf 0.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":80},"enabled":{"type":"boolean"}}},"example":{"label":"string","enabled":true}}}}},"delete":{"responses":{"200":{"description":"Löschen angenommen","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1Inbox-addressesById","tags":["inbox-addresses"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt `deleted_at` und `updated_at` auf der Adresse des eigenen Mandanten. Die Zeile bleibt erhalten und verschwindet nur aus der Liste. Die Adresse bleibt danach global belegt, weil die UNIQUE-Spalte `address` auch gelöschte Zeilen umfasst; sie lässt sich also nicht erneut anlegen. Die Route prüft nicht, ob eine Zeile getroffen wurde, und antwortet auch bei unbekannter id mit `{ \"success\": true }`.","summary":"Setzt `deleted_at` und `updated_at` auf der Adresse des eigenen Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inbox-addresses/{id}/stats":{"get":{"responses":{"200":{"description":"Zähler für den gewählten Zeitraum","content":{"application/json":{"schema":{"type":"object","properties":{"period":{"type":"string","enum":["week","month","year"]},"mails":{"type":"integer"},"documents":{"type":"integer"}},"required":["period","mails","documents"]},"example":{"period":"week","mails":0,"documents":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"getApiV1Inbox-addressesByIdStats","tags":["inbox-addresses"],"parameters":[{"in":"query","name":"period","schema":{"type":"string","enum":["week","month","year"],"default":"month"}},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mail- und Dokumentzähler einer Empfangsadresse lesen","description":"Zählt in `public.email_imports` die Mails, die an diese Adresse gingen, und die daraus entstandenen Dokumente (Summe der `document_ids`). Der Zeitraum beginnt am Kalenderanfang der gewählten Periode, also Montag, Monatserster oder 1. Januar (`period` = week, month oder year, Standard month). Gehört die id keiner ungelöschten Adresse des Mandanten, kommt 404."}},"/api/v1/email/send":{"post":{"responses":{"200":{"description":"Angenommen und an den Anbieter uebergeben. Das ist KEINE Zustellbestaetigung, und die Antwort traegt keine Nachrichten-Kennung.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"to":{"type":"string"},"subject":{"type":"string"}},"required":["ok","to","subject"],"additionalProperties":false},"example":{"ok":true,"to":"string","subject":"string"}}}},"400":{"description":"Bad request / tenant missing"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"502":{"description":"Der Mailanbieter hat den Versand abgelehnt"}},"operationId":"postApiV1EmailSend","tags":["email"],"parameters":[],"description":"Versendet eine E-Mail im Namen des Tenants. Der Rumpf ist REINER TEXT — er wird zeilenweise in Absaetze umgewandelt und dabei HTML-escaped; eigenes Markup kommt also nicht durch. Hoechstens 50 000 Zeichen, Betreff hoechstens 500. `cc`-Adressen werden derzeit wie weitere Empfaenger behandelt, nicht als echtes CC-Feld. Versendet wird ueber den fuer diesen Mandanten hinterlegten Absender, sonst ueber den System-Absender. Der Aufruf schreibt nichts in ein Postfach und legt keinen Beleg an; ein Fehler des Anbieters ergibt 502 mit einer verstaendlichen Meldung — die Rohmeldung bleibt serverseitig.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","format":"email"},"subject":{"type":"string","minLength":1,"maxLength":500},"body":{"type":"string","minLength":1,"maxLength":50000},"cc":{"type":"array","items":{"type":"string","format":"email"}}},"required":["to","subject","body"]},"example":{"to":"beispiel@example.com","subject":"string","body":"string","cc":["beispiel@example.com"]}}}},"summary":"Versendet eine E-Mail im Namen des Tenants","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/crm/segments":{"get":{"responses":{"200":{"description":"Die Segmente des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `name`, `description`, `type`, `filters`, `member_ids`, `created_at`, `updated_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu."},"description":"Die Segmente des Mandanten, absteigend nach Anlagezeitpunkt"}},"required":["items"],"additionalProperties":false},"example":{"items":[{}]}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"503":{"description":"Abfrage fehlgeschlagen; die Liste wird NICHT ersatzweise aus dem Speicher beantwortet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CrmSegments","tags":["CRM","Segments"],"parameters":[],"summary":"List segments","description":"Listet die Kundensegmente des Mandanten. Ohne erreichbare Datenbank antwortet ein Rueckfall aus dem Prozessspeicher; der wird nirgends befuellt und liefert deshalb immer eine leere Liste."},"post":{"responses":{"201":{"description":"Das angelegte Segment, wie der Server es gespeichert hat","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Segments, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandantenkennung in Schema-Schreibweise — Bindestriche sind zu `_` normalisiert"},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Bezeichnung des Segments"},"description":{"type":["string","null"],"description":"Freitext-Erlaeuterung; `null`, wenn keine angegeben wurde"},"type":{"type":"string","enum":["dynamic","static"],"description":"`dynamic` = Filter wird bei jeder Abfrage ausgewertet, `static` = feste Mitgliederliste"},"filters":{"type":["object","null"],"additionalProperties":{},"description":"Filterdefinition fuer `dynamic`; `null` bei einem statischen Segment"},"memberIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Feste Mitglieder fuer `static`; leer bei einem dynamischen Segment"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC); beim Anlegen gleich `createdAt`"}},"required":["id","tenantId","name","description","type","filters","memberIds","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","name":"string","description":"string","type":"dynamic","filters":{},"memberIds":["00000000-0000-4000-8000-000000000000"],"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis). Ein unzulaessiger Mandanten-Slug antwortet dagegen mit Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"503":{"description":"Keine Datenbank erreichbar oder INSERT fehlgeschlagen — es wurde nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1CrmSegments","tags":["CRM","Segments"],"parameters":[],"summary":"Create segment","description":"Legt ein Segment an. Ohne erreichbare Datenbank wird KEIN 201 vorgetaeuscht, sondern 503 gesendet — der Speicher-Rueckfall nimmt keine Schreibvorgaenge entgegen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string"},"type":{"type":"string","enum":["dynamic","static"],"default":"dynamic"},"filters":{"type":"object","additionalProperties":{}},"memberIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["name"]},"example":{"name":"string","description":"string","type":"dynamic","filters":{},"memberIds":["00000000-0000-4000-8000-000000000000"]}}}}}},"/api/v1/crm/segments/{id}":{"get":{"responses":{"200":{"description":"Der Datensatz in Datenbank-Schreibweise","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `name`, `description`, `type`, `filters`, `member_ids`, `created_at`, `updated_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu."},"example":{"id":"5d8e2f1a-3b4c-4d5e-9f6a-7b8c9d0e1f2a","tenant_id":"musterbau_gmbh","name":"Stammkunden Bayern","description":"Kunden mit mindestens drei Aufträgen in den letzten zwölf Monaten, Sitz in Bayern.","type":"dynamic","filters":{"region":"BY","min_orders":3,"months":12},"member_ids":[],"created_at":"2026-01-15T11:20:00.000Z","updated_at":"2026-06-02T07:45:31.000Z"}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Kein Segment mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CrmSegmentsById","tags":["CRM","Segments"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get segment","description":"Liefert ein einzelnes Segment. Die Datenbankzeile wird OHNE Umschlag gesendet — die Felder stehen also direkt im Wurzelobjekt. Ohne erreichbare Datenbank antwortet der leere Speicher-Rueckfall mit 404."},"patch":{"responses":{"200":{"description":"Der geaenderte Datensatz in Datenbank-Schreibweise","content":{"application/json":{"schema":{"type":"object","additionalProperties":{},"description":"Der Datensatz in Datenbank-Schreibweise (`id`, `tenant_id`, `name`, `description`, `type`, `filters`, `member_ids`, `created_at`, `updated_at`). Der Handler reicht `SELECT *` unveraendert durch — deshalb sagt die Spezifikation die Feldliste nicht zu."}}}},"400":{"description":"ZWEI Formen unter demselben Code: das rohe Zod-Ergebnis bei unzulaessigem Rumpf, oder `{ \"error\": \"No fields\" }`, wenn der Rumpf kein aenderbares Feld enthaelt.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]},{"type":"object","properties":{"error":{"type":"string","const":"No fields","description":"Der Rumpf enthielt kein Feld, das geschrieben werden koennte"}},"required":["error"],"additionalProperties":false}]}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Kein Segment mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Keine Datenbank erreichbar oder UPDATE fehlgeschlagen — es wurde nichts geaendert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1CrmSegmentsById","tags":["CRM","Segments"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update segment","description":"Aendert einzelne Felder eines Segments und gibt die geaenderte Datenbankzeile OHNE Umschlag zurueck. Ohne erreichbare Datenbank wird kein 200 vorgetaeuscht, sondern 503 gesendet.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string"},"type":{"type":"string","enum":["dynamic","static"],"default":"dynamic"},"filters":{"type":"object","additionalProperties":{}},"memberIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},"example":{"name":"string","description":"string","type":"dynamic","filters":{},"memberIds":["00000000-0000-4000-8000-000000000000"]}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzlast","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` — die Zeile ist entfernt"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"503":{"description":"Keine Datenbank erreichbar oder DELETE fehlgeschlagen — es wurde nichts entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1CrmSegmentsById","tags":["CRM","Segments"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete segment","description":"Entfernt das Segment endgueltig (kein Soft-Delete). Die Antwort ist auch dann 200, wenn die Kennung gar nicht vorhanden war — der Aufruf sagt „danach ist es weg\", nicht „es war da\"."}},"/api/v1/crm/segments/{id}/preview":{"get":{"responses":{"200":{"description":"Umfang und Stichprobe der hinterlegten Mitglieder","content":{"application/json":{"schema":{"type":"object","properties":{"memberCount":{"type":"integer","minimum":0,"description":"Anzahl der hinterlegten Mitglieder; 0, wenn `member_ids` keine Liste ist"},"sample":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":5,"description":"Die ersten hoechstens fuenf Mitglieder — eine Stichprobe, keine vollstaendige Liste"},"type":{"type":"string","enum":["dynamic","static"],"description":"Art des Segments"}},"required":["memberCount","sample","type"],"additionalProperties":false},"example":{"memberCount":0,"sample":["00000000-0000-4000-8000-000000000000"],"type":"dynamic"}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Kein Segment mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CrmSegmentsByIdPreview","tags":["CRM","Segments"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Preview segment members","description":"Zeigt Umfang und Stichprobe der Segment-Mitglieder. ACHTUNG, unfertig: der Filter eines `dynamic`-Segments wird NICHT ausgewertet. Gezaehlt und gezeigt wird nur die fest hinterlegte Liste `member_ids` — ein dynamisches Segment erscheint hier deshalb mit `memberCount` 0, auch wenn sein Filter Kunden treffen wuerde."}},"/api/v1/crm/campaigns":{"get":{"responses":{"200":{"description":"Die Kampagnen des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string"},"body_html":{"type":"string"},"segment_id":{"type":"string","format":"uuid"},"trigger":{"type":"string","description":"`manual`, `scheduled` oder `event`; Vorgabe `manual`"},"send_at":{"type":["string","null"],"description":"Geplanter Versand (ISO 8601) oder `null`"},"from_email":{"type":["string","null"]},"from_name":{"type":["string","null"]},"status":{"type":"string","description":"Vorgabe `draft`"},"sent_count":{"type":"integer"},"opened_count":{"type":"integer"},"clicked_count":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","name","subject","body_html","segment_id","trigger","send_at","from_email","from_name","status","sent_count","opened_count","clicked_count","created_at","updated_at"],"description":"Der Datensatz in Datenbank-Schreibweise, so wie `SELECT *` ihn liefert"},"description":"Die Kampagnen des Mandanten, absteigend nach Anlagezeitpunkt"}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"string","name":"string","subject":"string","body_html":"string","segment_id":"00000000-0000-4000-8000-000000000000","trigger":"string","send_at":"string","from_email":"string","from_name":"string","status":"string","sent_count":0,"opened_count":0,"clicked_count":0,"created_at":"string","updated_at":"string"}]}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"503":{"description":"Abfrage fehlgeschlagen; die Liste wird NICHT ersatzweise aus dem Speicher beantwortet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CrmCampaigns","tags":["CRM","Campaigns"],"parameters":[],"summary":"List campaigns","description":"Listet die Mailkampagnen des Mandanten. Ohne erreichbare Datenbank antwortet ein Rueckfall aus dem Prozessspeicher; der wird nirgends befuellt und liefert deshalb immer eine leere Liste."},"post":{"responses":{"201":{"description":"Die angelegte Kampagne, wie der Server sie gespeichert hat","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Kampagne, im Server erzeugt (UUID v4)"},"tenantId":{"type":"string","minLength":1,"description":"Mandantenkennung in Schema-Schreibweise — Bindestriche sind zu `_` normalisiert"},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Interne Bezeichnung der Kampagne"},"subject":{"type":"string","minLength":1,"maxLength":200,"description":"Betreffzeile der Mail"},"bodyHtml":{"type":"string","minLength":1,"description":"Mailtext als HTML"},"segmentId":{"type":"string","format":"uuid","description":"Kennung des Segments, das die Empfaenger bestimmt"},"trigger":{"type":"string","enum":["manual","scheduled","event"],"description":"`manual` = von Hand ausloesen, `scheduled` = zu `sendAt`, `event` = durch ein Ereignis"},"sendAt":{"type":["string","null"],"format":"date-time","description":"Geplanter Versandzeitpunkt (ISO 8601); `null` bei `manual` und `event`"},"fromEmail":{"type":["string","null"],"format":"email","description":"Absenderadresse; `null`, wenn die Mandanten-Vorgabe gilt"},"fromName":{"type":["string","null"],"maxLength":100,"description":"Absendername; `null`, wenn die Mandanten-Vorgabe gilt"},"status":{"type":"string","enum":["draft","scheduled"],"description":"Beim Anlegen nur diese zwei Werte: `scheduled` bei `trigger` = `scheduled`, sonst `draft`. Spaeter kommen `sending`, `sent` und `failed` hinzu."},"sentCount":{"type":"integer","minimum":0,"description":"Zahl der versendeten Mails — beim Anlegen 0"},"openedCount":{"type":"integer","minimum":0,"description":"Zahl der geoeffneten Mails — beim Anlegen 0"},"clickedCount":{"type":"integer","minimum":0,"description":"Zahl der angeklickten Mails — beim Anlegen 0"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC); beim Anlegen gleich `createdAt`"}},"required":["id","tenantId","name","subject","bodyHtml","segmentId","trigger","sendAt","fromEmail","fromName","status","sentCount","openedCount","clickedCount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","name":"string","subject":"string","bodyHtml":"string","segmentId":"00000000-0000-4000-8000-000000000000","trigger":"manual","sendAt":"2026-01-01T12:00:00.000Z","fromEmail":"beispiel@example.com","fromName":"string","status":"draft","sentCount":0,"openedCount":0,"clickedCount":0,"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis). Ein unzulaessiger Mandanten-Slug antwortet dagegen mit Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"503":{"description":"Keine Datenbank erreichbar oder INSERT fehlgeschlagen — es wurde nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1CrmCampaigns","tags":["CRM","Campaigns"],"parameters":[],"summary":"Create campaign","description":"Legt eine Kampagne an. Ohne erreichbare Datenbank wird KEIN 201 vorgetaeuscht, sondern 503 gesendet. `segmentId` wird NICHT gegen die Segment-Tabelle geprueft — eine unbekannte Kennung faellt erst beim Versand auf.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"subject":{"type":"string","minLength":1,"maxLength":200},"bodyHtml":{"type":"string","minLength":1},"segmentId":{"type":"string","format":"uuid"},"trigger":{"type":"string","enum":["manual","scheduled","event"],"default":"manual"},"sendAt":{"type":"string","format":"date-time"},"fromEmail":{"type":"string","format":"email"},"fromName":{"type":"string","maxLength":100}},"required":["name","subject","bodyHtml","segmentId"]},"example":{"name":"string","subject":"string","bodyHtml":"string","segmentId":"00000000-0000-4000-8000-000000000000","trigger":"manual","sendAt":"2026-01-01T12:00:00.000Z","fromEmail":"beispiel@example.com","fromName":"string"}}}}}},"/api/v1/crm/campaigns/{id}":{"get":{"responses":{"200":{"description":"Der Datensatz in Datenbank-Schreibweise","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string"},"body_html":{"type":"string"},"segment_id":{"type":"string","format":"uuid"},"trigger":{"type":"string","description":"`manual`, `scheduled` oder `event`; Vorgabe `manual`"},"send_at":{"type":["string","null"],"description":"Geplanter Versand (ISO 8601) oder `null`"},"from_email":{"type":["string","null"]},"from_name":{"type":["string","null"]},"status":{"type":"string","description":"Vorgabe `draft`"},"sent_count":{"type":"integer"},"opened_count":{"type":"integer"},"clicked_count":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","name","subject","body_html","segment_id","trigger","send_at","from_email","from_name","status","sent_count","opened_count","clicked_count","created_at","updated_at"],"description":"Der Datensatz in Datenbank-Schreibweise, so wie `SELECT *` ihn liefert"},"example":{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"string","name":"string","subject":"string","body_html":"string","segment_id":"00000000-0000-4000-8000-000000000000","trigger":"string","send_at":"string","from_email":"string","from_name":"string","status":"string","sent_count":0,"opened_count":0,"clicked_count":0,"created_at":"string","updated_at":"string"}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Keine Kampagne mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CrmCampaignsById","tags":["CRM","Campaigns"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get campaign","description":"Liefert eine einzelne Kampagne. Die Datenbankzeile wird OHNE Umschlag gesendet — die Felder stehen also direkt im Wurzelobjekt. Ohne erreichbare Datenbank antwortet der leere Speicher-Rueckfall mit 404."},"patch":{"responses":{"200":{"description":"Der geaenderte Datensatz in Datenbank-Schreibweise","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string"},"body_html":{"type":"string"},"segment_id":{"type":"string","format":"uuid"},"trigger":{"type":"string","description":"`manual`, `scheduled` oder `event`; Vorgabe `manual`"},"send_at":{"type":["string","null"],"description":"Geplanter Versand (ISO 8601) oder `null`"},"from_email":{"type":["string","null"]},"from_name":{"type":["string","null"]},"status":{"type":"string","description":"Vorgabe `draft`"},"sent_count":{"type":"integer"},"opened_count":{"type":"integer"},"clicked_count":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","name","subject","body_html","segment_id","trigger","send_at","from_email","from_name","status","sent_count","opened_count","clicked_count","created_at","updated_at"],"description":"Der Datensatz in Datenbank-Schreibweise, so wie `SELECT *` ihn liefert"},"example":{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"string","name":"string","subject":"string","body_html":"string","segment_id":"00000000-0000-4000-8000-000000000000","trigger":"string","send_at":"string","from_email":"string","from_name":"string","status":"string","sent_count":0,"opened_count":0,"clicked_count":0,"created_at":"string","updated_at":"string"}}}},"400":{"description":"ZWEI Formen unter demselben Code: das rohe Zod-Ergebnis bei unzulaessigem Rumpf, oder `{ \"error\": \"No fields\" }`, wenn der Rumpf kein aenderbares Feld enthaelt.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]},{"type":"object","properties":{"error":{"type":"string","const":"No fields","description":"Der Rumpf enthielt kein Feld, das geschrieben werden koennte"}},"required":["error"],"additionalProperties":false}]}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Keine Kampagne mit dieser Kennung in diesem Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Not found","description":"Feste Fehlerkennung; es gibt keine weitere Angabe"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Keine Datenbank erreichbar oder UPDATE fehlgeschlagen — es wurde nichts geaendert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1CrmCampaignsById","tags":["CRM","Campaigns"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update campaign","description":"Aendert einzelne Felder einer Kampagne und gibt die geaenderte Datenbankzeile OHNE Umschlag zurueck. Zaehler und `status` lassen sich hier NICHT setzen — die schreibt nur der Versand.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"subject":{"type":"string","minLength":1,"maxLength":200},"bodyHtml":{"type":"string","minLength":1},"segmentId":{"type":"string","format":"uuid"},"trigger":{"type":"string","enum":["manual","scheduled","event"],"default":"manual"},"sendAt":{"type":"string","format":"date-time"},"fromEmail":{"type":"string","format":"email"},"fromName":{"type":"string","maxLength":100}}},"example":{"name":"string","subject":"string","bodyHtml":"string","segmentId":"00000000-0000-4000-8000-000000000000","trigger":"manual","sendAt":"2026-01-01T12:00:00.000Z","fromEmail":"beispiel@example.com","fromName":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzlast","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` — die Zeile ist entfernt"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"503":{"description":"Keine Datenbank erreichbar oder DELETE fehlgeschlagen — es wurde nichts entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1CrmCampaignsById","tags":["CRM","Campaigns"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete campaign","description":"Entfernt die Kampagne endgueltig (kein Soft-Delete). Die Antwort ist auch dann 200, wenn die Kennung gar nicht vorhanden war — der Aufruf sagt „danach ist sie weg\", nicht „sie war da\"."}},"/api/v1/crm/campaigns/{id}/send":{"post":{"responses":{"200":{"description":"Quittung des Statuswechsels — KEINE Bestaetigung eines Versands","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` — der Statuswechsel wurde abgesetzt"},"message":{"type":"string","description":"Feste englische Meldung `Campaign queued for sending`"},"id":{"type":"string","description":"Die Kennung aus dem Pfad, unveraendert zurueckgegeben"}},"required":["ok","message","id"],"additionalProperties":false},"example":{"ok":true,"message":"string","id":"string"}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"503":{"description":"Keine Datenbank erreichbar oder UPDATE fehlgeschlagen — der Status blieb unveraendert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1CrmCampaignsByIdSend","tags":["CRM","Campaigns"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Send campaign now (queue)","description":"Setzt den Status der Kampagne auf `sending`. Mehr tut dieser Aufruf NICHT — er verschickt keine Mail und wartet auf nichts; der eigentliche Versand laeuft ausserhalb dieser Route. Der Statuscode ist 200, nicht 202, und er ist auch dann 200, wenn die Kennung gar nicht vorhanden war: das UPDATE trifft dann null Zeilen, und der Handler prueft das nicht nach."}},"/api/v1/billing/tenants/add":{"post":{"responses":{"200":{"description":"Tenant created and Stripe item updated","content":{"application/json":{"schema":{"type":"object","properties":{"tenant":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"joinedAt":{"type":"string"}},"required":["id","name","slug","joinedAt"]},"group":{"type":"object","properties":{"id":{"type":"string"},"tenant_count":{"type":"integer"},"extra_tenant_item_id":{"type":["string","null"],"description":"Stripe-Positions-Id des Aufpreises; `null`, solange die Gruppe nur einen Mandanten hat"}},"required":["id","tenant_count","extra_tenant_item_id"]},"aufpreis_eur":{"type":"number","description":"Monatlicher Aufpreis je zusaetzlichem Mandanten in Euro"}},"required":["tenant","group","aufpreis_eur"]},"example":{"tenant":{"id":"string","name":"string","slug":"string","joinedAt":"string"},"group":{"id":"string","tenant_count":0,"extra_tenant_item_id":"string"},"aufpreis_eur":0}}}},"400":{"description":"Invalid input"},"401":{"description":"Not authenticated"},"402":{"description":"Free-tier disallows multi-tenant"},"409":{"description":"Slug already used in this group"}},"operationId":"postApiV1BillingTenantsAdd","tags":["billing","multi-tenant"],"parameters":[],"summary":"Add a new tenant to the caller account-group (+ Aufpreis)","description":"Legt einen NEUEN Mandanten an (`public.tenants` mit Plan `starter`), nimmt ihn in die Abrechnungsgruppe des Aufrufers auf (`public.tenant_group_members`) und zieht den Zaehler `tenant_count` der Gruppe hoch. Hat der Aufrufer noch keine Gruppe, wird sie beim ersten Aufruf angelegt und auf seinen aktuellen Mandanten als Abrechnungsanker gesetzt.\n\nNebenwirkung bei Stripe: der erste Mandant ist im Grundpreis enthalten, ab dem zweiten entsteht auf dem Abonnement des Ankers die Position `extra_tenant` — beim zweiten Mandanten neu, danach wird nur die Menge erhoeht. Fehlt dem Anker ein Abonnement oder ist der Preis fuer den Plan nicht hinterlegt, wird der Stripe-Teil uebersprungen und der Mandant trotzdem angelegt — die Rechnung geht dann NICHT mit.\n\nDer Plan des aufrufenden Mandanten entscheidet: `free` wird mit 402 abgewiesen. Ein bereits vergebener Slug bricht an der Eindeutigkeit der Mandantentabelle ab. Es gibt keine Transaktion ueber Mandant, Mitgliedschaft und Stripe — brechen spaetere Schritte ab, bleiben die frueheren stehen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]*$","minLength":2,"maxLength":40}},"required":["name","slug"]},"example":{"name":"string","slug":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/billing/tenants/remove":{"post":{"responses":{"200":{"description":"Tenant removed, Stripe item updated","content":{"application/json":{"schema":{"type":"object","properties":{"removed_tenant_id":{"type":"string"},"group":{"type":"object","properties":{"id":{"type":"string"},"tenant_count":{"type":"integer"},"extra_tenant_item_id":{"type":["string","null"],"description":"Stripe-Positions-Id des Aufpreises; `null`, solange die Gruppe nur einen Mandanten hat"}},"required":["id","tenant_count","extra_tenant_item_id"]}},"required":["removed_tenant_id","group"]},"example":{"removed_tenant_id":"string","group":{"id":"string","tenant_count":0,"extra_tenant_item_id":"string"}}}}},"400":{"description":"Cannot remove the anchor tenant"},"401":{"description":"Not authenticated"},"403":{"description":"Caller is not the group owner"},"404":{"description":"Tenant not in caller group"}},"operationId":"postApiV1BillingTenantsRemove","tags":["billing","multi-tenant"],"parameters":[],"summary":"Soft-remove a tenant from the caller account-group","description":"Loest die Mitgliedschaft in `public.tenant_group_members` auf und setzt am Mandanten selbst `deleted_at` — die Zeile in `public.tenants` bleibt also erhalten, damit Rechnungen, Belege und das Protokoll weiter darauf zeigen koennen. Rueckgaengig macht dieser Endpunkt das nicht.\n\nDanach wird `tenant_count` heruntergezaehlt (Untergrenze 1) und die Stripe-Position `extra_tenant` angepasst: bleibt nur noch der Anker uebrig, wird die Position GELOESCHT statt auf Menge 0 gesetzt, damit die Rechnung sauber bleibt. Ohne Abonnement am Anker oder ohne hinterlegten Preis unterbleibt der Stripe-Teil.\n\nNur der Eigentuemer der Gruppe kommt durch; wer keine Gruppe hat, bekommt 404. Der Abrechnungsanker selbst laesst sich nicht entfernen (400), ein Mandant ausserhalb der eigenen Gruppe ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenant_id":{"type":"string","minLength":1,"maxLength":64}},"required":["tenant_id"]},"example":{"tenant_id":"string"}}}}}},"/api/v1/billing/tenants/list":{"get":{"responses":{"200":{"description":"Tenant list"},"401":{"description":"Not authenticated"}},"operationId":"getApiV1BillingTenantsList","tags":["billing","multi-tenant"],"parameters":[],"description":"Listet die Mandanten der Konto-Gruppe des angemeldeten Nutzers. Ohne Gruppe (noch kein zweiter Mandant angelegt) kommt `group: null` und der eigene Mandant als einziges Mitglied mit `is_anchor: true`, damit die Oberflaeche ihre Eigenzeile zeigen kann. Mitgeliefert werden der Plan und `aufpreis_eur` je Zusatz-Mandant (0 im Plan `free`). Ohne Nutzer- oder Mandantenkontext antwortet die Route 401.","summary":"Listet die Mandanten der Konto-Gruppe des angemeldeten Nutzers","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/tenants/webhook-sync":{"post":{"responses":{"200":{"description":"Sync applied","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenant_count":{"type":"integer"}},"required":["ok","tenant_count"]},"example":{"ok":true,"tenant_count":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"No group for subscription id"}},"operationId":"postApiV1BillingTenantsWebhook-sync","tags":["billing","multi-tenant","_internal"],"parameters":[],"summary":"Internal: reconcile tenant_count with Stripe quantity","description":"Interner Abgleich, ausgeloest vom Stripe-Ereignis `customer.subscription.updated`. Sucht die Gruppe ueber die Abonnement-Kennung des Abrechnungsankers (`organizations.stripe_subscription_id`) und schreibt den uebergebenen `tenant_count` in `public.tenant_groups` — damit der gespeicherte Zaehler nicht von der bei Stripe gebuchten Menge abweicht.\n\nDer Wert wird UNGEPRUEFT uebernommen: es wird nicht nachgezaehlt, wie viele Mitgliedschaften tatsaechlich bestehen, und weder Mandanten noch Mitgliedschaften werden angelegt oder entfernt. Gibt es zu der Abonnement-Kennung keine Gruppe, antwortet der Endpunkt mit 404 und aendert nichts. Nicht fuer Aufrufe aus der Oberflaeche gedacht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"subscription_id":{"type":"string","minLength":1,"maxLength":120},"tenant_count":{"type":"integer","minimum":1,"maximum":500}},"required":["subscription_id","tenant_count"]},"example":{"subscription_id":"string","tenant_count":1}}}}}},"/api/v1/billing/checkout":{"post":{"responses":{"200":{"description":"Checkout session created","content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string"},"url":{"type":"string"},"expiresAt":{"type":["number","null"]}},"required":["sessionId","url","expiresAt"],"additionalProperties":false},"example":{"sessionId":"string","url":"string","expiresAt":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"postApiV1BillingCheckout","tags":["billing"],"parameters":[],"description":"Create a Stripe Checkout session for the given plan. An already stored Stripe customer id is reused; a failed lookup is logged and checkout continues, because the webhook reconciles the customer later. successUrl and cancelUrl default to /settings/billing?status=success and ?status=cancelled. No subscription exists until the returned url has been completed in Stripe.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string"},"successUrl":{"type":"string","format":"uri"},"cancelUrl":{"type":"string","format":"uri"},"customerEmail":{"type":"string","format":"email"}},"required":["plan"]},"example":{"plan":"string","successUrl":"https://example.com","cancelUrl":"https://example.com","customerEmail":"beispiel@example.com"}}}},"summary":"Create a Stripe Checkout session for the given plan","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/portal":{"post":{"responses":{"200":{"description":"Portal session URL","content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string"},"url":{"type":"string"}},"required":["sessionId","url"],"additionalProperties":false},"example":{"sessionId":"string","url":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"409":{"description":"Noch kein Stripe-Kunde — zuerst Checkout laufen lassen"}},"operationId":"postApiV1BillingPortal","tags":["billing"],"parameters":[],"description":"Open a Stripe Billing Portal session for the current tenant. Needs a Stripe customer id on the organization record: without one the route answers 409 and POST /billing/checkout has to run first. returnUrl defaults to /settings/billing.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"returnUrl":{"type":"string","format":"uri"}}},"example":{"returnUrl":"https://example.com"}}}},"summary":"Open a Stripe Billing Portal session for the current tenant","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/subscribe":{"post":{"responses":{"200":{"description":"Abo erstellt — bei mode \"checkout\" muss die url noch besucht werden","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["checkout","direct"]},"url":{"type":["string","null"]},"customerId":{"type":"string"},"subscriptionId":{"type":"string"},"plan":{"type":"string"}},"required":["mode","customerId","plan"],"additionalProperties":false},"example":{"mode":"checkout","url":"string","customerId":"string","subscriptionId":"string","plan":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"postApiV1BillingSubscribe","tags":["billing"],"parameters":[],"description":"Erstellt ein neues Stripe-Abo für den Mandanten. Ein bereits hinterlegter Stripe-Kunde wird wiederverwendet; schlägt diese Suche fehl, läuft der Aufruf trotzdem weiter. Bei mode \"checkout\" besteht das Abo noch NICHT — der Nutzer muss die zurückgegebene url erst in Stripe abschließen. trialDays erlaubt 0 bis 30 Testtage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["starter","professional","enterprise"]},"customerEmail":{"type":"string","format":"email"},"trialDays":{"type":"integer","minimum":0,"maximum":30},"successUrl":{"type":"string","format":"uri"},"cancelUrl":{"type":"string","format":"uri"}},"required":["plan"]},"example":{"plan":"starter","customerEmail":"beispiel@example.com","trialDays":0,"successUrl":"https://example.com","cancelUrl":"https://example.com"}}}},"summary":"Erstellt ein neues Stripe-Abo für den Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/change-plan":{"post":{"responses":{"200":{"description":"Plan gewechselt","content":{"application/json":{"schema":{"type":"object","properties":{"subscriptionId":{"type":"string"},"fromPlan":{"type":["string","null"]},"toPlan":{"type":"string"},"kind":{"type":"string"},"prorationBehavior":{"type":"string","enum":["always_invoice","create_prorations","none"]},"status":{"type":"string"}},"required":["subscriptionId","fromPlan","toPlan","kind","prorationBehavior","status"],"additionalProperties":false},"example":{"subscriptionId":"string","fromPlan":"string","toPlan":"string","kind":"string","prorationBehavior":"always_invoice","status":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"409":{"description":"Kein aktives Abo — zuerst POST /subscribe"}},"operationId":"postApiV1BillingChange-plan","tags":["billing"],"parameters":[],"description":"Wechselt den Tarif des Stripe-Abos. Ohne hinterlegte Abo-Kennung antwortet die Route mit 409 — dann zuerst POST /billing/subscribe. Der bisherige Tarif wird aus dem Mandantenkontext übernommen, prorationBehavior steuert die anteilige Abrechnung (always_invoice, create_prorations oder none).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"toPlan":{"type":"string","enum":["starter","professional","enterprise"]},"prorationBehavior":{"type":"string","enum":["always_invoice","create_prorations","none"]}},"required":["toPlan"]},"example":{"toPlan":"starter","prorationBehavior":"always_invoice"}}}},"summary":"Wechselt den Tarif des Stripe-Abos","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/cancel":{"post":{"responses":{"200":{"description":"Abo gekündigt","content":{"application/json":{"schema":{"type":"object","properties":{"subscriptionId":{"type":"string"},"status":{"type":"string"},"cancelAtPeriodEnd":{"type":"boolean"},"canceledAt":{"type":["number","null"]}},"required":["subscriptionId","status","cancelAtPeriodEnd","canceledAt"],"additionalProperties":false},"example":{"subscriptionId":"string","status":"string","cancelAtPeriodEnd":true,"canceledAt":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"409":{"description":"Kein aktives Abo zum Kündigen"}},"operationId":"postApiV1BillingCancel","tags":["billing"],"parameters":[],"description":"Kündigt das aktive Abo des Mandanten. Ohne hinterlegte Abo-Kennung antwortet die Route mit 409. Ohne immediate läuft das Abo bis zum Periodenende weiter (cancelAtPeriodEnd). Die Rückstufung des Tarifs und das Nullsetzen des KI-Budgets geschehen nicht hier, sondern erst, wenn Stripe das zugehörige Ereignis an POST /billing/webhook liefert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"immediate":{"type":"boolean"},"reason":{"type":"string","maxLength":500}}},"example":{"immediate":true,"reason":"string"}}}},"summary":"Kündigt das aktive Abo des Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/current":{"get":{"responses":{"200":{"description":"Current plan","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"plan":{"type":"string"},"quotas":{"type":"object","properties":{"maxUsers":{"type":"number"},"maxStorageGb":{"type":"number"},"aiRequestsPerMonth":{"type":["number","null"]},"apiRpm":{"type":["number","null"]},"emailsPerMonth":{"type":["number","null"]},"maxApiCallsPerMonth":{"type":["number","null"]},"voiceMinutesPerMonth":{"type":["number","null"]}},"required":["maxUsers","maxStorageGb","aiRequestsPerMonth","maxApiCallsPerMonth"]},"period":{"type":"string"},"trialStatus":{}},"required":["tenantId","plan","quotas","period"],"additionalProperties":false},"example":{"tenantId":"string","plan":"string","quotas":{"maxUsers":0,"maxStorageGb":0,"aiRequestsPerMonth":0,"apiRpm":0,"emailsPerMonth":0,"maxApiCallsPerMonth":0,"voiceMinutesPerMonth":0},"period":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"getApiV1BillingCurrent","tags":["billing"],"parameters":[],"description":"Return the current tenant plan + quota envelope. Answered from the request tenant context and the static plan table — no Stripe round-trip, so a plan change shows up here only after the Stripe webhook has been processed. A plan value the mapper does not know falls back to starter, and trialStatus is null when the tenant is not in a trial.","summary":"Return the current tenant plan + quota envelope","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/overage/live":{"get":{"responses":{"200":{"description":"Live overage snapshot — bei Teilausfall degraded=true, aber trotzdem 200","content":{"application/json":{"schema":{"type":"object","properties":{"month":{"type":"string"},"ai_actions":{"type":"object","properties":{"used":{"type":"number"},"included":{"type":"number"},"overage":{"type":"number"},"cost_eur":{"type":"number"}},"required":["used","included","overage","cost_eur"]},"emails":{"type":"object","properties":{"used":{"type":"number"},"included":{"type":"number"},"overage":{"type":"number"},"cost_eur":{"type":"number"}},"required":["used","included","overage","cost_eur"]},"storage_gb":{"type":"object","properties":{"used":{"type":"number"},"included":{"type":"number"},"overage":{"type":"number"},"cost_eur":{"type":"number"}},"required":["used","included","overage","cost_eur"]},"seats":{"type":"object","properties":{"used":{"type":"number"},"included":{"type":"number"},"overage":{"type":"number"},"cost_eur":{"type":"number"}},"required":["used","included","overage","cost_eur"]},"api_calls":{"type":"object","properties":{"used":{"type":"number"},"included":{"type":"number"},"overage":{"type":"number"},"cost_eur":{"type":"number"}},"required":["used","included","overage","cost_eur"]},"ai_tokens_input":{"type":"object","properties":{"used":{"type":"number"},"included":{"type":"number"},"overage":{"type":"number"},"cost_eur":{"type":"number"}},"required":["used","included","overage","cost_eur"]},"ai_tokens_output":{"type":"object","properties":{"used":{"type":"number"},"included":{"type":"number"},"overage":{"type":"number"},"cost_eur":{"type":"number"}},"required":["used","included","overage","cost_eur"]},"total_overage_eur":{"type":"number"},"reported_to_stripe_at":{"type":["string","null"]},"budget_eur":{"type":"number"},"budget_used_pct":{"type":"number"},"degraded":{"type":"boolean"}},"required":["month","ai_actions","emails","storage_gb","seats","api_calls","ai_tokens_input","ai_tokens_output","total_overage_eur","reported_to_stripe_at","budget_eur","budget_used_pct","degraded"],"additionalProperties":false},"example":{"month":"string","ai_actions":{"used":0,"included":0,"overage":0,"cost_eur":0},"emails":{"used":0,"included":0,"overage":0,"cost_eur":0},"storage_gb":{"used":0,"included":0,"overage":0,"cost_eur":0},"seats":{"used":0,"included":0,"overage":0,"cost_eur":0},"api_calls":{"used":0,"included":0,"overage":0,"cost_eur":0},"ai_tokens_input":{"used":0,"included":0,"overage":0,"cost_eur":0},"ai_tokens_output":{"used":0,"included":0,"overage":0,"cost_eur":0},"total_overage_eur":0,"reported_to_stripe_at":"string","budget_eur":0,"budget_used_pct":0,"degraded":true}}}},"401":{"description":"Not authenticated"},"403":{"description":"Admin role required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"getApiV1BillingOverageLive","tags":["billing","billing-overage"],"parameters":[],"description":"Real-time overage snapshot for the tenant billing dashboard. Requires the admin or owner role and collects AI actions, e-mails, storage, seats, API calls and AI tokens together with the tenant budget and the time of the last Stripe flush. A partial outage does not fail the call: the route still answers 200 and sets degraded=true so the dashboard never blanks — read that flag before trusting the numbers.","summary":"Real-time overage snapshot for the tenant billing dashboard","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/usage/tokens/current-month":{"get":{"responses":{"200":{"description":"Token usage snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cache_read":{"type":"number"},"cache_write":{"type":"number"},"cost_eur":{"type":"number"},"degraded":{"type":"boolean"}},"required":["input","output","cache_read","cache_write","cost_eur","degraded"],"additionalProperties":false},"example":{"input":0,"output":0,"cache_read":0,"cache_write":0,"cost_eur":0,"degraded":true}}}},"401":{"description":"Not authenticated"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"getApiV1BillingUsageTokensCurrent-month","tags":["billing","ai-token-usage"],"parameters":[],"description":"Live AI token usage (Input/Output/Cache) for the current month. Read from Redis first, with a database aggregate over tenant_ai_token_usage as the fallback when Redis is unavailable; degraded=true marks a snapshot that could not be read live. The EUR cost is pre-computed, so callers do not have to price tokens themselves.","summary":"Live AI token usage (Input/Output/Cache) for the current month","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/ai-budget":{"get":{"responses":{"200":{"description":"Budget snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"budget_eur":{"type":"number"},"hard_lock":{"type":"boolean"},"overage_count":{"type":"number"},"current_cost_eur":{"type":"number"},"ratio":{"type":"number"},"ai_hard_locked":{"type":"boolean"},"ai_hard_lock_reason":{"type":["string","null"]},"degraded":{"type":"boolean"}},"required":["budget_eur","hard_lock","overage_count","current_cost_eur","ratio","ai_hard_locked","ai_hard_lock_reason","degraded"],"additionalProperties":false},"example":{"budget_eur":0,"hard_lock":true,"overage_count":0,"current_cost_eur":0,"ratio":0,"ai_hard_locked":true,"ai_hard_lock_reason":"string","degraded":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"getApiV1BillingAi-budget","tags":["billing"],"parameters":[],"description":"Current AI EUR budget + live overage usage. The ceiling and the hard-lock flag come from public.tenants; if that read fails the route still answers, using the 100 EUR default. current_cost_eur is the live overage counter multiplied by the per-action price, and ratio is that cost divided by the budget.","summary":"Current AI EUR budget + live overage usage","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Budget updated — quittiert nur den gesetzten Wert, keinen Verbrauch","content":{"application/json":{"schema":{"type":"object","properties":{"budget_eur":{"type":"number"},"hard_lock":{"type":"boolean"}},"required":["budget_eur","hard_lock"],"additionalProperties":false},"example":{"budget_eur":0,"hard_lock":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1BillingAi-budget","tags":["billing"],"parameters":[],"description":"Update the monthly EUR budget for AI overage spend. Only the six slider steps 10, 50, 100, 250, 500 and 1000 EUR are accepted; the value is written to public.tenants and recorded in the audit log under billing.ai_budget.updated. The answer echoes only what was set and carries no usage figures — use GET /billing/ai-budget for those.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"budget_eur":{"type":"number"},"hard_lock":{"type":"boolean","default":true}},"required":["budget_eur"]},"example":{"budget_eur":0,"hard_lock":true}}}},"summary":"Update the monthly EUR budget for AI overage spend","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/users/{userId}/ai-budget":{"get":{"responses":{"200":{"description":"Per-user budget snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string"},"budget_eur":{"type":["number","null"]},"user_locked":{"type":"boolean"},"user_lock_reason":{"type":["string","null"]},"spent_eur_cents":{"type":"number"},"spent_eur":{"type":"number"},"ratio":{"type":["number","null"]},"degraded":{"type":"boolean"}},"required":["user_id","budget_eur","user_locked","user_lock_reason","spent_eur_cents","spent_eur","ratio","degraded"],"additionalProperties":false},"example":{"user_id":"string","budget_eur":0,"user_locked":true,"user_lock_reason":"string","spent_eur_cents":0,"spent_eur":0,"ratio":0,"degraded":true}}}},"401":{"description":"Not authenticated"},"403":{"description":"Admin role required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}}},"operationId":"getApiV1BillingUsersByUserIdAi-budget","tags":["billing","ai-user-budget"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"description":"Read per-user AI EUR budget + this-month spend. Requires the admin or owner role and reads public.tenant_user_settings; a user without a row gets budget_eur null, which means only the tenant budget applies. The spend comes from the live per-user counter (in cents and in euro), ratio stays null while no per-user budget is set, and degraded=true marks a counter that could not be read live.","summary":"Read per-user AI EUR budget + this-month spend","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Budget updated","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string"},"budget_eur":{"type":["number","null"]},"user_locked":{"type":"boolean"}},"required":["user_id","budget_eur","user_locked"],"additionalProperties":false},"example":{"user_id":"string","budget_eur":0,"user_locked":true}}}},"401":{"description":"Not authenticated"},"403":{"description":"Admin role required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1BillingUsersByUserIdAi-budget","tags":["billing","ai-user-budget"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"description":"Set per-user AI EUR budget (admin only). null clears the budget.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"budget_eur":{"anyOf":[{"type":"number"},{"type":"null"}]},"user_locked":{"type":"boolean","default":false}},"required":["budget_eur"]},"example":{"budget_eur":0,"user_locked":true}}}},"summary":"Set per-user AI EUR budget (admin only)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/apply-promo":{"post":{"responses":{"200":{"description":"Promo applied","content":{"application/json":{"schema":{"type":"object","properties":{"subscriptionId":{"type":"string"},"appliedCode":{"type":"string"},"promotionCodeId":{"type":["string","null"]},"couponId":{"type":["string","null"]}},"required":["subscriptionId","appliedCode","promotionCodeId","couponId"],"additionalProperties":false},"example":{"subscriptionId":"string","appliedCode":"string","promotionCodeId":"string","couponId":"string"}}}},"400":{"description":"Invalid input or promo code"},"401":{"description":"Not authenticated"},"403":{"description":"Admin-Rolle erforderlich","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"required":{"type":"string"},"actual":{"type":["string","null"]},"hint":{"type":"string"}},"required":["error","code","required","actual","hint"],"additionalProperties":false}}}},"409":{"description":"No active subscription"}},"operationId":"postApiV1BillingApply-promo","tags":["billing"],"parameters":[],"description":"Apply a Stripe promo code to the active subscription. Without an active subscription the route answers 409, and a code Stripe rejects becomes 400. On success the code is mirrored into public.tenants so the dashboard can show it without a Stripe round-trip and the application is written to the audit log; a failed mirror is logged and does not undo the promo.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":64}},"required":["code"]},"example":{"code":"string"}}}},"summary":"Apply a Stripe promo code to the active subscription","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/billing/webhook":{"post":{"responses":{"200":{"description":"Event angenommen — handled=false plus reason heisst „bewusst ignoriert\"","content":{"application/json":{"schema":{"type":"object","properties":{"handled":{"type":"boolean"},"type":{"type":"string"},"eventId":{"type":"string"},"reason":{"type":"string"}},"required":["handled","type","eventId"]},"example":{"handled":true,"type":"string","eventId":"string","reason":"string"}}}},"400":{"description":"Ungültige Signatur"}},"operationId":"postApiV1BillingWebhook","tags":["billing"],"parameters":[],"description":"Empfängt Stripe-Webhook-Events (Signaturprüfung). Diese Route läuft ohne Sitzung und ohne Rollenprüfung — sie weist sich allein über den Kopf stripe-signature gegen STRIPE_WEBHOOK_SECRET aus, eine ungültige Signatur ergibt 400. Wiederholte Zustellungen fängt ein Idempotenzspeicher in Postgres ab. Ein bewusst nicht verarbeitetes Ereignis bekommt trotzdem 200 mit handled=false und einer reason, damit Stripe nicht endlos erneut zustellt; nach customer.subscription.updated gleicht die Route zusätzlich die gespeicherte Mandantenzahl der Gruppe mit der Stripe-Menge ab.","summary":"Empfängt Stripe-Webhook-Events (Signaturprüfung)","x-nemix-summary-source":"description:first-sentence","security":[]}},"/api/v1/referrals/apply":{"post":{"responses":{"200":{"description":"Es gab schon eine Empfehlung — `already_existed: true`, die bestehende kommt zurueck. Ein abweichender Code wurde verworfen.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"already_existed":{"type":"boolean"},"referral":{"type":"object","properties":{"id":{"type":"string"},"referrerTenantId":{"type":"string"},"referredTenantId":{"type":"string"},"status":{"type":"string","enum":["pending","activated","cancelled"]},"discountPct":{"type":"number"},"referrerCouponId":{"type":["string","null"]},"referredCouponId":{"type":["string","null"]},"activatedAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","referrerTenantId","referredTenantId","status","discountPct","referrerCouponId","referredCouponId","activatedAt","createdAt"]}},"required":["ok","already_existed","referral"]},"example":{"ok":true,"already_existed":true,"referral":{"id":"string","referrerTenantId":"string","referredTenantId":"string","status":"pending","discountPct":0,"referrerCouponId":"string","referredCouponId":"string","activatedAt":"string","createdAt":"string"}}}}},"201":{"description":"Empfehlung im Zustand `pending` angelegt.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"already_existed":{"type":"boolean"},"referral":{"type":"object","properties":{"id":{"type":"string"},"referrerTenantId":{"type":"string"},"referredTenantId":{"type":"string"},"status":{"type":"string","enum":["pending","activated","cancelled"]},"discountPct":{"type":"number"},"referrerCouponId":{"type":["string","null"]},"referredCouponId":{"type":["string","null"]},"activatedAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","referrerTenantId","referredTenantId","status","discountPct","referrerCouponId","referredCouponId","activatedAt","createdAt"]}},"required":["ok","already_existed","referral"]},"example":{"ok":true,"already_existed":true,"referral":{"id":"string","referrerTenantId":"string","referredTenantId":"string","status":"pending","discountPct":0,"referrerCouponId":"string","referredCouponId":"string","activatedAt":"string","createdAt":"string"}}}}},"400":{"description":"Kein `code` — oder `cannot self-refer`: der Code gehoert dem Aufrufer selbst."},"401":{"description":"Kein Mandantenkontext."},"404":{"description":"Zu diesem Code gibt es keinen Mandanten."}},"operationId":"postApiV1ReferralsApply","tags":["Empfehlungen"],"parameters":[],"summary":"Empfehlungscode eintragen (noch ohne Rabatt)","description":"Vermerkt, dass der AUFRUFENDE Mandant ueber den Code eines anderen\nMandanten gekommen ist. Angelegt wird eine Zeile im Zustand `pending`\nin `public.tenant_referrals`.\n\nHIER ENTSTEHT NOCH KEIN RABATT. Es wird nichts bei Stripe angelegt und\nnichts an einem Abonnement geaendert — das tut erst\n`POST /referrals/activate`. Der Sinn der Trennung: eine Empfehlung, die\nnie zu einem zahlenden Kunden wird, hinterlaesst keinen Gutschein bei\nStripe.\n\nDER GEWORBENE IST IMMER DER AUFRUFER. Die Kennung kommt aus der Sitzung.\n`referred_tenant_id` im Rumpf wird angenommen und IGNORIERT — es steht\nnur noch aus Kompatibilitaetsgruenden im Schema. Vor der Pruefung vom\n21.06.2026 liess es einen Mandanten Empfehlungen fuer andere eintragen\nund ausloesen.\n\n`code` nimmt beides: den Code in der Form `REF-XXXXXXXX` oder die\nMandantenkennung des Werbers, aus der der Code dann selbst gebildet\nwird.\n\nZWEIMAL AUFRUFEN LEGT NICHTS NEUES AN — und das ist mehr als\nWiederholungsschutz: hat der Mandant SCHON eine Empfehlung, kommt sie\nmit 200 und `already_existed: true` zurueck, **auch wenn ein ANDERER\nCode mitgeschickt wurde**. Der zweite Code wird stillschweigend\nverworfen; die Antwort nennt den bereits eingetragenen Werber. Ein\nWechsel des Werbers ist ueber diese Route nicht moeglich.\n\nJeder angemeldete Benutzer des Mandanten darf das — es gibt keine\nRollenpruefung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":64},"referred_tenant_id":{"type":"string","minLength":1,"maxLength":128}},"required":["code"]},"example":{"code":"string","referred_tenant_id":"string"}}}}}},"/api/v1/referrals/activate":{"post":{"responses":{"200":{"description":"Freigeschaltet — `coupon_code` nennt den Gutschein. War sie es schon, kommt `already_activated: true` ohne neuen Gutschein.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"already_activated":{"type":"boolean"},"coupon_code":{"type":"string"},"referral":{"type":"object","properties":{"id":{"type":"string"},"referrerTenantId":{"type":"string"},"referredTenantId":{"type":"string"},"status":{"type":"string","enum":["pending","activated","cancelled"]},"discountPct":{"type":"number"},"referrerCouponId":{"type":["string","null"]},"referredCouponId":{"type":["string","null"]},"activatedAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","referrerTenantId","referredTenantId","status","discountPct","referrerCouponId","referredCouponId","activatedAt","createdAt"]}},"required":["ok","already_activated","referral"]},"example":{"ok":true,"already_activated":true,"coupon_code":"string","referral":{"id":"string","referrerTenantId":"string","referredTenantId":"string","status":"pending","discountPct":0,"referrerCouponId":"string","referredCouponId":"string","activatedAt":"string","createdAt":"string"}}}}},"401":{"description":"Kein Mandantenkontext."},"404":{"description":"Fuer diesen Mandanten ist keine Empfehlung vorgemerkt."},"409":{"description":"Die Empfehlung ist weder offen noch freigeschaltet (`status=cancelled`)."},"500":{"description":"Der Gutschein liess sich bei Stripe nicht anlegen; es wurde nichts gespeichert."}},"operationId":"postApiV1ReferralsActivate","tags":["Empfehlungen"],"parameters":[],"summary":"Empfehlung freischalten — 10 % Dauerrabatt fuer beide Seiten","description":"Schaltet die vorgemerkte Empfehlung des AUFRUFENDEN Mandanten scharf.\nGedacht fuer den Uebergang Test → zahlend.\n\nWAS FREIGESCHALTET WIRD: bei Stripe entsteht ein Gutschein ueber **10 %**\nmit der Laufzeit `forever` — also dauerhaft, nicht einmalig — samt\neinloesbarem Code `REF-XXXXXXXX`. Der Gutschein wird auf BEIDE\nAbonnements gebucht: das des Werbers und das des Geworbenen. Beide\nSeiten bekommen denselben Gutschein; in der Zeile stehen deshalb unter\n`referrerCouponId` und `referredCouponId` dieselbe Kennung.\n\nFUER WEN: der Werber ist der Mandant, dessen Code beim `apply`\neingetragen wurde. Der Geworbene ist immer der Aufrufer — die Kennung\nkommt aus der Sitzung, `referred_tenant_id` im Rumpf wird angenommen und\nIGNORIERT.\n\nUMKEHRBAR IST DAS NICHT. Es gibt keinen Endpunkt, der eine Empfehlung\nzurueckdreht; den Zustand `cancelled` kennt zwar die Datenstruktur, aber\nkein Schreibpfad setzt ihn. Wer den Rabatt zuruecknehmen will, muss den\nGutschein bei Stripe entfernen.\n\nEIN FEHLGESCHLAGENES BUCHEN AUF DAS ABONNEMENT WIRD NICHT GEMELDET. Hat\neine der beiden Seiten kein laufendes Abonnement, wird sie\nuebersprungen; scheitert das Buchen, wird der Fehler nur protokolliert.\nIn beiden Faellen gilt die Empfehlung danach trotzdem als `activated`,\ndie Antwort ist `ok: true`, und ein zweiter Aufruf holt es NICHT nach —\ner antwortet `already_activated: true`. Ob der Rabatt wirklich am\nAbonnement haengt, sagt diese Antwort also nicht.\n\nScheitert dagegen das ANLEGEN des Gutscheins bei Stripe (kein Zugang,\nkein Schluessel), bricht der Aufruf mit 500 ab, bevor irgendetwas\ngespeichert wird.\n\nJeder angemeldete Benutzer des Mandanten darf das — es gibt keine\nRollenpruefung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"referred_tenant_id":{"type":"string","minLength":1,"maxLength":128}}},"example":{"referred_tenant_id":"string"}}}}}},"/api/v1/referrals/list":{"get":{"responses":{"200":{"description":"Code immer, Liste wenn abrufbar. Gesetztes `warning` heisst: die Liste fehlt, der Code stimmt.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"referral_code":{"type":"string","description":"Aus der Mandanten-Kennung gebildet."},"count":{"type":"integer","description":"0 kann auch „nicht abrufbar\" heissen — dann ist `warning` gesetzt."},"referrals":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Form aus dem Speicher, hier nicht zugesagt."},"warning":{"type":"string","description":"Nur gesetzt, wenn die Liste nicht geladen werden konnte."}},"required":["ok","referral_code","count","referrals"]},"example":{"ok":true,"referral_code":"string","count":0,"referrals":[{}],"warning":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ReferralsList","tags":["Empfehlungen"],"parameters":[],"summary":"Eigener Empfehlungs-Code und geworbene Mandanten","description":"Gibt den Empfehlungs-Code des Mandanten zurueck und die Liste derer,\ndie darueber geworben wurden.\n\nDER CODE HAENGT NICHT AN DER DATENBANK: er wird aus der\nMandanten-Kennung errechnet. Bricht die Liste weg, kommt der Code\ntrotzdem — dann mit `count: 0`, leerer Liste und gesetztem `warning`.\nGenau daran erkennt ein Aufrufer den Unterschied zwischen „noch\nniemanden geworben\" und „Liste gerade nicht abrufbar\": am Vorhandensein\nvon `warning`, nicht am Statuscode, der in beiden Faellen 200 ist.\n\nDas ist Absicht und hat einen Anlass: die Seite meldete frueher\n„Konnte den Referral-Code nicht laden: HTTP 500\", obwohl der Code\njederzeit verfuegbar war (gemeldet 06.08.2026).\n\nOhne Mandantenkontext: 401 als `text/plain`, kein JSON."}},"/api/v1/telegram/webhook":{"post":{"responses":{"200":{"description":"Update verarbeitet. `outcome.kind` sagt, was daraus wurde: `welcome`, `help`, `binding_created` (Konto verknuepft), `binding_failed`, `ai_reply` (mit Zahl der gesendeten Teile), `callback_handled` oder `ignored`. Auch `ignored` und `binding_failed` sind 200 — die Anfrage wurde entgegengenommen, nur nichts damit getan.","content":{"application/json":{"schema":{"type":"object","properties":{"handled":{"type":"boolean","const":true},"outcome":{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","const":"binding_created"},"binding":{"type":"object","properties":{"tenantSlug":{"type":"string"},"chatId":{"type":"number"},"userId":{"type":["string","null"]},"username":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["tenantSlug","chatId","userId","username","createdAt"]}},"required":["kind","binding"]},{"type":"object","properties":{"kind":{"type":"string","const":"binding_failed"},"reason":{"type":"string"}},"required":["kind","reason"]},{"type":"object","properties":{"kind":{"type":"string","const":"welcome"}},"required":["kind"]},{"type":"object","properties":{"kind":{"type":"string","const":"help"}},"required":["kind"]},{"type":"object","properties":{"kind":{"type":"string","const":"ai_reply"},"chunks":{"type":"integer"}},"required":["kind","chunks"]},{"type":"object","properties":{"kind":{"type":"string","const":"callback_handled"},"data":{"type":"string"}},"required":["kind","data"]},{"type":"object","properties":{"kind":{"type":"string","const":"ignored"},"reason":{"type":"string"}},"required":["kind","reason"]}]}},"required":["handled","outcome"]},"example":{"handled":true,"outcome":{"kind":"binding_created","binding":{"tenantSlug":"string","chatId":0,"userId":"string","username":"string","createdAt":"string"}}}}}},"400":{"description":"Rumpf ist kein JSON oder kein brauchbares Telegram-Update."},"401":{"description":"Geheimnis-Kopf fehlt oder passt nicht — auch dann, wenn `TELEGRAM_WEBHOOK_SECRET` serverseitig gar nicht gesetzt ist."},"500":{"description":"Die Verarbeitung des Updates ist gescheitert."},"502":{"description":"Der Aufruf gegen die Telegram-API ist gescheitert."}},"operationId":"postApiV1TelegramWebhook","tags":["telegram"],"parameters":[],"summary":"Telegram Bot Webhook fuer eingehende Updates verarbeiten","description":"Der Ausweis ist der Kopf `X-Telegram-Bot-Api-Secret-Token`, laufzeit-konstant verglichen mit `TELEGRAM_WEBHOOK_SECRET`. IST DIESE VARIABLE NICHT GESETZT, WIRD JEDE ANFRAGE ABGEWIESEN (401) — ein fehlendes Geheimnis gilt als falsch eingerichteter Server, nicht als „jeder darf senden\". Der Rumpf muss ein JSON-Objekt mit numerischer `update_id` und mindestens einem von `message`, `edited_message`, `callback_query` sein; sonst 400.\n\nDIESE ROUTE ANTWORTET IM FEHLERFALL NICHT MIT 200. Nachgesehen in `mapErrorToHttp`: 401 bei fehlendem oder falschem Geheimnis (bewusst — Telegram hoert dann auf zu wiederholen, statt bei einem falschen Geheimnis endlos nachzuliefern), 400 bei unbrauchbarem Rumpf, 502 wenn die Telegram-API selbst klemmt, 500 sonst. Nur der ERFOLG ist immer 200.\n\nKein Mandantenkontext: die Route haengt vor der Mandanten-Middleware, Telegram kennt unsere Mandanten nicht. Der Mandant wird erst spaeter ueber die chat-id-Bindung aufgeloest; unbekannte Chats enden in `outcome.kind: \"ignored\"` — ebenfalls mit 200.\n\nNEBENWIRKUNG: traegt die Nachricht ein Dokument oder ein Foto, wird die Datei nebenher eingezogen (`ingestTelegramDocument`) — losgeschickt und nicht abgewartet. Scheitert das Einziehen, steht das nur im Protokoll; die Antwort bleibt 200 und meldet den Fehlschlag NICHT."}},"/api/v1/whatsapp/webhook":{"get":{"responses":{"200":{"description":"Der rohe `hub.challenge`-Wert als Text zurueckgeschickt. KEIN JSON — Meta erwartet genau die Zeichenkette.","content":{"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"},"500":{"description":"Kein Verify-Token konfiguriert"}},"operationId":"getApiV1WhatsappWebhook","tags":["whatsapp"],"parameters":[],"summary":"WhatsApp Webhook-Verifizierungs-Handshake (hub.mode=subscribe)","description":"Beantwortet den einmaligen Handschlag, den Meta beim Einrichten des Webhooks im Business Manager schickt. Erwartet `hub.mode=subscribe`, ein `hub.verify_token`, das dem hinterlegten Wert entspricht, und ein nicht leeres `hub.challenge`. Stimmt eines davon nicht, kommt 403; ist serverseitig gar kein Verify-Token gesetzt, 500. Diese Route umgeht die Mandanten-Middleware — Meta kennt unsere Mandanten nicht."},"post":{"responses":{"200":{"description":"Verarbeitet — die drei Zaehler sagen, was angekommen ist","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"inbound":{"type":"number"},"statuses":{"type":"number"},"optOuts":{"type":"number"}},"required":["ok","inbound","statuses","optOuts"],"additionalProperties":false},"example":{"ok":true,"inbound":0,"statuses":0,"optOuts":0}}}},"400":{"description":"Rumpf ist kein gueltiges JSON oder kein WhatsApp-Payload"},"401":{"description":"Unauthorized — Signatur fehlt oder passt nicht"}},"operationId":"postApiV1WhatsappWebhook","tags":["whatsapp"],"parameters":[],"summary":"WhatsApp eingehende Nachrichten und Status-Updates verarbeiten","description":"Nimmt die Zustellungen von Meta entgegen. Der Rumpf wird ROH gelesen, weil die HMAC-Pruefung ihn byte-genau braucht; fehlt die Signatur oder passt sie nicht, kommt 401, bei kaputtem JSON 400. Die Antwort zaehlt, was verarbeitet wurde: `inbound` Nachrichten, `statuses` Zustellmeldungen und `optOuts` erkannte Abmeldungen. Dokumente und Bilder werden nebenher zur Ablage im DMS angestossen — scheitert das, wird es nur protokolliert und die Antwort bleibt unveraendert. Diese Route umgeht die Mandanten-Middleware."}},"/api/v1/whatsapp/send/template":{"post":{"responses":{"200":{"description":"An Meta uebergeben — `ack` ist deren unveraenderte Antwort","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"ack":{"type":"object","properties":{"messaging_product":{"type":"string"},"contacts":{"type":"array","items":{"type":"object","additionalProperties":{}}},"messages":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["messaging_product","messages"],"additionalProperties":true}},"required":["ok","ack"],"additionalProperties":false},"example":{"ok":true,"ack":{"messaging_product":"string","contacts":[{}],"messages":[{}]}}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"403":{"description":"Empfaenger ist abgemeldet — es wurde nichts gesendet"},"502":{"description":"Meta hat den Versand abgelehnt"}},"operationId":"postApiV1WhatsappSendTemplate","tags":["whatsapp"],"parameters":[],"summary":"WhatsApp Template-Nachricht an Empfaenger senden","description":"Sendet ueber eine bei Meta freigegebene Vorlage — der einzige Weg ausserhalb des 24-Stunden-Fensters. Pflicht sind `to` und `templateName`; `variables` fuellt die Platzhalter der Vorlage. Ein abgemeldeter Empfaenger wird mit 403 abgelehnt, bevor irgendetwas an Meta geht. Die Antwort reicht die Quittung von Meta unveraendert unter `ack` durch; ein Fehler auf Meta-Seite ergibt 502."}},"/api/v1/whatsapp/send/text":{"post":{"responses":{"200":{"description":"An Meta uebergeben — `ack` ist deren unveraenderte Antwort","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"ack":{"type":"object","properties":{"messaging_product":{"type":"string"},"contacts":{"type":"array","items":{"type":"object","additionalProperties":{}}},"messages":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["messaging_product","messages"],"additionalProperties":true}},"required":["ok","ack"],"additionalProperties":false},"example":{"ok":true,"ack":{"messaging_product":"string","contacts":[{}],"messages":[{}]}}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"403":{"description":"Empfaenger ist abgemeldet — es wurde nichts gesendet"},"409":{"description":"24-Stunden-Fenster ist zu — Vorlage statt Freitext nehmen"},"502":{"description":"Meta hat den Versand abgelehnt"}},"operationId":"postApiV1WhatsappSendText","tags":["whatsapp"],"parameters":[],"summary":"WhatsApp Freitext-Nachricht im 24h-Fenster senden","description":"Sendet freien Text — erlaubt nur, solange das 24-Stunden-Fenster nach der letzten Kundennachricht offen ist. Ist es zu, antwortet die Route mit 409 und verweist auf /send/template; ein abgemeldeter Empfaenger ergibt 403. Pflicht sind `to` und `body`. Die Antwort reicht die Quittung von Meta unveraendert unter `ack` durch; ein Fehler auf Meta-Seite ergibt 502."}},"/api/v1/whatsapp/opt-out":{"post":{"responses":{"200":{"description":"Abmeldung gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"`phone` fehlt"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1WhatsappOpt-out","tags":["whatsapp"],"parameters":[],"summary":"WhatsApp Empfaenger manuell auf Opt-Out setzen","description":"Traegt eine Rufnummer von Hand als abgemeldet ein; danach lehnen beide Sende-Routen sie mit 403 ab. Pflicht ist `phone`. Die Antwort ist eine reine Quittung und sagt nicht, ob die Nummer vorher schon abgemeldet war. Zuruecknehmen laesst sich eine Abmeldung ueber diese Route nicht."}},"/api/v1/onboarding/register":{"post":{"responses":{"201":{"description":"Mandant direkt angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"plan":{"type":"string","enum":["starter","professional","enterprise"]}},"required":["ok","tenantId","plan"]},"example":{"ok":true,"tenantId":"string","plan":"starter"}}}},"202":{"description":"Bereitstellung eingereiht — oder nur bestaetigt, wenn kein Repository verdrahtet ist","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"jobId":{"type":"string"},"tenantId":{"type":"string"},"plan":{"type":"string","enum":["starter","professional","enterprise"]},"statusUrl":{"type":"string"}},"required":["ok","jobId","tenantId","plan","statusUrl"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"null"},"mode":{"type":"string","const":"echo"}},"required":["ok","tenantId","mode"]}]},"example":{"ok":true,"jobId":"string","tenantId":"string","plan":"starter","statusUrl":"string"}}}},"403":{"description":"Self-service registration is closed on this deployment"},"422":{"description":"Registrierungsdaten ungueltig"}},"operationId":"postApiV1OnboardingRegister","tags":["onboarding"],"parameters":[],"summary":"Neuen Tenant inkl. Admin-Nutzer registrieren","description":"Antwortet 403 REGISTRATION_CLOSED, solange NEMIX_REGISTRATION_OPEN nicht auf true steht — siehe lib/registration-lock.ts. Der Tarifwert „pro\" wird vor dem Speichern auf „professional\" vereinheitlicht. Ist die asynchrone Bereitstellung verdrahtet, reiht der Endpunkt einen Auftrag ein und antwortet 202 mit `jobId` und `statusUrl` — das Mandanten-Schema entsteht erst danach im Hintergrund. Sonst wird der Mandant sofort angelegt (201), und ohne verdrahtetes Repository werden die Daten nur bestaetigt (202, mode=\"echo\", tenantId=null). In allen Faellen mit Mandant folgen drei Nebenwirkungen, die absichtlich NICHT scheitern duerfen: 30-Tage-Testphase starten, Standard-Kundenportal anlegen und Beispieldaten passend zur Branche einspielen — Fehler dort werden nur protokolliert. Ein ungueltiger Rumpf ergibt 422, nicht 400.","security":[]}},"/api/v1/onboarding/status/{jobId}":{"get":{"responses":{"200":{"description":"Stand des Bereitstellungsauftrags","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"status":{"type":"object","properties":{"jobId":{"type":"string"},"tenantId":{"type":"string"},"status":{"type":"string","enum":["pending","running","active","failed","dead-letter"]},"currentStep":{"type":"string","enum":["pending","creating-schema","running-migrations","activating-packs","creating-stripe","sending-welcome","active"]},"attempts":{"type":"integer"},"maxAttempts":{"type":"integer"},"progressPercent":{"type":"number"},"error":{"type":["string","null"]},"startedAt":{"type":["string","null"]},"completedAt":{"type":["string","null"]}},"required":["jobId","tenantId","status","currentStep","attempts","maxAttempts","progressPercent","error","startedAt","completedAt"]}},"required":["status"]},{"type":"object","properties":{"status":{"type":"null"},"mode":{"type":"string","const":"echo"}},"required":["status","mode"]}]},"example":{"status":{"jobId":"string","tenantId":"string","status":"pending","currentStep":"pending","attempts":0,"maxAttempts":0,"progressPercent":0,"error":"string","startedAt":"string","completedAt":"string"}}}}},"400":{"description":"jobId ungueltig"},"401":{"description":"Unauthorized"},"404":{"description":"Bereitstellungsauftrag nicht gefunden"}},"operationId":"getApiV1OnboardingStatusByJobId","tags":["onboarding"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"jobId","required":true}],"summary":"Status eines Onboarding-Jobs abrufen","description":"Poll-Endpunkt zur `jobId` aus der Registrierung. Die Momentaufnahme nennt den aktuellen Schritt (pending → creating-schema → running-migrations → activating-packs → creating-stripe → sending-welcome → active), den Fortschritt in Prozent, Versuchszahl und Hoechstversuche sowie den letzten Fehlertext. Rein lesend — der Aufruf stoeszt keinen Wiederholungsversuch an. Eine `jobId` unter 8 Zeichen ergibt 400, eine unbekannte 404. Ist die asynchrone Bereitstellung gar nicht verdrahtet, antwortet der Endpunkt 200 mit status=null und mode=\"echo\"."}},"/api/v1/onboarding/progress":{"post":{"responses":{"200":{"description":"Fortschritt uebernommen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"step":{"type":"integer"}},"required":["ok","step"]},"example":{"ok":true,"step":0}}}},"401":{"description":"Unauthorized"},"422":{"description":"Rumpf ungueltig"}},"operationId":"postApiV1OnboardingProgress","tags":["onboarding"],"parameters":[],"summary":"Onboarding-Fortschritt fuer den Nutzer setzen","description":"Legt eine Momentaufnahme des Einrichtungsassistenten ab, damit er auf einem anderen Geraet fortgesetzt werden kann. `step` ist die Schrittnummer 1…5, `snapshot` ein frei geformtes Objekt, das ungeprueft gespeichert wird. Der Aufruf ist ohne angemeldeten Mandanten erlaubt — der Assistent laeuft schon vor der Registrierung; dann faellt die Ablage ins Leere und die Antwort ist trotzdem 200. Jeder Aufruf ersetzt die vorige Momentaufnahme vollstaendig, es wird nichts zusammengefuehrt. Ein ungueltiger Rumpf ergibt 422, nicht 400."},"get":{"responses":{"200":{"description":"Zuletzt gespeicherter Fortschritt oder null","content":{"application/json":{"schema":{"type":"object","properties":{"progress":{"type":["object","null"],"properties":{"step":{"type":"integer"},"snapshot":{}},"required":["step"]}},"required":["progress"]},"example":{"progress":{"step":0}}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1OnboardingProgress","tags":["onboarding"],"parameters":[],"summary":"Onboarding-Fortschritt fuer den Nutzer abrufen","description":"Liefert die zuletzt per POST abgelegte Momentaufnahme des Assistenten zum Mandanten des Aufrufers — nicht pro Nutzer, sondern pro Mandant. `progress` ist null, wenn nie etwas abgelegt wurde. Fehlt der Mandantenkontext (Assistent vor der Registrierung), antwortet der Endpunkt ebenfalls 200 mit null; der Fortschritt liegt in dem Fall nur lokal im Browser."}},"/api/v1/onboarding/checklist":{"get":{"responses":{"200":{"description":"Stand der Checkliste","content":{"application/json":{"schema":{"type":"object","properties":{"progress":{"type":"object","additionalProperties":{}},"collapsed":{"type":"boolean"}},"required":["progress","collapsed"]},"example":{"progress":{},"collapsed":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1OnboardingChecklist","tags":["onboarding"],"parameters":[],"summary":"Onboarding-Checkliste fuer den Nutzer abrufen","description":"Liest den Stand der In-App-Checkliste aus public.tenant_user_settings — je Mandant UND Nutzer eine Zeile, anders als beim Assistenten-Fortschritt. `collapsed` sagt, ob der Nutzer die Checkliste eingeklappt hat. Der Endpunkt scheitert nie: fehlender Mandanten- oder Nutzerkontext, fehlende Datenbank, fehlende Zeile und ein Abfragefehler ergeben alle 200 mit leerem Fortschritt und collapsed=false."},"post":{"responses":{"200":{"description":"Checkliste gespeichert (oder nur bestaetigt)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"mode":{"type":"string","const":"echo"}},"required":["ok"]},"example":{"ok":true,"mode":"echo"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Rumpf ungueltig"},"500":{"description":"Speichern fehlgeschlagen"}},"operationId":"postApiV1OnboardingChecklist","tags":["onboarding"],"parameters":[],"summary":"Onboarding-Checkliste fuer den Nutzer speichern","description":"Legt die Zeile in public.tenant_user_settings an oder aktualisiert sie. Beide Felder sind einzeln optional: wird `progress` weggelassen, bleibt der gespeicherte Stand erhalten, dasselbe gilt fuer `collapsed`. Ein mitgesendetes `progress` ERSETZT den bisherigen Stand jedoch vollstaendig — einzelne Schritte werden nicht zusammengefuehrt. Ohne Mandanten-/Nutzerkontext oder ohne Datenbank antwortet der Endpunkt 200 mit mode=\"echo\", ohne etwas zu speichern. Ein ungueltiger Rumpf ergibt 422."}},"/api/v1/onboarding/sample-data":{"delete":{"responses":{"200":{"description":"Beispieldaten entfernt — Anzahl je Bereich","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"contacts":{"type":"integer"},"articles":{"type":"integer"},"invoices":{"type":"integer"},"quotes":{"type":"integer"},"total":{"type":"integer"}},"required":["ok","contacts","articles","invoices","quotes","total"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"mode":{"type":"string","const":"echo"},"deleted":{"type":"number","const":0}},"required":["ok","mode","deleted"]}]},"example":{"ok":true,"contacts":0,"articles":0,"invoices":0,"quotes":0,"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1OnboardingSample-data","tags":["onboarding"],"parameters":[],"summary":"Alle Sample-Daten (is_sample_data=true) des Tenants entfernen","description":"Loescht die beim Onboarding eingespielten Beispiel-Kontakte, -Artikel, -Rechnungen und -Angebote ENDGUELTIG aus dem Mandanten-Schema — kein Soft-Delete, kein Rueckgaengig. Betroffen sind nur die gesondert gefuehrten Beispieltabellen; echte Datensaetze bleiben unberuehrt. Die Antwort nennt die geloeschte Anzahl je Bereich und in `total` die Summe. Ohne Mandantenkontext oder ohne Datenbank antwortet der Endpunkt 200 mit mode=\"echo\" und deleted=0, ohne etwas zu loeschen. Wiederholte Aufrufe sind unschaedlich und melden dann Nullen."}},"/api/v1/onboarding/achievements":{"get":{"responses":{"200":{"description":"Katalog und erreichte Auszeichnungen","content":{"application/json":{"schema":{"type":"object","properties":{"catalogue":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"icon":{"type":"string"}},"required":["key","label","description","icon"]}},"earned":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"earnedAt":{"type":"string"}},"required":["key","earnedAt"]}}},"required":["catalogue","earned"]},"example":{"catalogue":[{"key":"string","label":"string","description":"string","icon":"string"}],"earned":[{"key":"string","earnedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1OnboardingAchievements","tags":["onboarding"],"parameters":[],"summary":"Eigene Achievement-Badges abrufen + Katalog","description":"Liefert zwei Listen: `catalogue` sind alle im Code hinterlegten Auszeichnungen mit Beschriftung, Beschreibung und Symbolnamen — unabhaengig vom Nutzer und fuer alle Mandanten gleich; `earned` sind die vom aufrufenden Nutzer bereits erreichten, mit dem Zeitpunkt in ISO-Form. Ohne Mandanten- oder Nutzerkontext bleibt `earned` leer, der Katalog kommt trotzdem."},"post":{"responses":{"200":{"description":"Auszeichnung vergeben oder bereits vorhanden","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"awarded":{"type":"boolean"},"mode":{"type":"string","const":"echo"}},"required":["ok","awarded"]},"example":{"ok":true,"awarded":true,"mode":"echo"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Unbekannter Auszeichnungsschluessel"}},"operationId":"postApiV1OnboardingAchievements","tags":["onboarding"],"parameters":[],"summary":"Eigenes Achievement-Badge manuell vergeben","description":"Vergibt eine Auszeichnung an den AUFRUFENDEN Nutzer — ein fremder Nutzer laesst sich nicht angeben. Gedacht fuer Ereignisse, die der Server nicht selbst sieht (etwa die PWA-Installation fuer „mobile_user\"); die uebrigen Auszeichnungen vergeben die Fach-Endpunkte selbst. `key` muss aus der festen Liste stammen, sonst 422. Der Aufruf ist wiederholbar: hatte der Nutzer die Auszeichnung schon, kommt 200 mit awarded=false und es wird nichts geschrieben. Ohne Mandanten-/Nutzerkontext antwortet der Endpunkt 200 mit mode=\"echo\"."}},"/api/v1/onboarding-tutor/progress":{"get":{"responses":{"200":{"description":"Der Fortschritt — ODER der leere Rueckfall, wenn die Datenbank nicht erreichbar war.","content":{"application/json":{"schema":{"type":"object","properties":{"progress":{"type":"object","properties":{"steps_completed":{"type":"array","items":{"type":"string"}},"completed_at":{"type":["string","null"]}},"required":["steps_completed","completed_at"]},"isCompleted":{"type":"boolean"}},"required":["progress","isCompleted"]},"example":{"progress":{"steps_completed":["string"],"completed_at":"string"},"isCompleted":true}}}},"401":{"description":"Kein angemeldeter Benutzer."}},"operationId":"getApiV1Onboarding-tutorProgress","tags":["onboarding"],"parameters":[],"summary":"Fortschritt im Einfuehrungsrundgang lesen","description":"Die abgeschlossenen Schritte des Anwenders und ob der Rundgang als Ganzes erledigt ist.\n\nDIESE ROUTE FAELLT BEI DATENBANKPROBLEMEN WEICH ZURUECK: keine Verbindung, kein Client oder ein Fehler in der Abfrage ergeben 200 mit LEEREM Fortschritt (`steps_completed: []`, `completed_at: null`, `isCompleted: false`) — nicht zu unterscheiden von einem Anwender, der noch nichts getan hat. Das ist hier ABSICHT und begruendet: der Rundgang soll die Seite nicht sprengen. Der Grund wird protokolliert, nicht verschluckt.\n\nDie SCHREIBENDEN Routen derselben Datei machen es ausdruecklich anders und melden 503 — ein Schreibversuch, der nicht ankam, darf nicht „success\" sagen. Diese Asymmetrie ist gewollt, nicht vergessen.\n\n`isCompleted` ist nichts weiter als `completed_at != null`.\n\nOhne Mandantenkontext dient die Benutzerkennung als Mandantenkennung — derselbe Anwender kann dadurch zwei getrennte Fortschritte haben."},"post":{"responses":{"200":{"description":"Vermerkt. Kommt auch, wenn der Schritt schon drin war.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]},"example":{"success":true}}}},"400":{"description":"Kein `step`, oder laenger als 255 Zeichen."},"401":{"description":"Kein angemeldeter Benutzer."},"503":{"description":"Nicht gespeichert — `error: \"database_unavailable\"`, `retryAfter: 5`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Onboarding-tutorProgress","tags":["onboarding"],"parameters":[],"summary":"Einen Schritt des Rundgangs als erledigt vermerken","description":"Haengt die Kennung eines Schrittes an die Liste der erledigten Schritte des Anwenders. Gibt es noch keine Zeile, wird sie angelegt.\n\nZWEIMAL DERSELBE SCHRITT AENDERT NICHTS: die Datenbank prueft vor dem Anhaengen, ob die Kennung schon in der Liste steht. Es entstehen keine Dubletten, und der zweite Aufruf meldet genauso Erfolg wie der erste. Die Antwort sagt NICHT, ob der Schritt neu war.\n\nDIE SCHRITT-KENNUNG WIRD NICHT GEPRUEFT. Jede Zeichenkette von 1 bis 255 Zeichen wird angenommen — es gibt keine Liste gueltiger Schritte, gegen die abgeglichen wird. Ein Tippfehler landet als eigener Schritt in der Liste.\n\nDER RUNDGANG WIRD DADURCH NIE FERTIG: `completed_at` bleibt unangetastet, egal wie viele Schritte zusammenkommen. Das setzt allein `POST /complete`.\n\nEin Schreibversuch, der die Datenbank nicht erreicht, meldet 503 und NICHT `success` — anders als das Lesen derselben Datei. Genau hier stand bis 28.07.2026 ein `{ success: true, mode: \"echo\" }` im Fehlerfall: der Anwender sah einen Haken, gespeichert wurde nichts.\n\nOhne Mandantenkontext dient die Benutzerkennung als Mandantenkennung — derselbe Anwender kann dadurch zwei getrennte Fortschritte haben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"step":{"type":"string","minLength":1,"maxLength":255}},"required":["step"]},"example":{"step":"string"}}}}},"delete":{"responses":{"200":{"description":"Der Loeschbefehl lief. Sagt NICHT, dass eine Zeile getroffen wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"Kein angemeldeter Benutzer."},"503":{"description":"Nicht geloescht — `error: \"database_unavailable\"`, `retryAfter: 5`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"]}}}}},"operationId":"deleteApiV1Onboarding-tutorProgress","tags":["onboarding"],"parameters":[],"summary":"Fortschritt zuruecksetzen","description":"Loescht die Zeile des Anwenders ENDGUELTIG — Schritte und Erledigt-Zeitpunkt zusammen. Danach steht der Rundgang wieder am Anfang, und `GET /progress` liefert denselben leeren Fortschritt wie bei einem neuen Anwender.\n\nDas ist zugleich der einzige Weg, ein `POST /complete` rueckgaengig zu machen — feiner geht es nicht.\n\nDIE ANTWORT SAGT NICHT, OB ES ETWAS ZU LOESCHEN GAB: `success: true` kommt auch dann, wenn der Anwender nie eine Zeile hatte. Es wird nicht geprueft, ob eine getroffen wurde.\n\nBetroffen ist nur der eigene Fortschritt (Benutzer plus Mandant), nie der anderer.\n\nOhne Mandantenkontext dient die Benutzerkennung als Mandantenkennung — derselbe Anwender kann dadurch zwei getrennte Fortschritte haben."}},"/api/v1/onboarding-tutor/complete":{"post":{"responses":{"200":{"description":"Als erledigt vermerkt.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"Kein angemeldeter Benutzer."},"503":{"description":"Nicht gespeichert — `error: \"database_unavailable\"`, `retryAfter: 5`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Onboarding-tutorComplete","tags":["onboarding"],"parameters":[],"summary":"Den Rundgang als erledigt markieren","description":"Setzt `completed_at` auf jetzt. Die abgeschlossenen SCHRITTE bleiben unangetastet — es wird nichts nachgetragen. Ein Anwender kann den Rundgang also abschliessen, ohne einen einzigen Schritt gemacht zu haben; `steps_completed` ist dann weiterhin leer, `isCompleted` trotzdem `true`.\n\nGibt es noch keine Zeile, wird sie angelegt. Ein zweiter Aufruf setzt den Zeitstempel neu und meldet wieder Erfolg.\n\nDER WEG ZURUECK IST `DELETE /progress` — das loescht die ganze Zeile und damit auch das Erledigt-Kennzeichen. Ein eigenes „doch nicht fertig\" gibt es nicht.\n\nEin Schreibversuch, der die Datenbank nicht erreicht, meldet 503 und NICHT `success` — anders als das Lesen derselben Datei.\n\nOhne Mandantenkontext dient die Benutzerkennung als Mandantenkennung — derselbe Anwender kann dadurch zwei getrennte Fortschritte haben."}},"/api/v1/usage/current":{"get":{"responses":{"200":{"description":"Usage snapshot. The four blocks do not share a naming convention — `used`, `used_bytes` and `count` are the shipped field names. `storage.limit_bytes` is always a number, so an unlimited storage plan is not expressible there.","content":{"application/json":{"schema":{"type":"object","properties":{"ai":{"type":"object","properties":{"used":{"type":"number"},"limit":{"type":["number","null"]},"resetsAt":{"type":"string"},"degraded":{"type":"boolean"}},"required":["used","limit","resetsAt","degraded"],"additionalProperties":false},"api":{"type":"object","properties":{"used":{"type":"number"},"limit_rpm":{"type":["number","null"]},"today":{"type":"number"},"rpm_current":{"type":"number"},"degraded":{"type":"boolean"}},"required":["used","limit_rpm","today","rpm_current","degraded"],"additionalProperties":false},"storage":{"type":"object","properties":{"used_bytes":{"type":"number"},"limit_bytes":{"type":"number"},"degraded":{"type":"boolean"},"cached":{"type":"boolean"}},"required":["used_bytes","limit_bytes","degraded","cached"],"additionalProperties":false},"users":{"type":"object","properties":{"count":{"type":"number"},"limit":{"type":["number","null"]}},"required":["count","limit"],"additionalProperties":false}},"required":["ai","api","storage","users"],"additionalProperties":false},"example":{"ai":{"used":0,"limit":0,"resetsAt":"string","degraded":true},"api":{"used":0,"limit_rpm":0,"today":0,"rpm_current":0,"degraded":true},"storage":{"used_bytes":0,"limit_bytes":0,"degraded":true,"cached":true},"users":{"count":0,"limit":0}}}}},"401":{"description":"Tenant required"}},"operationId":"getApiV1UsageCurrent","tags":["usage"],"parameters":[],"description":"Single-call usage dashboard — AI / API / Storage / Users vs plan quota. All four axes are read in parallel for the CURRENT tenant; there are no parameters and no way to ask about another tenant. The limits come from the plan, the numbers from Redis and the storage backend, and the user count from a separate counter that yields 0 rather than failing the request. A `limit` of `null` means unlimited, never zero. Watch `degraded` per block: where it is true the number beside it is a fallback, so a 0 means „not measured\" instead of „nothing used\". Nothing here is billed or written — it is a read.","summary":"Single-call usage dashboard — AI / API / Storage / Users vs plan quota","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ocr/extract":{"post":{"responses":{"200":{"description":"OCR extraction result. `source` names the engine that produced it. `fallback` is true when the Vision call threw and Tesseract took over.","content":{"application/json":{"schema":{"type":"object","properties":{"datum":{"type":["string","null"]},"betrag":{"type":["string","null"]},"ust":{"type":["string","null"]},"iban":{"type":["string","null"]},"source":{"type":"string","enum":["anthropic-vision","tesseract-fallback"]},"fallback":{"type":"boolean"}},"required":["datum","betrag","ust","iban","source","fallback"]},"example":{"datum":"string","betrag":"string","ust":"string","iban":"string","source":"anthropic-vision","fallback":true}}}},"400":{"description":"Invalid JSON body"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Missing image"}},"operationId":"postApiV1OcrExtract","tags":["ocr"],"parameters":[],"description":"Extract structured fields (datum/betrag/ust/iban) from a base64 image. Tries Anthropic Vision first, falls back to Tesseract.","summary":"Extract structured fields (datum/betrag/ust/iban) from a base64 image","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ocr/extract-belegtyp":{"post":{"responses":{"200":{"description":"Classify result with belegTyp, extractedFields, kontierungsvorschlag, notes. The body is the JSON of the vision model, passed through unvalidated: every key is optional and additional keys are possible. When the model answer cannot be parsed as JSON, this endpoint still answers 200 — with the fallback { belegTyp: \"sonstiges\", belegTypConfidence: 0, notes: [<reason>] }.","content":{"application/json":{"schema":{"type":"object","properties":{"belegTyp":{"type":"string"},"belegTypConfidence":{"type":"number"},"extractedFields":{"type":"object","additionalProperties":{}},"kontierungsvorschlag":{"type":"object","additionalProperties":{}},"notes":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"example":{"belegTyp":"string","belegTypConfidence":0,"extractedFields":{},"kontierungsvorschlag":{},"notes":["string"]}}}},"400":{"description":"Invalid JSON body"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Missing image or tenantId"},"500":{"description":"Vision API error"}},"operationId":"postApiV1OcrExtract-belegtyp","tags":["ocr"],"parameters":[],"summary":"Classifies a document via vision AI: type, fields and posting suggestion","description":"Classify a Beleg via Vision-AI and return belegTyp, extracted fields, and Kontierungsvorschlag. Uses ClassifyBelegTool logic."}},"/api/v1/ocr/extract-positions":{"post":{"responses":{"200":{"description":"Positions array with requiresManualReview flag. The flag is true as soon as one row has a confidence below 0.7. When the model answer cannot be parsed as a JSON array, this endpoint still answers 200 — with an empty positions array.","content":{"application/json":{"schema":{"type":"object","properties":{"positions":{"type":"array","items":{"type":"object","properties":{"position":{"type":"number"},"beschreibung":{"type":["string","null"]},"menge":{"type":["number","null"]},"einheit":{"type":["string","null"]},"einzelpreis":{"type":["string","null"]},"gesamtpreis":{"type":["string","null"]},"confidence":{"type":"number"}},"required":["position","beschreibung","menge","einheit","einzelpreis","gesamtpreis","confidence"]}},"requiresManualReview":{"type":"boolean"}},"required":["positions","requiresManualReview"]},"example":{"positions":[{"position":0,"beschreibung":"string","menge":0,"einheit":"string","einzelpreis":"string","gesamtpreis":"string","confidence":0}],"requiresManualReview":true}}}},"400":{"description":"Invalid JSON body"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Missing image"},"500":{"description":"Vision API error"}},"operationId":"postApiV1OcrExtract-positions","tags":["ocr"],"parameters":[],"description":"Extract line-item positions (table rows) from a Lieferschein or Rechnung image. Returns array of {position, beschreibung, menge, einheit, einzelpreis, gesamtpreis, confidence}.","summary":"Extract line-item positions (table rows) from a Lieferschein or Rechnung image","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ocr/extract-smart":{"post":{"responses":{"200":{"description":"OCR result with `stage` indicating which engine produced it. stage=\"text-layer\" means the PDF text layer was sufficient (no Vision call): then source=\"pdf-text-layer\", plausible=true, reasons=[] and pageCount is present. stage=\"vision\" or \"tesseract\" means the cascade escalated: then plausible=false, reasons=[\"no_plausible_text_layer\"] and pageCount is absent.","content":{"application/json":{"schema":{"type":"object","properties":{"datum":{"type":["string","null"]},"betrag":{"type":["string","null"]},"ust":{"type":["string","null"]},"iban":{"type":["string","null"]},"source":{"type":"string","enum":["pdf-text-layer","anthropic-vision","tesseract-fallback"]},"stage":{"type":"string","enum":["text-layer","vision","tesseract"]},"plausible":{"type":"boolean"},"reasons":{"type":"array","items":{"type":"string"}},"pageCount":{"type":"number"},"fallback":{"type":"boolean"}},"required":["datum","betrag","ust","iban","source","stage","plausible","reasons","fallback"]},"example":{"datum":"string","betrag":"string","ust":"string","iban":"string","source":"pdf-text-layer","stage":"text-layer","plausible":true,"reasons":["string"],"pageCount":0,"fallback":true}}}},"400":{"description":"Invalid JSON body"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Missing pdf or image input"}},"operationId":"postApiV1OcrExtract-smart","tags":["ocr"],"parameters":[],"description":"Cost-aware extraction cascade. For PDFs with a text layer (native invoices) returns parsed fields without calling Vision. Falls back to Vision then Tesseract for scans / image-only PDFs.","summary":"Cost-aware extraction cascade","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/beleg-history/record":{"post":{"responses":{"200":{"description":"Entry recorded — returns the generated row id","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string","description":"Primary key of the row just written, e.g. \"bh_m1x2y3_ab12cd3\""}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"400":{"description":"Validation error in the request body"},"401":{"description":"No tenant context in the session"},"500":{"description":"Insert into belege_history failed"},"503":{"description":"No database access — nothing was recorded"}},"operationId":"postApiV1AiBeleg-historyRecord","tags":["ai"],"parameters":[],"summary":"Record a confirmed booking into beleg history for template learning.","description":"Inserts one row into the shared table `public.belege_history`, tagged with the tenant_id taken from the authenticated session and never from the request body — otherwise a caller could write history under a foreign tenant. Table and `(tenant_id, lieferant)` index are created on demand before the insert, so the route works before any migration has run; `lieferant` and `betrag` are mandatory, the response carries the generated row id. Without a tenant context the answer is 401, without a database client 503 (`BELEG_HISTORY_RECORD_FAILED`) and a failed insert 500 — success is never reported for a row that was not written.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lieferant":{"type":"string","minLength":1},"iban":{"type":"string"},"betrag":{"type":"number"},"sollkonto":{"type":"string"},"habenkonto":{"type":"string"},"kostenstelle":{"type":"string"},"steuerschluessel":{"type":"string"},"beschreibung":{"type":"string"},"periodicity":{"type":"string","enum":["monatlich","quartalsweise","jaehrlich"]}},"required":["lieferant","betrag"]},"example":{"lieferant":"string","iban":"string","betrag":0,"sollkonto":"string","habenkonto":"string","kostenstelle":"string","steuerschluessel":"string","beschreibung":"string","periodicity":"monatlich"}}}}}},"/api/v1/ai/feedback":{"post":{"responses":{"200":{"description":"Feedback recorded","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":["string","null"],"description":"Id of the new public.ai_feedback row"},"createdAt":{"type":["string","null"],"description":"Insert timestamp as returned by the database"}},"required":["ok","id","createdAt"]},"example":{"ok":true,"id":"string","createdAt":"string"}}}},"401":{"description":"Unauthorized"},"500":{"description":"Insert failed"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiFeedback","tags":["ai"],"parameters":[],"summary":"Bewertung zu einer KI-Antwort erfassen","description":"Writes one row into public.ai_feedback for the calling tenant and user, setting the RLS tenant GUC on the same connection first. Stored are the rating (-1 or 1), the assistant text for context — `messageContent`, falling back to `messageId` and then an empty string — plus the optional correction and category. Nothing is de-duplicated: rating the same message twice leaves two rows.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"messageId":{"type":"string","minLength":1},"conversationTurnId":{"type":"string","format":"uuid"},"messageContent":{"type":"string","minLength":1},"rating":{"anyOf":[{"type":"number","const":-1},{"type":"number","const":1}]},"correction":{"type":"string","maxLength":4000},"category":{"type":"string","maxLength":120}},"required":["rating"]},"example":{"messageId":"string","conversationTurnId":"00000000-0000-4000-8000-000000000000","messageContent":"string","rating":-1,"correction":"string","category":"string"}}}}}},"/api/v1/ai/feedback/stats":{"get":{"responses":{"200":{"description":"Stats payload","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer"},"positive":{"type":"integer"},"negative":{"type":"integer"},"ratio":{"type":"number","description":"positive / total; 0 when nothing has been rated"},"byCategory":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string"},"count":{"type":"integer"}},"required":["category","count"]},"description":"The 20 most frequent categories, most frequent first"},"warning":{"type":"string","description":"Only present when there was no database connection — the counts are then all 0"}},"required":["total","positive","negative","ratio","byCategory"]},"example":{"total":0,"positive":0,"negative":0,"ratio":0,"byCategory":[{"category":"string","count":0}],"warning":"string"}}}},"401":{"description":"Unauthorized"},"500":{"description":"Aggregation query failed"}},"operationId":"getApiV1AiFeedbackStats","tags":["ai"],"parameters":[],"description":"Aggregates public.ai_feedback for the active tenant in two queries: the total plus the positive and negative counts, and separately the twenty most frequent categories (rows without a category are left out). `ratio` is the positive share and is 0 while nothing has been rated. Without a database connection the endpoint still answers 200 — all zeros plus an extra `warning` — rather than failing.","summary":"Aggregates public.ai_feedback for the active tenant in two queries","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/feedback/export-training-data":{"get":{"responses":{"200":{"description":"JSONL attachment by default, or a JSON object when ?format=json","content":{"text/plain":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"examples":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"originalMessage":{"type":["string","null"]},"correction":{"type":["string","null"]},"category":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","originalMessage","correction","category","createdAt"]}}},"required":["count","examples"]},"example":{"count":0,"examples":[{"id":"string","originalMessage":"string","correction":"string","category":"string","createdAt":"string"}]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Admin role required"},"500":{"description":"Export query failed"}},"operationId":"getApiV1AiFeedbackExport-training-data","tags":["ai"],"parameters":[],"description":"Exports only the thumbs-down rows that carry a correction — the cases where a human wrote the better answer — newest first, with `?limit=` clamped to 1..5000 (default 500). The body is JSONL by default, one fine-tuning example per line (`messages` plus a `_meta` block), served as an attachment; `?format=json` returns the same rows as a JSON object instead. Without a database connection the download is empty rather than an error.","summary":"Exports only the thumbs-down rows that carry a correction","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/tenant/ai/tools":{"get":{"responses":{"200":{"description":"Die Abweichungen des Mandanten. Leer heisst „keine\" ODER „keine Datenbank\".","content":{"application/json":{"schema":{"type":"object","properties":{"tools":{"type":"array","items":{"type":"object","properties":{"toolName":{"type":"string"},"mode":{"type":"string"}},"required":["toolName","mode"],"additionalProperties":false}}},"required":["tools"],"additionalProperties":false},"example":{"tools":[{"toolName":"string","mode":"string"}]}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TenantAiTools","tags":["tenant"],"parameters":[],"summary":"Abweichungen des Mandanten von den Standardrechten der KI-Werkzeuge","description":"Liefert NUR die Abweichungen aus `public.tenant_tool_acl` — nicht den\nKatalog der Werkzeuge. Bei einem Mandanten, der nie etwas umgestellt\nhat, ist die Liste leer, und trotzdem sind alle Werkzeuge nutzbar. Den\nKatalog liefert `GET /api/v1/tenant/ai/tools/catalog`.\n\nEINE LEERE LISTE IST KEIN BEWEIS: `getToolAcl` gibt `[]` zurueck, wenn\ngar kein Datenbank-Client da ist (`lib/ai-whitelist.ts`). Diese\nOperation antwortet dann mit 200 und `{ \"tools\": [] }` — nicht mit 503.\n„Keine Abweichungen\" und „konnte nicht nachsehen\" sehen von aussen\ngleich aus.\n\nScheitert dagegen die ABFRAGE bei vorhandenem Client, faengt das hier\nniemand ab: dann kommt der zentrale 500 aus `app.onError`."}},"/api/v1/tenant/ai/tools/catalog":{"get":{"responses":{"200":{"description":"Der ungefilterte Werkzeugkatalog des Systems.","content":{"application/json":{"schema":{"type":"object","properties":{"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"destructive":{"type":"boolean"},"requiresConfirmation":{"type":"boolean"}},"required":["name","description"],"additionalProperties":false}}},"required":["tools"],"additionalProperties":false},"example":{"tools":[{"name":"string","description":"string","destructive":true,"requiresConfirmation":true}]}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TenantAiToolsCatalog","tags":["tenant"],"parameters":[],"summary":"Katalog der KI-Werkzeuge (ungefiltert, nicht die Rechte des Aufrufers)","description":"Liefert die Werkzeuge der Stufe `active` aus der laufenden Registry —\ndie, die der Agent ueberhaupt ausfuehren kann.\n\nDER KATALOG IST NICHT GEFILTERT. `buildAgentPlannerCatalog` fragt die\nRegistry fest mit `userRole: \"admin\"` und `tenantPlan: \"enterprise\"`\nab. Die Liste zeigt also, was das SYSTEM kann — nicht, was dieser\nAufrufer, dieser Mandant oder dieser Tarif darf. Sie beruecksichtigt\nweder die Rolle des Anfragenden noch die Abweichungen aus\n`GET /api/v1/tenant/ai/tools`. Wer sie als Rechteliste anzeigt, zeigt\nzu viel.\n\nDas ist Absicht und an der Ausfuehrung abgesichert: die eigentliche\nRechtepruefung passiert beim Aufruf des Werkzeugs, nicht hier.\n\n`destructive` und `requiresConfirmation` fehlen im JSON, wenn die\nRegistry fuer das Werkzeug nichts hinterlegt hat — sie sind dann NICHT\n`false`, sondern unbekannt.\n\nDie Route ist absichtlich VOR `/{toolName}` angemeldet. Andernfalls\nwuerde Honos Matcher `catalog` als Werkzeugnamen lesen und diese\nOperation waere unerreichbar."}},"/api/v1/tenant/ai/tools/{toolName}":{"get":{"responses":{"200":{"description":"Der Modus des Werkzeugs. `inherit` heisst „keine Abweichung gefunden\".","content":{"application/json":{"schema":{"type":"object","properties":{"toolName":{"type":"string"},"mode":{"type":"string"}},"required":["toolName","mode"],"additionalProperties":false},"example":{"toolName":"string","mode":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1TenantAiToolsByToolName","tags":["tenant"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"toolName","required":true}],"summary":"Rechte-Modus eines Werkzeugs lesen (kennt unbekannte Namen nicht)","description":"Liefert den Modus, den dieser Mandant fuer das Werkzeug gesetzt hat:\n`autonomous`, `confirm`, `disabled` — oder `inherit`, wenn nichts\ngesetzt ist und der Systemstandard gilt.\n\nDIESE OPERATION ANTWORTET NIE MIT 404. `toolName` wird gegen NICHTS\ngeprueft: der Handler laedt die Abweichungen des Mandanten und sucht\ndarin. Findet er nichts, antwortet er `inherit`. Ein Tippfehler\n(`nemix_creat_customer`), ein zurueckgezogenes Werkzeug und ein\nWerkzeug ohne Abweichung liefern alle dieselbe Antwort. Wer wissen\nwill, ob das Werkzeug ueberhaupt existiert, gleicht gegen\n`GET /api/v1/tenant/ai/tools/catalog` ab.\n\nSteht kein Datenbank-Client bereit, liefert `getToolAcl` eine leere\nListe — die Antwort ist dann `inherit`, obwohl der Mandant das Werkzeug\nvielleicht auf `disabled` gestellt hat. Auch das ist ein 200."},"put":{"responses":{"200":{"description":"Der Modus wurde geschrieben. Ob sich dabei etwas geaendert hat, sagt die Antwort NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"toolName":{"type":"string"},"mode":{"type":"string","enum":["autonomous","confirm","disabled","inherit"]},"updated":{"type":"boolean","const":true}},"required":["toolName","mode","updated"],"additionalProperties":false},"example":{"toolName":"string","mode":"autonomous","updated":true}}}},"400":{"description":"Kein Mandant im Anfragekontext (`{ \"error\": \"Tenant not found\" }`, Status 400 statt 401) ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"500":{"description":"Kein Datenbank-Client oder die Abfrage ist gescheitert. Kommt aus `app.onError`, NICHT als 503."}},"operationId":"putApiV1TenantAiToolsByToolName","tags":["tenant"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"toolName","required":true}],"summary":"Rechte-Modus eines KI-Werkzeugs setzen (Name wird nicht geprueft)","description":"Schreibt fuer dieses Werkzeug eine Abweichung vom Systemstandard nach\n`public.tenant_tool_acl` und legt damit fest, wie der KI-Agent damit\numgehen darf:\n\n  · `autonomous` — laeuft ohne Rueckfrage\n  · `confirm`    — fragt vor jeder Ausfuehrung nach\n  · `disabled`   — fuer diesen Mandanten gesperrt\n  · `inherit`    — Abweichung aufheben, Systemstandard gilt wieder\n\nDer Aufruf kostet KEIN Modell-Kontingent: es wird kein Sprachmodell\nbefragt, nur eine Zeile geschrieben. Die Wirkung ist dauerhaft und\numkehrbar — derselbe Aufruf mit einem anderen `mode` stellt zurueck,\n`inherit` entfernt die Abweichung ganz.\n\nDER WERKZEUGNAME WIRD NICHT GEPRUEFT. `toolName` kommt aus dem Pfad und\nwird ungepruefte in die Datenbank geschrieben — kein Abgleich mit\n`GET /api/v1/tenant/ai/tools/catalog`, keine Registry-Suche. Ein\nTippfehler (`nemix_creat_customer`) legt eine Abweichung fuer ein\nWerkzeug an, das es nicht gibt; die Antwort ist 200 und sieht aus wie\nein Erfolg. Diese Operation antwortet NIE mit 404.\n\n`updated: true` steht fest im Handler. `setToolMode` meldet nicht\nzurueck, ob eine Zeile entstanden oder geaendert wurde — die Antwort\nunterscheidet „neu gesetzt\" und „stand schon so\" nicht.\n\nDie Antwort spiegelt `mode` aus dem RUMPF zurueck, nicht aus der\nDatenbank. Sie belegt also, was verlangt wurde, nicht, was gespeichert\nist. Den gespeicherten Stand liest\n`GET /api/v1/tenant/ai/tools/{toolName}`.\n\n`reason` wird mitgeschrieben, wenn angegeben, aber von KEINER Leseroute\ndieses Routers wieder ausgeliefert.\n\nKEIN 503 AUF DIESEM WEG — anders als bei den Nachbarrouten. Fehlt der\nDatenbank-Client, wirft `setToolMode` einen gewoehnlichen `Error`, und\nder Handler faengt nichts ab: daraus wird der zentrale 500 aus\n`app.onError`. Dasselbe gilt fuer jeden Fehler der Abfrage selbst.\n\nVerlangt mindestens die Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["autonomous","confirm","disabled","inherit"]},"reason":{"type":"string","maxLength":500}},"required":["mode"]},"example":{"mode":"autonomous","reason":"string"}}}}}},"/api/v1/tenant/ai/tools/reset":{"post":{"responses":{"200":{"description":"Die Loeschung wurde ausgefuehrt. Wie viele Zeilen betroffen waren, sagt die Antwort NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"reset":{"type":"boolean","const":true},"message":{"type":"string"}},"required":["reset","message"],"additionalProperties":false},"example":{"reset":true,"message":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1TenantAiToolsReset","tags":["tenant"],"parameters":[],"summary":"ALLE Rechte-Abweichungen des Mandanten endgueltig entfernen","description":"Loescht saemtliche Zeilen des Mandanten aus `public.tenant_tool_acl`.\nDanach gelten ueberall die Systemstandards.\n\nDAS TRIFFT AUCH SPERREN. Ein Werkzeug, das der Mandant bewusst auf\n`disabled` gestellt hat, ist danach wieder freigegeben — der Aufruf\nunterscheidet nicht zwischen „Erlaubnis erweitert\" und „Sperre\ngesetzt\". Es gibt keine Vorschau und kein Rueckgaengig; wer die\nbisherigen Werte behalten will, liest sie vorher ueber\n`GET /api/v1/tenant/ai/tools`.\n\n`reset: true` steht fest im Handler. Die Zahl der geloeschten Zeilen\nwird nicht geprueft: ein Mandant ohne jede Abweichung bekommt dieselbe\nAntwort wie einer, bei dem 40 Zeilen verschwunden sind."}},"/api/v1/ai/authority":{"get":{"responses":{"200":{"description":"Authoritative AI context — every value is resolved on the server from the session and tenant middleware; nothing here can be influenced by the request. `role` is never `api` (that case is a 403), and `permissions` is currently always the wildcard `[\"*\"]`.","content":{"application/json":{"schema":{"type":"object","properties":{"actor":{"type":"object","properties":{"id":{"type":"string"},"sessionId":{"type":"string"},"role":{"type":"string","enum":["super_admin","admin","hr_manager","accountant","manager","user"]},"permissions":{"type":"array","items":{"type":"string"}}},"required":["id","sessionId","role","permissions"],"additionalProperties":false},"tenant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"plan":{"type":"string","enum":["free","starter","professional","enterprise"]},"activePacks":{"type":"array","items":{"type":"string"}},"activeModules":{"type":"array","items":{"type":"string"}},"policyVersion":{"type":"string"}},"required":["id","slug","plan","activePacks","activeModules","policyVersion"],"additionalProperties":false}},"required":["actor","tenant"],"additionalProperties":false},"example":{"actor":{"id":"string","sessionId":"string","role":"super_admin","permissions":["string"]},"tenant":{"id":"string","slug":"string","plan":"free","activePacks":["string"],"activeModules":["string"],"policyVersion":"string"}}}}},"401":{"description":"Unauthorized (`ai_authority_unavailable`) — one of tenant, user, session or role is missing from the context."},"403":{"description":"Interactive session required (`interactive_session_required`) — the caller authenticated with an API key. Those are valid for the integration APIs but cannot authorise an interactive chat or a user-bound confirmation."}},"operationId":"getApiV1AiAuthority","tags":["ai"],"parameters":[],"summary":"Returns the authenticated actor and the tenant capability snapshot","description":"Return the authenticated actor and active tenant capability snapshot for AI policy checks."}},"/api/v1/ai/confirmation-grants":{"post":{"responses":{"201":{"description":"Confirmation grant created","content":{"application/json":{"schema":{"type":"object","properties":{"grant":{"type":"object","properties":{"id":{"type":"string","description":"Grant id — pass this back as `grantId` when consuming"},"toolId":{"type":"string"},"confirmationLevel":{"type":"string","enum":["standard","strict"]},"expiresAt":{"type":"string","description":"ISO 8601 timestamp, five minutes after creation"}},"required":["id","toolId","confirmationLevel","expiresAt"]}},"required":["grant"]},"example":{"grant":{"id":"string","toolId":"string","confirmationLevel":"standard","expiresAt":"string"}}}}},"400":{"description":"Malformed body or input the tool schema rejects"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"No interactive session, or the role may not run this tool"},"404":{"description":"Unknown tool id"},"409":{"description":"This action requires no confirmation — nothing to grant"},"503":{"description":"Grant store unavailable"}},"operationId":"postApiV1AiConfirmation-grants","tags":["ai"],"parameters":[],"summary":"Request a one-time confirmation grant for an AI mutation","description":"Takes a tool id plus its input, re-validates both server-side and stores a one-time grant that expires after five minutes. The tool must exist in the AI registry (404 otherwise), its own input schema must accept the payload (400), the caller's role must pass the RBAC check (403), and the action must actually require a confirmation — a tool that needs none is refused with 409 rather than granted. Only an interactive Better-Auth session counts: an API-key caller or an unresolved (non-UUID) tenant is rejected with 403. The preview is stored as a hash only; the canonical input stays server-owned and is never taken from the browser again."}},"/api/v1/ai/confirmation-grants/consume":{"post":{"responses":{"200":{"description":"Canonical invocation returned once","content":{"application/json":{"schema":{"type":"object","properties":{"toolId":{"type":"string"},"input":{"description":"The canonical, server-owned tool input as validated at create time"},"confirmationLevel":{"type":"string","enum":["standard","strict"]},"idempotencyKey":{"type":"string","description":"Carry this into the tool run so a retry stays one action"}},"required":["toolId","confirmationLevel","idempotencyKey"]},"example":{"toolId":"string","confirmationLevel":"standard","idempotencyKey":"string"}}}},"400":{"description":"Body without a valid grantId"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"No interactive session, or grant unknown, expired or already consumed"},"503":{"description":"Grant store unavailable"}},"operationId":"postApiV1AiConfirmation-grantsConsume","tags":["ai"],"parameters":[],"description":"Redeems a grant by id and hands back the canonical invocation exactly once. Tenant, actor and Better-Auth session must be the same as at create time; the redemption is a single atomic UPDATE, so a replay, an expired grant or any mismatch all fail closed with 403 and are indistinguishable from outside. The returned `idempotencyKey` belongs to the following tool run, so a retried run does not become a second action.","summary":"Redeems a grant by id and hands back the canonical invocation exactly once","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/tenant/ai/rules":{"get":{"responses":{"200":{"description":"Die Regeln des Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"rules":{"type":"array","items":{"type":"object","properties":{"ruleKey":{"type":"string"},"ruleValue":{},"isActive":{"type":"boolean"},"updatedAt":{"type":"string"}},"required":["ruleKey","isActive","updatedAt"],"additionalProperties":false}}},"required":["rules"],"additionalProperties":false},"example":{"rules":[{"ruleKey":"string","isActive":true,"updatedAt":"string"}]}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1TenantAiRules","tags":["tenant"],"parameters":[],"summary":"KI-Regeln des Mandanten lesen (auch die stillgelegten)","description":"Liefert die KI-Verhaltensregeln dieses Mandanten aus\n`public.tenant_ai_rules`, nach `ruleKey` sortiert.\n\nWICHTIG: die Liste enthaelt AUCH stillgelegte Regeln (`isActive: false`).\nEs wird nicht gefiltert. Die Kontext-Engine wertet spaeter nur die\naktiven aus — wer dieselbe Sicht will, filtert hier selbst.\n\n`ruleValue` ist eine JSONB-Spalte. Der Inhalt kommt unveraendert aus einer JSONB-Spalte. Es werden KEINE Feldnamen zugesagt — was heute darin steht, hat der Schreibpfad hineingelegt, nicht dieser Vertrag.\nJe nach Regel steht dort eine Zeichenkette (`\"formal\"`), eine Liste\n(`[\"run_payroll\"]`) oder ein Objekt.\n\nEs gibt KEIN `try`/`catch` um die Abfrage: schlaegt sie fehl (fehlende\nTabelle, Rechteproblem), kommt der zentrale 500 aus `app.onError` — kein\n503 und ausdruecklich keine leere Liste."}},"/api/v1/tenant/ai/rules/{ruleKey}":{"put":{"responses":{"200":{"description":"Die Regel wurde geschrieben. `ruleKey` stammt aus dem Rumpf, nicht aus dem Pfad.","content":{"application/json":{"schema":{"type":"object","properties":{"ruleKey":{"type":"string"},"ruleValue":{},"isActive":{"type":"boolean"},"updated":{"type":"boolean","const":true}},"required":["ruleKey","isActive","updated"],"additionalProperties":false},"example":{"ruleKey":"string","isActive":true,"updated":true}}}},"400":{"description":"Kein Mandant im Anfragekontext (`{ \"error\": \"Tenant not found\" }`, Status 400 statt 401) ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1TenantAiRulesByRuleKey","tags":["tenant"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"ruleKey","required":true}],"summary":"KI-Regel anlegen oder aendern (der Pfad-Parameter wird ignoriert)","description":"Legt eine KI-Verhaltensregel in `public.tenant_ai_rules` an oder\naktualisiert sie (`INSERT … ON CONFLICT DO UPDATE`). Die Regeln steuern,\nwie sich der Assistent bei diesem Mandanten verhaelt — etwa\n`language: \"formal\"` oder `blocked_tools: [\"run_payroll\"]`.\n\nDER PFAD-PARAMETER WIRD NICHT GELESEN. Der Handler nimmt `ruleKey`\nausschliesslich aus dem RUMPF. `PUT /api/v1/tenant/ai/rules/language`\nmit `{\"ruleKey\":\"greeting_name\", …}` schreibt `greeting_name` — die\nAdresse bleibt folgenlos, es gibt keinen Abgleich und keinen Fehler.\nWer den Pfad fuer massgeblich haelt, aendert die falsche Regel.\n\nDer Aufruf kostet KEIN Modell-Kontingent: es wird kein Sprachmodell\nbefragt, nur eine Zeile geschrieben. Die Wirkung ist dauerhaft und\numkehrbar — derselbe Aufruf mit anderem Wert ueberschreibt,\n`DELETE /api/v1/tenant/ai/rules/{ruleKey}` entfernt die Regel.\n\nES GIBT KEINEN VORHER-WERT IN DER ANTWORT. Ein bestehender `ruleValue`\nwird ersetzt, nicht verschmolzen; wer ihn behalten will, liest ihn\nvorher ueber `GET /api/v1/tenant/ai/rules`.\n\n`isActive` fehlt im Rumpf, gilt als `true` — eine stillgelegte Regel\nwird durch ein Update ohne dieses Feld also wieder eingeschaltet.\n\n`ruleValue` ist `unknown` und landet unveraendert in einer\nJSONB-Spalte. Der Inhalt kommt unveraendert aus einer JSONB-Spalte. Es werden KEINE Feldnamen zugesagt — was heute darin steht, hat der Schreibpfad hineingelegt, nicht dieser Vertrag.\nWelche Schluessel eine Regel tatsaechlich braucht, weiss allein die\nKontext-Engine, die sie spaeter auswertet — hier wird nichts geprueft.\nEin unbekannter `ruleKey` wird ebenso angenommen wie ein bekannter und\nbleibt danach wirkungslos in der Tabelle stehen.\n\n`updated: true` steht fest im Handler. Ob eine Zeile neu entstanden oder\neine bestehende geaendert wurde, sagt die Antwort NICHT.\n\nDIE WIRKUNG SETZT VERZOEGERT EIN: die Kontext-Engine haelt die Regeln bis\nzu 15 Minuten im Zwischenspeicher, und dieser Aufruf macht ihn nicht\naktiv ungueltig.\n\nVerlangt mindestens die Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ruleKey":{"type":"string","minLength":1,"maxLength":100},"ruleValue":{},"isActive":{"type":"boolean"}},"required":["ruleKey"]},"example":{"ruleKey":"string","isActive":true}}}}},"delete":{"responses":{"200":{"description":"Die Loeschung wurde ausgefuehrt. Ob sie eine Zeile getroffen hat, sagt die Antwort NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"ruleKey":{"type":"string"},"deleted":{"type":"boolean","const":true}},"required":["ruleKey","deleted"],"additionalProperties":false},"example":{"ruleKey":"string","deleted":true}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1TenantAiRulesByRuleKey","tags":["tenant"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"ruleKey","required":true}],"summary":"KI-Regel endgueltig loeschen (antwortet immer mit deleted: true)","description":"Entfernt die Regel endgueltig aus `public.tenant_ai_rules` — kein\nSoft-Delete, kein `deleted_at`, keine Wiederherstellung.\n\nZWEI DINGE, DIE DIE ANTWORT NICHT SAGT:\n\n1. `deleted: true` steht FEST im Handler. Die Zahl der geloeschten\n   Zeilen wird nicht geprueft. Ein unbekannter `ruleKey`, ein Tippfehler\n   und ein zweiter Aufruf derselben Loeschung liefern alle 200 mit\n   `deleted: true`. Diese Operation antwortet NIE mit 404.\n2. Wer wissen muss, ob die Regel wirklich existierte, liest vorher\n   `GET /api/v1/tenant/ai/rules`.\n\nDie Kontext-Engine haelt die Regeln bis zu 15 Minuten im Zwischen-\nspeicher. Die Loeschung wirkt auf den Chat also verzoegert; aktiv\nungueltig gemacht wird der Zwischenspeicher nicht."}},"/api/v1/tenant/ai/settings":{"get":{"responses":{"200":{"description":"Drei Einstellungsgruppen. Leerstaende koennen verschluckte Fehler sein.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"personality":{"type":"object","properties":{"prefix":{"type":"string"},"suffix":{"type":"string"}},"additionalProperties":false},"budget":{"type":["object","null"],"properties":{"monthlyLimitUsd":{"type":"number"},"alertAtPct":{"type":"number"},"hardLimit":{"type":"boolean"},"currentMonthUsd":{"type":"number"}},"required":["monthlyLimitUsd","alertAtPct","hardLimit","currentMonthUsd"],"additionalProperties":false},"toolAclOverrides":{"type":"number"},"note":{"type":"string"}},"required":["tenantId","personality","budget","toolAclOverrides","note"],"additionalProperties":false},"example":{"tenantId":"string","personality":{"prefix":"string","suffix":"string"},"budget":{"monthlyLimitUsd":0,"alertAtPct":0,"hardLimit":true,"currentMonthUsd":0},"toolAclOverrides":0,"note":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1TenantAiSettings","tags":["tenant"],"parameters":[],"summary":"Sammelansicht der KI-Einstellungen (drei Gruppen, nicht alle)","description":"Fasst zusammen, was sich in EINER Abfrage lesen laesst:\n\n  · `personality` — Vor- und Nachtext des System-Prompts\n  · `budget`      — Grenze und laufender Monatsverbrauch, oder `null`\n  · `toolAclOverrides` — die ANZAHL der Rechte-Abweichungen\n\nWAS HIER NICHT DRINSTEHT, obwohl der Name es nahelegt: die\nHintergrund-Agenten (`GET /settings/agents`), die Werkzeug-Rechte selbst\n(`GET /tenant/ai/tools`) und die Regeln (`GET /tenant/ai/rules`).\n`toolAclOverrides` ist eine Zahl, keine Liste.\n\nDREI STILLE RUECKFAELLE — der wichtigste Vorbehalt dieser Operation:\njede der drei Abfragen steht in einem eigenen `catch`, das den Fehler\nverschluckt und den Wert auf seinen Leerstand setzt. Nach aussen ist\ndas nicht zu unterscheiden:\n\n  · `personality: {}` — nichts gesetzt ODER `layer_template` unlesbar\n  · `budget: null`    — kein Budget ODER `tenant_ai_budgets` unlesbar\n  · `toolAclOverrides: 0` — keine Abweichung ODER Abfrage gescheitert\n\nDie Antwort ist in allen Faellen 200. Wer sicher wissen muss, ob ein\nWert wirklich fehlt, fragt die Einzelrouten — die antworten bei einem\nFehler mit einem Fehlerstatus."}},"/api/v1/tenant/ai/settings/personality":{"put":{"responses":{"200":{"description":"Geschrieben. `prefix`/`suffix` sind aus dem Rumpf zurueckgespiegelt.","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"boolean","const":true},"prefix":{"type":"string"},"suffix":{"type":"string"},"note":{"type":"string"}},"required":["updated","note"],"additionalProperties":false},"example":{"updated":true,"prefix":"string","suffix":"string","note":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext (`{ \"error\": \"Tenant not found\" }`, Status 400 statt 401) ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"422":{"description":"Der Mandant hat keinen Ebenen-Knoten (`tenants.layer_id` ist leer). Es wurde NICHTS geschrieben.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant has no layer node configured. Contact your Nemix administrator."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1TenantAiSettingsPersonality","tags":["tenant"],"parameters":[],"summary":"Vor- und Nachtext des System-Prompts setzen (ersetzt beide, verschmilzt nicht)","description":"Schreibt den mandanteneigenen Vor- und Nachtext des System-Prompts nach\n`public.layer_template` (`kind = \"ai.system-prompt\"`) am Ebenen-Knoten\ndes Mandanten. Der Text umgibt danach den Grund-Prompt bei JEDEM\nGespraech dieses Mandanten.\n\nDer Aufruf selbst kostet KEIN Modell-Kontingent — es wird kein\nSprachmodell befragt, nur eine Zeile geschrieben. Wer den Text vorher\nausprobieren will, nimmt\n`POST /api/v1/tenant/ai/settings/test-prompt`; DER Aufruf befragt\nwirklich ein Modell.\n\nDER RUMPF ERSETZT DEN GANZEN EINTRAG. Der Handler baut ein neues\nNutzlast-Objekt aus genau den Feldern, die mitkommen, und schreibt es\nper `ON CONFLICT DO UPDATE SET payload = EXCLUDED.payload`. Wer nur\n`prefix` schickt, LOESCHT damit einen zuvor gespeicherten `suffix` —\nobwohl beide Felder einzeln optional sind. Ein leerer Rumpf `{}` loescht\nbeide. Den bisherigen Stand liest man vorher ueber\n`GET /api/v1/tenant/ai/settings` im Block `personality`.\n\nDie Wirkung ist dauerhaft und umkehrbar: derselbe Aufruf mit anderem\nText ueberschreibt. Einen Verlauf gibt es nicht — der vorherige Text ist\nnach dem Schreiben weg.\n\nDER MANDANT BRAUCHT EINEN EBENEN-KNOTEN. Fehlt `tenants.layer_id`,\nantwortet die Operation mit 422 und schreibt NICHTS. Das ist keine\nEingabefrage, sondern eine Einrichtungsfrage; wiederholen hilft nicht.\n\nDer Zwischenspeicher des Prompts wird fuer diesen Mandanten aktiv\nverworfen (`invalidatePromptCache`). Laufende Gespraeche behalten ihren\nPrompt trotzdem — die Antwort sagt das im Feld `note`.\n\nUM DIE ABFRAGEN STEHT KEIN `try`/`catch`: scheitert das Lesen des\nEbenen-Knotens oder das Schreiben, kommt der zentrale 500 aus\n`app.onError` — kein 503.\n\nVerlangt mindestens die Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"prefix":{"type":"string","maxLength":2000},"suffix":{"type":"string","maxLength":2000}}},"example":{"prefix":"string","suffix":"string"}}}}}},"/api/v1/tenant/ai/settings/agents":{"get":{"responses":{"200":{"description":"Konfigurierte Agenten (snake_case) ODER fuenf Standardeintraege (camelCase).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"agents":{"type":"array","items":{"type":"object","properties":{"agent_type":{"type":"string"},"enabled":{"type":"boolean"},"cron_pattern":{"type":["string","null"]},"settings":{},"updated_at":{"type":"string"}},"required":["agent_type","enabled","cron_pattern","updated_at"],"additionalProperties":false}}},"required":["agents"],"additionalProperties":false},{"type":"object","properties":{"agents":{"type":"array","items":{"type":"object","properties":{"agentType":{"type":"string"},"enabled":{"type":"boolean"},"cronPattern":{"type":["string","null"]},"settings":{"type":"object","additionalProperties":{}}},"required":["agentType","enabled","cronPattern","settings"],"additionalProperties":false}}},"required":["agents"],"additionalProperties":false}]},"example":{"agents":[{"agent_type":"string","enabled":true,"cron_pattern":"string","updated_at":"string"}]}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1TenantAiSettingsAgents","tags":["tenant"],"parameters":[],"summary":"Hintergrund-Agenten des Mandanten (zwei unterschiedliche Antwortformen)","description":"Liefert die Konfiguration der Hintergrund-Agenten aus\n`public.tenant_ai_agent_configs`.\n\nZWEI FORMEN UNTER EINEM 200 — das ist der wichtigste Punkt hier:\n\n  · Gelingt die Abfrage, kommen die Zeilen ROH aus der Datenbank, also\n    in snake_case: `agent_type`, `cron_pattern`, `updated_at`. Nur die\n    Agenten, die wirklich konfiguriert wurden.\n  · Scheitert sie, faengt ein `catch` sie ab und liefert fuenf\n    Standard-Eintraege in camelCase: `agentType`, `cronPattern` — und\n    OHNE `updated_at`.\n\nEin Aufrufer, der nur `agentType` liest, sieht bei funktionierender\nDatenbank nichts; einer, der nur `agent_type` liest, sieht im\nRueckfall nichts. Beide Formen abfangen.\n\nDER RUECKFALL VERSCHLUCKT JEDEN FEHLER, nicht nur die fehlende Tabelle,\nauf die sein Kommentar zeigt. Bei einem Rechteproblem oder einem\nDatenbankausfall liest ein Aufrufer fuenf abgeschaltete Agenten und\nhaelt das fuer eine Aussage. Der Status ist auch dann 200.\n\n`settings` ist eine JSONB-Spalte. Der Inhalt kommt unveraendert aus einer JSONB-Spalte. Es werden KEINE Feldnamen zugesagt — was heute darin steht, hat der Schreibpfad hineingelegt, nicht dieser Vertrag."}},"/api/v1/tenant/ai/settings/agents/{type}":{"put":{"responses":{"200":{"description":"Der Aufruf ist durchgelaufen. Das ist KEIN Beleg dafuer, dass geschrieben wurde — siehe Beschreibung.","content":{"application/json":{"schema":{"type":"object","properties":{"agentType":{"type":"string"},"enabled":{"type":"boolean"},"updated":{"type":"boolean","const":true}},"required":["agentType","enabled","updated"],"additionalProperties":false},"example":{"agentType":"string","enabled":true,"updated":true}}}},"400":{"description":"Kein Mandant im Anfragekontext, ODER unbekannte Agentenart (`Unknown agent type: …`), ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1TenantAiSettingsAgentsByType","tags":["tenant"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"type","required":true}],"summary":"Hintergrund-Agenten einstellen (updated: true auch ohne Schreibvorgang)","description":"Schaltet einen Hintergrund-Agenten dieses Mandanten ein oder aus und\nlegt Zeitplan und Einstellungen fest. Geschrieben wird nach\n`public.tenant_ai_agent_configs` (`INSERT … ON CONFLICT DO UPDATE`).\n\nDer Aufruf selbst kostet KEIN Modell-Kontingent: es wird nur\nkonfiguriert. Ein eingeschalteter Agent laeuft aber SPAETER von selbst\nund verbraucht dann Kontingent, ohne dass jemand ihn erneut anstoesst —\ndas ist die eigentliche Folge dieses Aufrufs. Umkehrbar durch denselben\nAufruf mit `enabled: false`.\n\n`updated: true` IST KEIN SCHREIBBELEG. Das `INSERT` steht in einem\n`try`/`catch`, dessen `catch` nichts weiterreicht: es schreibt eine\nZeile ins Server-Protokoll und faellt durch zur Erfolgsantwort. Fehlt\ndie Tabelle, fehlen Rechte oder bricht die Verbindung mitten im Schreiben\nab, antwortet die Operation trotzdem mit 200 und `updated: true` —\ngespeichert ist dann nichts. Wer sicher sein muss, liest danach\n`GET /api/v1/tenant/ai/settings/agents` und prueft, ob der Agent dort in\nder snake_case-Form (also aus der Datenbank) auftaucht.\n\nDER RUMPF ERSETZT DIE GANZE ZEILE. `cronPattern` ohne Angabe wird `null`,\n`settings` ohne Angabe wird `{}` — beides ueberschreibt vorhandene Werte,\nstatt sie stehen zu lassen. Ein Aufruf, der nur `enabled` umschaltet,\nloescht damit den eingestellten Zeitplan.\n\n`type` muss einer der fuenf bekannten Agenten sein (`daily-briefing`,\n`dunning-check`, `stock-monitor`, `cashflow-alert`, `monthly-report`);\nsonst 400 mit `Unknown agent type: …`. Das ist die EINZIGE Pruefung des\nPfad-Parameters.\n\n`cronPattern` wird NICHT auf Gueltigkeit geprueft — jede Zeichenkette bis\n100 Zeichen wird angenommen. `recipients` prueft das Schema zwar als\nE-Mail-Adressen, der Handler schreibt das Feld aber gar nicht: es faellt\nersatzlos weg und ist danach nirgends zu finden.\n\n`settings` landet unveraendert in einer JSONB-Spalte. Der Inhalt kommt unveraendert aus einer JSONB-Spalte. Es werden KEINE Feldnamen zugesagt — was heute darin steht, hat der Schreibpfad hineingelegt, nicht dieser Vertrag.\n\nVerlangt mindestens die Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"cronPattern":{"type":"string","maxLength":100},"recipients":{"type":"array","items":{"type":"string","format":"email"},"maxItems":20},"settings":{"type":"object","additionalProperties":{}}},"required":["enabled"]},"example":{"enabled":true,"cronPattern":"string","recipients":["beispiel@example.com"],"settings":{}}}}}}},"/api/v1/tenant/ai/settings/budget":{"get":{"responses":{"200":{"description":"Gespeichertes Budget (mit `updatedAt`) ODER erfundener Standard (mit `note`).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"monthlyLimitUsd":{"type":"number"},"alertAtPct":{"type":"number"},"hardLimit":{"type":"boolean"},"currentMonthUsd":{"type":"number"},"usagePct":{"type":"number"},"updatedAt":{"type":"string"}},"required":["monthlyLimitUsd","alertAtPct","hardLimit","currentMonthUsd","usagePct","updatedAt"],"additionalProperties":false},{"type":"object","properties":{"monthlyLimitUsd":{"type":"number"},"alertAtPct":{"type":"number"},"hardLimit":{"type":"boolean"},"currentMonthUsd":{"type":"number"},"usagePct":{"type":"number"},"note":{"type":"string"}},"required":["monthlyLimitUsd","alertAtPct","hardLimit","currentMonthUsd","usagePct","note"],"additionalProperties":false}]},"example":{"monthlyLimitUsd":0,"alertAtPct":0,"hardLimit":true,"currentMonthUsd":0,"usagePct":0,"updatedAt":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Die Abfrage ist gescheitert. Der Text nennt eine Migration, gilt aber fuer jeden Fehler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Budget table not available — run Wave A10 migration"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1TenantAiSettingsBudget","tags":["tenant"],"parameters":[],"summary":"KI-Budget und Monatsverbrauch (Standardwerte sind erfunden, nicht gespeichert)","description":"Liefert die Budgetgrenze und den laufenden Monatsverbrauch aus\n`public.tenant_ai_budgets`.\n\nZWEI FORMEN UNTER EINEM 200:\n\n  · Gibt es eine Zeile, traegt die Antwort `updatedAt`.\n  · Gibt es KEINE, antwortet der Handler mit fest verdrahteten Werten\n    (500 USD Monatsgrenze, Warnung bei 80 %, keine harte Grenze) und\n    setzt stattdessen `note`. Diese Zahlen stehen NICHT in der\n    Datenbank — sie stehen im Quelltext dieser Route. Ein Mandant, der\n    nie ein Budget gesetzt hat, ist dadurch nicht auf 500 USD begrenzt.\n\n`note` ist der einzige verlaessliche Unterscheider. Ist es da, ist\nnichts gespeichert.\n\n`usagePct` ist auf ganze Prozent gerundet und wird 0, wenn die Grenze 0\nist — nicht „unendlich\" und kein Fehler.\n\nDer 503 nennt eine Migration. Er kommt aber aus einem `catch`, das\nJEDEN Fehler der Abfrage abfaengt: auch ein Rechteproblem oder ein\nVerbindungsabbruch antwortet mit diesem Text."},"put":{"responses":{"200":{"description":"Das Budget wurde geschrieben. Die Werte stammen aus dem Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"monthlyLimitUsd":{"type":"number"},"alertAtPct":{"type":"number"},"hardLimit":{"type":"boolean"},"updated":{"type":"boolean","const":true}},"required":["monthlyLimitUsd","alertAtPct","hardLimit","updated"],"additionalProperties":false},"example":{"monthlyLimitUsd":0,"alertAtPct":0,"hardLimit":true,"updated":true}}}},"400":{"description":"Kein Mandant im Anfragekontext (`{ \"error\": \"Tenant not found\" }`, Status 400 statt 401) ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client — ODER das Schreiben ist gescheitert (Text nennt eine Migration, gilt aber fuer jeden Fehler). In beiden Faellen wurde nichts gespeichert.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"Budget table not available — run Wave A10 migration"}},"required":["error"],"additionalProperties":false}]}}}}},"operationId":"putApiV1TenantAiSettingsBudget","tags":["tenant"],"parameters":[],"summary":"Monatliches KI-Budget des Mandanten setzen","description":"Schreibt Monatsgrenze, Warnschwelle und die Frage „harte Grenze\" nach\n`public.tenant_ai_budgets` (`INSERT … ON CONFLICT DO UPDATE`).\n\nDer Aufruf selbst kostet KEIN Modell-Kontingent. Er ist dauerhaft und\numkehrbar: derselbe Aufruf mit anderen Zahlen ueberschreibt. Ein\nLoeschen der Grenze gibt es nicht — ohne Zeile liest\n`GET /api/v1/tenant/ai/settings/budget` einen erfundenen Standardwert,\neinmal geschrieben bleibt die Zeile.\n\nDER VERBRAUCHSZAEHLER WIRD NICHT ZURUECKGESETZT. Beim ERSTEN Anlegen\nsteht `current_month_usd` auf 0; das `DO UPDATE` fasst die Spalte\ndanach nicht mehr an. Ein neues Budget setzt den laufenden Monat also\nnicht auf null — wer das erwartet, sieht die Warnschwelle sofort wieder\nerreicht.\n\nACHTUNG — `hardLimit` SPERRT NICHTS (nachgemessen 30.08.2026).\nDas Feld wird ausschliesslich\nGESCHRIEBEN und in Auskuenften wieder ANGEZEIGT. Keine Stelle im\nSystem liest sie, um einen Modellaufruf abzuweisen. Wer sie auf `true`\nsetzt, hat NICHT gedeckelt.\n\nDie wirksame Sperre haengt an einem ANDEREN Budget:\n`tenants.ai_monthly_budget_eur`, gegen den ein Cron alle 15 Minuten\nprueft (`jobs/ai-hard-cap-cron.ts`); ueberschritten setzt er\n`tenants.ai_hard_locked`, und erst daraufhin antwortet die Middleware\nmit 402. Diese Route beruehrt jenes Budget NICHT.\n\nALLE DREI FELDER WERDEN GESCHRIEBEN, auch die nicht mitgeschickten:\n`alertAtPct` faellt ohne Angabe auf 80, `hardLimit` auf `false`. Das\nSchema setzt die Standardwerte, bevor der Handler sie sieht — eine\nzuvor gesetzte harte Grenze wird von einem Aufruf, der nur\n`monthlyLimitUsd` schickt, still aufgehoben (was folgenlos ist, siehe\noben).\n\n`monthlyLimitUsd` ist in US-DOLLAR (1 bis 100.000), passend zur Spalte\n`monthly_limit_usd`. Es wird nicht umgerechnet.\n\nDIESES BUDGET SPERRT NICHTS — auch nicht mit `hardLimit: true`. Der Wert\nwird geschrieben und wieder ausgeliefert, aber keine Stelle im System\nliest `hardLimit`, um einen Aufruf abzuweisen. Was\naus `monthly_limit_usd` wirklich folgt, ist eine Protokollzeile: die\nKostenerfassung vergleicht den Monatswert gegen `alert_at_pct` und\nschreibt bei Ueberschreitung eine Warnung ins Server-Protokoll. Mehr\nnicht.\n\nDIE ECHTE SPERRE HAENGT AN EINEM ANDEREN BUDGET, das ueber diese API\nnicht zu setzen ist. Der Sperr-Lauf (alle 15 Minuten) vergleicht\n`public.tenants.ai_monthly_budget_eur` (Standard 100 EUR) gegen einen\naus dem Redis-Zaehler hochgerechneten Betrag und setzt bei 100 Prozent\n`tenants.ai_hard_locked`; die Middleware antwortet danach mit 402. Er\nliest `public.tenant_ai_budgets` NICHT — weder die Grenze noch\n`hard_limit`, und auch nicht denselben Verbrauchswert. Wer hier eine\nharte Grenze setzt und glaubt, damit die Ausgaben gedeckelt zu haben,\nirrt.\n\n`updated: true` steht fest im Handler. Ob eine Zeile neu entstanden oder\ngeaendert wurde, sagt die Antwort NICHT — und die Antwort spiegelt die\nWerte aus dem Rumpf zurueck, nicht den gelesenen Stand.\n\nDER 503 NENNT EINE MIGRATION, gilt aber fuer jeden Fehler des Schreibens:\nder `catch` unterscheidet nicht zwischen fehlender Tabelle,\nRechteproblem und Verbindungsabbruch. In diesem Fall wurde NICHTS\ngespeichert — anders als bei `/settings/agents/{type}`, das denselben\nFall zu einem 200 glaettet.\n\nVerlangt mindestens die Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"monthlyLimitUsd":{"type":"number","minimum":1,"maximum":100000},"alertAtPct":{"type":"number","minimum":10,"maximum":100,"default":80},"hardLimit":{"type":"boolean","default":false}},"required":["monthlyLimitUsd"]},"example":{"monthlyLimitUsd":1,"alertAtPct":10,"hardLimit":true}}}}}},"/api/v1/tenant/ai/settings/test-prompt":{"post":{"responses":{"200":{"description":"Der zusammengesetzte Prompt und die Antwort des Modells. Nichts wurde gespeichert.","content":{"application/json":{"schema":{"type":"object","properties":{"testMessage":{"type":"string"},"systemPrompt":{"type":"string"},"response":{"type":"string"},"note":{"type":"string"}},"required":["testMessage","systemPrompt","response","note"],"additionalProperties":false},"example":{"testMessage":"string","systemPrompt":"string","response":"string","note":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext (`{ \"error\": \"Tenant not found\" }`, Status 400 statt 401) ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"500":{"description":"Der Modellaufruf ist gescheitert — auch, wenn kein Anbieter eingerichtet ist. `details` traegt den rohen Fehlertext und ist kein Vertrag.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"LLM call failed in sandbox mode"},"details":{"type":"string"}},"required":["error","details"],"additionalProperties":false}}}}},"operationId":"postApiV1TenantAiSettingsTest-prompt","tags":["tenant"],"parameters":[],"summary":"System-Prompt ausprobieren (echter Modellaufruf, nicht in der Kostenerfassung)","description":"Setzt aus `prefix`, dem festen Grund-Prompt und `suffix` einen\nSystem-Prompt zusammen, schickt `testMessage` damit an ein Sprachmodell\nund gibt Prompt und Antwort zurueck. Gedacht zum Ausprobieren, bevor man\n`PUT /api/v1/tenant/ai/settings/personality` benutzt.\n\nDER MODELLAUFRUF IST ECHT UND KOSTET GELD. „Sandbox\" bezieht sich allein\ndarauf, dass NICHTS gespeichert wird: weder der Prompt noch die Antwort,\nund die Einstellungen des Mandanten bleiben unveraendert. Insofern ist\nder Aufruf folgenlos und braucht kein Rueckgaengig.\n\nER TAUCHT ABER IN KEINER ABRECHNUNG AUF. `callLLM` erfasst Kosten und\nwendet die Guardrails nur an, wenn eine Mandantenkennung als `tenantId`\nankommt; dieser Aufruf uebergibt sie im Feld `userId` und laesst\n`tenantId` leer. Folgen:\n\n  · Es entsteht KEINE Zeile in `public.ai_cost_events` — der Aufruf ist\n    in `GET /api/v1/tenant/ai/costs` und in\n    `GET /api/v1/tenant/ai/audit` unsichtbar.\n  · Der Monatszaehler in `public.tenant_ai_budgets` steigt NICHT. (Der\n    haelt ohnehin niemanden auf — siehe\n    `PUT /api/v1/tenant/ai/settings/budget`.)\n  · Die Guardrails greifen nicht: weder wird der eingegebene Text vor\n    dem Senden maskiert, noch die Antwort danach. Wer personenbezogene\n    Daten in `testMessage` schreibt, schickt sie ungefiltert an den\n    Anbieter.\n\nDas Modell ist die guenstige Stufe (`task: \"fast\"`), die Antwort auf 300\nToken begrenzt — eine laengere Antwort wird abgeschnitten, ohne dass die\nAntwort es sagt.\n\nDER GRUND-PROMPT HIER IST EINE ATTRAPPE. Der Handler setzt fest „Du bist\nWilli, ein hilfreicher ERP-Assistent.\" zwischen Vor- und Nachtext. Das\nist NICHT der Prompt, den der Assistent im Betrieb benutzt, und der\nERP-Kontext des Mandanten fehlt vollstaendig. Das Ergebnis zeigt die\nWirkung der beiden Texte, nicht die spaetere Antwort des Systems.\n\nLeere und nur aus Leerzeichen bestehende Texte werden verworfen, nicht\neingefuegt.\n\nScheitert der Modellaufruf, kommt 500 mit dem ROHEN Fehlertext in\n`details` — auch dann, wenn gar kein Anbieter eingerichtet ist.\n\nVerlangt mindestens die Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"prefix":{"type":"string","maxLength":2000},"suffix":{"type":"string","maxLength":2000},"testMessage":{"type":"string","minLength":1,"maxLength":500}},"required":["testMessage"]},"example":{"prefix":"string","suffix":"string","testMessage":"string"}}}}}},"/api/v1/tenant/ai/costs":{"get":{"responses":{"200":{"description":"Die Kostenaufschluesselung in US-Dollar — ODER der Rueckfall mit `note`, wenn die Abfrage scheiterte.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"month":{"type":"string"},"totalUsd":{"type":"number"},"breakdown":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"},"taskType":{"type":["string","null"]},"requestCount":{"type":"number"},"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"costUsd":{"type":"number"}},"required":["modelId","taskType","requestCount","inputTokens","outputTokens","costUsd"],"additionalProperties":false}}},"required":["month","totalUsd","breakdown"],"additionalProperties":false},{"type":"object","properties":{"month":{"type":"string"},"totalUsd":{"type":"number","const":0},"breakdown":{"type":"array","items":{},"maxItems":0},"note":{"type":"string","const":"ai_cost_events not available yet"}},"required":["month","totalUsd","breakdown","note"],"additionalProperties":false}]},"example":{"month":"string","totalUsd":0,"breakdown":[{"modelId":"string","taskType":"string","requestCount":0,"inputTokens":0,"outputTokens":0,"costUsd":0}]}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1TenantAiCosts","tags":["tenant"],"parameters":[{"in":"query","name":"month","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$"}}],"summary":"KI-Kosten je Modell und Aufgabenart (0 kann „nicht messbar\" heissen)","description":"Liest die Kosten eines Kalendermonats aus `public.ai_cost_events`,\ngruppiert nach Modell und Aufgabenart, teuerste Gruppe zuerst. Reine\nLeseoperation: es wird nichts geschrieben und KEIN Modell-Kontingent\nverbraucht — der Aufruf selbst kostet nichts.\n\nOhne `month` gilt der LAUFENDE Monat nach UTC-Systemzeit. Der Monat wird\nals halboffener Bereich abgefragt (`>= Monatserster`, `< Folgemonat`).\n\nDIE BETRAEGE SIND US-DOLLAR, nicht Euro: `totalUsd` und `costUsd` kommen\nunveraendert aus der Spalte `cost_usd`. Es wird NICHT umgerechnet.\n\nEINE 0 IST KEIN BEWEIS. Scheitert die Abfrage — fehlende Tabelle,\nRechteproblem, Verbindungsabbruch —, faengt ein `catch` sie ab und\nantwortet mit 200, `totalUsd: 0`, leerer Aufschluesselung und dem Feld\n`note`. Ein Mandant ohne KI-Nutzung und eine kaputte Messung liefern\ndieselbe Zahl; allein die Anwesenheit von `note` trennt sie.\n\nDIESE ANSICHT IST NICHT DAS BUDGET.\n`GET /api/v1/tenant/ai/settings/budget` liest einen eigenen,\nfortgeschriebenen Zaehler. Beide zaehlen denselben Verbrauch auf zwei\nWegen und koennen auseinanderlaufen.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten sieht die\nKosten des ganzen Mandanten. Die Aufschluesselung PRO NUTZER steht unter\n`GET /api/v1/tenant/ai/audit/by-user` und verlangt dort `manager`."}},"/api/v1/tenant/ai/audit":{"get":{"responses":{"200":{"description":"Die Ereignisse — ODER der Rueckfall mit `note`, wenn die Abfrage scheiterte.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":["string","null"]},"modelId":{"type":"string"},"taskType":{"type":["string","null"]},"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"costUsd":{"type":"number"},"createdAt":{"type":"string"}},"required":["id","userId","modelId","taskType","inputTokens","outputTokens","costUsd","createdAt"],"additionalProperties":false}},"total":{"type":"number"}},"required":["events","total"],"additionalProperties":false},{"type":"object","properties":{"events":{"type":"array","items":{},"maxItems":0},"total":{"type":"number","const":0},"note":{"type":"string","const":"Audit log not yet available — run Wave A10 migration"}},"required":["events","total","note"],"additionalProperties":false}]},"example":{"events":[{"id":"string","userId":"string","modelId":"string","taskType":"string","inputTokens":0,"outputTokens":0,"costUsd":0,"createdAt":"string"}],"total":0}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Kein Datenbank-Client verfuegbar. Ein Fehler der ABFRAGE ergibt dagegen 200.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1TenantAiAudit","tags":["tenant"],"parameters":[{"in":"query","name":"userId","schema":{"type":"string"}},{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":1000,"default":100}}],"summary":"KI-Nutzungsprotokoll des Mandanten (leere Liste heisst zweierlei)","description":"Liefert die zuletzt abgerechneten KI-Aufrufe dieses Mandanten aus\n`public.ai_cost_events`, neueste zuerst. Reine Leseoperation: es wird\nnichts geschrieben und KEIN Modell-Kontingent verbraucht — der Aufruf\nselbst kostet nichts.\n\nDAS IST KEIN VOLLSTAENDIGES PROTOKOLL DER KI-NUTZUNG, sondern das\nKostenjournal. Es enthaelt nur, was ein Schreibpfad wirklich verbucht\nhat. Aufrufe, die ohne Mandantenbezug laufen — etwa\n`POST /api/v1/tenant/ai/settings/test-prompt` —, tauchen hier NICHT\nauf, obwohl sie beim Anbieter Geld gekostet haben. Prompt und Antwort\nwerden ohnehin nicht gespeichert; es gibt Modell, Aufgabenart,\nToken-Zahlen und Kosten.\n\n`limit` begrenzt auf 1 bis 1000 (Standard 100).\n`total` ist die Laenge der zurueckgegebenen Liste, NICHT die Gesamtzahl\nim Zeitraum — bei erreichtem `limit` gibt es mehr, und die Antwort sagt\nes nicht. Es gibt keine Blaetterung; wer weiter zurueck will, verschiebt\n`to`.\n\n`from` und `to` werden ungeprueft als `timestamptz` an die Datenbank\ngereicht. Eine unparsbare Angabe ist deshalb kein 400, sondern faellt in\nden Rueckfall unten — mit leerer Liste und 200.\n\nACHTUNG, EINE LEERE LISTE HEISST ZWEIERLEI: entweder es gibt keine Ereignisse, oder `public.ai_cost_events` fehlt. Der Handler faengt jeden Abfragefehler ab und antwortet trotzdem mit 200 und leerer Liste — unterscheidbar allein am zusaetzlichen Feld `note`. Nur eine fehlende Datenbankverbindung ergibt 503.\n\n`costUsd` ist US-DOLLAR und wird NICHT umgerechnet.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten sieht das\nProtokoll des ganzen Mandanten, einschliesslich der `userId` fremder\nKollegen. Nur die Aufschluesselung unter `/by-user` ist auf `manager`\nbeschraenkt."}},"/api/v1/tenant/ai/audit/by-user":{"get":{"responses":{"200":{"description":"Der Verbrauch je Benutzer — ODER der Rueckfall mit `note`, wenn die Nutzer-Abfrage scheiterte.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"periodStart":{"type":"string"},"users":{"type":"array","items":{"type":"object","properties":{"userId":{"type":["string","null"]},"displayName":{"type":["string","null"]},"email":{"type":["string","null"]},"actions":{"type":"number"},"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"costEur":{"type":"number"}},"required":["userId","displayName","email","actions","inputTokens","outputTokens","costEur"],"additionalProperties":false}},"budget":{"type":["object","null"],"properties":{"monthlyLimitEur":{"type":"number"},"currentMonthEur":{"type":"number"},"alertAtPct":{"type":"number"},"hardLimit":{"type":"boolean"}},"required":["monthlyLimitEur","currentMonthEur","alertAtPct","hardLimit"],"additionalProperties":false}},"required":["periodStart","users","budget"],"additionalProperties":false},{"type":"object","properties":{"periodStart":{"type":"string"},"users":{"type":"array","items":{},"maxItems":0},"budget":{"type":"null"},"note":{"type":"string","const":"Usage breakdown not yet available — run Wave A10 migration"}},"required":["periodStart","users","budget","note"],"additionalProperties":false}]},"example":{"periodStart":"string","users":[{"userId":"string","displayName":"string","email":"string","actions":0,"inputTokens":0,"outputTokens":0,"costEur":0}],"budget":{"monthlyLimitEur":0,"currentMonthEur":0,"alertAtPct":0,"hardLimit":true}}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler."},"503":{"description":"Kein Datenbank-Client verfuegbar. Ein Fehler der ABFRAGE ergibt dagegen 200.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1TenantAiAuditBy-user","tags":["tenant"],"parameters":[{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}}],"summary":"KI-Verbrauch je Benutzer (costEur traegt US-Dollar, mit E-Mail-Adressen)","description":"Schluesselt den KI-Verbrauch des Mandanten nach Benutzer auf —\nAktionen, Token und Kosten je Person, teuerste zuerst. Reine\nLeseoperation: es wird nichts geschrieben und KEIN Modell-Kontingent\nverbraucht. Sie greift auch NICHT in die Kontingentdurchsetzung ein,\nsondern zeigt nur an.\n\n`costEur` TRAEGT US-DOLLAR. Der Wert kommt unveraendert aus\n`ai_cost_events.cost_usd`; umgerechnet wird nichts. Der Feldname folgt\nder Spaltenbeschriftung der Oberflaeche („Kosten €\") und ist damit\nirrefuehrend. Dasselbe gilt fuer `monthlyLimitEur` und `currentMonthEur`\nim Block `budget`, die aus `monthly_limit_usd` bzw. `current_month_usd`\nstammen. Wer die Zahlen als Euro anzeigt, zeigt sie zu niedrig.\n\nDIE ANTWORT IST PERSONENBEZOGEN: sie nennt zu jedem Benutzer Name UND\nE-Mail-Adresse. Daher die Mindestrolle `manager`; die\nMandanten-Gesamtsumme ohne Personenbezug steht unter\n`GET /api/v1/tenant/ai/costs` und ist fuer jeden Benutzer offen.\n\nOhne `from` beginnt der Zeitraum am Ersten des laufenden Monats (UTC) —\nderselbe Schnitt, auf den der Budgetzaehler zurueckspringt. Ohne `to`\nist das Ende offen. Beide Werte werden ungeprueft als `timestamptz`\nweitergereicht; eine unparsbare Angabe ergibt keinen 400, sondern den\nRueckfall unten.\n\nEs gibt weder Begrenzung noch Blaetterung: die Liste enthaelt jede\nGruppe des Zeitraums.\n\nGELOESCHTE BENUTZER BLEIBEN SICHTBAR. Der `LEFT JOIN` behaelt Zeilen,\nderen Benutzer es nicht mehr gibt — `displayName` und `email` sind dann\n`null`, die `userId` steht weiterhin da. Ereignisse ganz ohne\nBenutzerzuordnung landen in einer Gruppe mit `userId: null`.\n\nDER BLOCK `budget` HAT DREI HERKUENFTE, die die Antwort nicht\nunterscheidet:\n\n  · aus `public.tenant_ai_budgets`, wenn dort eine Zeile steht;\n  · HERGELEITET aus der Summe der Nutzerkosten, wenn keine Zeile\n    existiert — dann ist `monthlyLimitEur: 0`, was NICHT „keine Grenze\"\n    heisst, sondern „keine Grenze bekannt\", und `alertAtPct: 80` ist\n    geraten;\n  · `null`, wenn es weder eine Zeile noch Verbrauch gibt ODER die\n    Budgetabfrage gescheitert ist — dieser `catch` schweigt.\n\nSCHEITERT DIE NUTZER-ABFRAGE, kommt trotzdem 200: mit leerer Liste,\n`budget: null` und dem Feld `note`. Nur `note` unterscheidet das von\neinem Mandanten, der die KI nie benutzt hat.\n\nVerlangt mindestens die Rolle `manager`."}},"/api/v1/ai/knowledge/docs":{"get":{"responses":{"200":{"description":"Die neuesten 100 Wissensdokumente.","content":{"application/json":{"schema":{"type":"object","properties":{"documents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"chunkCount":{"type":["number","null"]},"indexedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":["number","null"]}},"required":["id","name","status","chunkCount","indexedAt","createdAt","mimeType","sizeBytes"],"additionalProperties":false}}},"required":["documents"],"additionalProperties":false},"example":{"documents":[{"id":"string","name":"string","status":"string","chunkCount":0,"indexedAt":"string","createdAt":"string","mimeType":"string","sizeBytes":0}]}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Kein Datenbank-Client ODER die Abfrage ist gescheitert. Der Koerper unterscheidet die beiden Faelle.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"getApiV1AiKnowledgeDocs","tags":["ai"],"parameters":[],"summary":"Wissensdokumente des Mandanten (die neuesten 100, ohne Blaettern)","description":"Liefert die registrierten Wissensdokumente aus\n`public.ai_knowledge_documents`, neueste zuerst.\n\nHARTE GRENZE VON 100 ZEILEN, OHNE BLAETTERN. Es gibt keine Parameter\nfuer Menge oder Versatz und keine Gesamtzahl in der Antwort. Wer mehr\nals 100 Dokumente registriert hat, sieht die aelteren ueber diesen\nEndpunkt nie — und merkt es nicht, weil die Antwort ganz normal\naussieht.\n\nDer Fortschritt der Indizierung steht auf der Zeile selbst:\n`pending` -> `indexed` (mit `chunkCount` und `indexedAt`) oder\n`failed`. Diese Liste ist der vorgesehene Weg, das abzulesen; einen\neigenen Status-Endpunkt gibt es nicht. Den Fehlergrund einer\ngescheiterten Indizierung traegt die Spalte `error_msg` — sie wird HIER\nNICHT ausgeliefert.\n\nDer 503 kommt aus einem `catch`, das jeden Abfragefehler abfaengt\n(auch die fehlende Tabelle). Eine leere Liste ist deshalb wirklich\nleer und kein verschluckter Fehler."}},"/api/v1/ai/knowledge/docs/register":{"post":{"responses":{"201":{"description":"Registriert. `indexing.started` sagt, ob indiziert wird — 201 kommt auch, wenn nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["pending","indexed","failed"]},"indexing":{"type":"object","properties":{"started":{"type":"boolean"},"reason":{"type":["string","null"]}},"required":["started","reason"],"additionalProperties":false},"message":{"type":"string"}},"required":["id","name","status","indexing","message"],"additionalProperties":false},"example":{"id":"string","name":"string","status":"pending","indexing":{"started":true,"reason":"string"},"message":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext, ODER der Schluessel gehoert nicht zu diesem Mandanten (`storage_key_out_of_scope`), ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators. In allen drei Faellen wurde nichts angelegt.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"storage_key_out_of_scope"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"500":{"description":"Das `INSERT` lief ohne Fehler, lieferte aber keine Zeile zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Insert failed"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client — ODER das Anlegen der Zeile ist gescheitert. Es wurde nichts angelegt; Wiederholen ist hier richtig.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"postApiV1AiKnowledgeDocsRegister","tags":["ai"],"parameters":[],"summary":"Hochgeladenes Dokument in die Wissensbasis aufnehmen und indizieren","description":"Traegt eine bereits im Objektspeicher liegende Datei als Wissensdokument\nein (`public.ai_knowledge_documents`) und stoesst die Indizierung an. Der\nvollstaendige Weg: `POST /api/v1/uploads/presign` holen, Datei per PUT\nablegen, dann diesen Aufruf mit genau demselben Schluessel.\n\nWAS DAUERHAFT ENTSTEHT: eine Dokumentzeile UND — sobald die Indizierung\ndurchlaeuft — Abschnitte samt Einbettungen in\n`public.tenant_rag_documents`. Ab diesem Moment kann der Assistent den\nInhalt in JEDEM Gespraech dieses Mandanten zitieren. Rueckgaengig macht\ndas nur `DELETE /api/v1/ai/knowledge/docs/{id}`, das Zeile UND Vektoren\nentfernt.\n\nDIE INDIZIERUNG RUFT DEN EINBETTUNGS-ANBIETER AUF — je Abschnitt einmal.\nDas kostet dort Geld. Es ist KEIN Sprachmodell-Aufruf und zaehlt nicht\ngegen das monatliche KI-Kontingent; in `public.ai_cost_events` taucht es\nebenfalls nicht auf.\n\nSIE LAEUFT NEBEN DER ANTWORT. Der Aufruf kehrt sofort zurueck; ob die\nIndizierung geglueckt ist, sagt er NICHT. Den Fortschritt zeigen\n`status` und `chunkCount` unter `GET /api/v1/ai/knowledge/docs`.\n\nWAS DIE ANTWORT SEHR WOHL SAGT, ist, ob ueberhaupt indiziert WIRD.\n`indexing.started: false` heisst: es laeuft nichts, und `indexing.reason`\nnennt den Grund — RAG fuer diesen Mandanten abgeschaltet, oder kein\nEinbettungs-Anbieter eingerichtet. Die Zeile traegt dann einen\nEndzustand mit demselben Grund und bleibt NICHT auf `pending` stehen.\n\nDER STATUS 201 KOMMT IN BEIDEN FAELLEN. Registriert IST das Dokument;\nein Fehlerstatus waere hier unwahr und lud zu einem zweiten Aufruf ein,\nder bloss eine zweite Zeile fuer dieselbe Datei erzeugte. Es gibt keine\nDoppelt-Erkennung: derselbe `s3Key` laesst sich beliebig oft\nregistrieren, und jede Registrierung indiziert erneut.\n\nDER SCHLUESSEL MUSS ZUM MANDANTEN GEHOEREN. `s3Key` kommt vom Aufrufer\nund muss mit `tenants/<tenantId>/` beginnen, sonst 400 und es wird\nnichts angelegt. Ohne diese Pruefung liesse sich die Datei eines fremden\nMandanten in die eigene Wissensbasis holen.\n\nSehr lange Texte werden bei 200.000 Zeichen gekuerzt; das Ergebnis sagt\nes dann auf der Zeile, statt still abzuschneiden.\n\nDER 503 GILT NUR FUER DAS ANLEGEN DER ZEILE — dort ist Wiederholen\nrichtig, weil nichts entstanden ist. Ab dem Anstossen der Indizierung\ngibt es keinen Fehlerstatus mehr.\n\nWer nur Text ablegen will, nimmt\n`POST /api/v1/ai/knowledge-items/upload`: dort geht die Datei direkt im\nRequest mit, der Umweg ueber den Objektspeicher entfaellt.\n\nVerlangt mindestens die Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"s3Key":{"type":"string","minLength":1},"mimeType":{"type":"string","default":"application/pdf"}},"required":["name","s3Key"]},"example":{"name":"string","s3Key":"string","mimeType":"string"}}}}}},"/api/v1/ai/knowledge/docs/{id}":{"delete":{"responses":{"200":{"description":"Dokument und Vektoren geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false},"example":{"id":"string","deleted":true}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Dokument mit dieser Kennung in diesem Mandanten — oder schon geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Document not found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1AiKnowledgeDocsById","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Wissensdokument und seine Vektoren endgueltig loeschen","description":"Entfernt die Dokumentzeile aus `public.ai_knowledge_documents` UND die\nzugehoerigen Abschnitte aus `public.tenant_rag_documents`. Beides\nendgueltig, kein `deleted_at`. Ein zweiter Aufruf antwortet mit 404.\n\nDas Loeschen der Vektoren steht ausdruecklich hier im Handler und\nhaengt nicht an einem Fremdschluessel: der frueher angenommene CASCADE\ngehoert zu einer stillgelegten Tabelle. Ohne diesen zweiten Schritt\nwaere der Inhalt aus der Liste verschwunden und im Chat geblieben.\n\nKEINE TRANSAKTION. Die beiden Loeschungen laufen nacheinander.\nScheitert die zweite, ist die Dokumentzeile bereits weg, die Vektoren\nsind es nicht — der Aufrufer bekommt dann einen Fehler und findet das\nDokument nicht mehr, um es erneut zu loeschen. Der Inhalt bleibt im\nChat auffindbar.\n\nDie Datei im Objektspeicher wird NICHT geloescht. Der Schluessel steht\nweiter in keiner Zeile — die Datei selbst bleibt liegen.\n\nVerlangt mindestens die Rolle `admin`."}},"/api/v1/ai/knowledge/docs/{id}/reindex":{"post":{"responses":{"200":{"description":"Angenommen. Ob wirklich indiziert wird, steht in `indexing.started`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["pending","indexed","failed"]},"indexing":{"type":"object","properties":{"started":{"type":"boolean"},"reason":{"type":["string","null"]}},"required":["started","reason"],"additionalProperties":false},"message":{"type":"string"}},"required":["id","status","indexing","message"],"additionalProperties":false},"example":{"id":"string","status":"pending","indexing":{"started":true,"reason":"string"},"message":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext. Beachte den Status: 400, nicht 401.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Tenant not found"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Dokument mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Document not found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"DB not available"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiKnowledgeDocsByIdReindex","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Dokument neu indizieren (Antwort sagt, OB ueberhaupt indiziert wird)","description":"Stoesst die Indizierung nach `public.tenant_rag_documents` (vector(1536))\nan — denselben Speicher, an dem der Chat haengt.\n\nDIE ANTWORT KOMMT SOFORT, DIE ARBEIT LAEUFT DANEBEN. Ein 200 heisst\nnicht „fertig indiziert\". Den Fortschritt liest man auf der Zeile:\n`GET /api/v1/ai/knowledge/docs` zeigt `pending` -> `indexed`\n(mit `chunkCount`) oder `failed`.\n\n`indexing.started` IST DER EIGENTLICHE INHALT DIESER ANTWORT. Ist es\n`false`, laeuft NICHTS, und `indexing.reason` sagt im Klartext warum\n(RAG fuer den Mandanten abgeschaltet, kein Einbettungs-Anbieter\neingerichtet). Die Zeile traegt dann einen Endzustand mit demselben\nGrund — kein `pending`, auf dem niemand sitzt. Auch dieser Fall ist\nein 200: die Anfrage war in Ordnung, die Indizierung ist es nicht.\n\nVor dem Anstoss wird der bisherige Zustand zurueckgesetzt (`pending`,\n`error_msg` geleert), damit kein alter Fehlergrund neben einem neuen\nLauf stehenbleibt. Wird danach nicht gestartet, traegt die Zeile den\nneuen Grund.\n\nDer Aufruf ist wiederholbar. Ein bereits indiziertes Dokument wird neu\nindiziert; die alten Abschnitte werden dabei ersetzt.\n\nVerlangt mindestens die Rolle `admin`."}},"/api/v1/ai/cashflow/forecast":{"get":{"responses":{"200":{"description":"Cash flow forecast","content":{"application/json":{"schema":{"type":"object","properties":{"horizonDays":{"type":"number"},"bucketDays":{"type":"number"},"currency":{"type":"string"},"totalExpected":{"type":"number"},"totalOverdue":{"type":"number"},"openInvoices":{"type":"number"},"buckets":{"type":"array","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"expectedInflow":{"type":"number"},"invoiceCount":{"type":"number"},"overdueAmount":{"type":"number"}},"required":["from","to","expectedInflow","invoiceCount","overdueAmount"],"additionalProperties":false}},"source":{"type":"string"},"liquidityRisk":{"type":"string","enum":["low","medium","high"]},"summary":{"type":"string"},"recommendations":{"type":"array","items":{"type":"string"}},"analysedAt":{"type":"string"}},"required":["horizonDays","bucketDays","currency","totalExpected","totalOverdue","openInvoices","buckets","source","liquidityRisk","summary","recommendations","analysedAt"],"additionalProperties":false},"example":{"horizonDays":0,"bucketDays":0,"currency":"string","totalExpected":0,"totalOverdue":0,"openInvoices":0,"buckets":[{"from":"string","to":"string","expectedInflow":0,"invoiceCount":0,"overdueAmount":0}],"source":"string","liquidityRisk":"low","summary":"string","recommendations":["string"],"analysedAt":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1AiCashflowForecast","tags":["ai","Finance"],"parameters":[{"in":"query","name":"horizonDays","schema":{"type":"number","minimum":7,"maximum":365,"default":90}},{"in":"query","name":"bucketDays","schema":{"type":"number","default":7}}],"summary":"AI cash flow forecast (90-day outlook)","description":"W24-H — analyses open receivables and generates a weekly/bi-weekly/monthly cash-flow projection. Enhanced with claude-haiku-4-5 when API key is present."}},"/api/v1/ai/invoice-anomalies":{"get":{"responses":{"200":{"description":"Anomaly report","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"number"},"scannedCount":{"type":"number"},"anomalyCount":{"type":"number"},"highSeverity":{"type":"number"},"mediumSeverity":{"type":"number"},"lowSeverity":{"type":"number"},"anomalies":{"type":"array","items":{"type":"object","properties":{"invoiceId":{"type":"string"},"invoiceNo":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"},"customerName":{"type":"string"},"issuedAt":{"type":"string"},"anomalyType":{"type":"string","enum":["duplicate_amount","round_amount","new_customer_high_value","rapid_sequence"]},"severity":{"type":"string","enum":["low","medium","high"]},"reason":{"type":"string"},"aiSummary":{"type":["string","null"]},"relatedIds":{"type":"array","items":{"type":"string"}}},"required":["invoiceId","invoiceNo","amount","currency","customerName","issuedAt","anomalyType","severity","reason","aiSummary","relatedIds"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["lookbackDays","scannedCount","anomalyCount","highSeverity","mediumSeverity","lowSeverity","anomalies","summary","aiEnriched","generatedAt"]},"example":{"lookbackDays":0,"scannedCount":0,"anomalyCount":0,"highSeverity":0,"mediumSeverity":0,"lowSeverity":0,"anomalies":[{"invoiceId":"string","invoiceNo":"string","amount":0,"currency":"string","customerName":"string","issuedAt":"string","anomalyType":"duplicate_amount","severity":"low","reason":"string","aiSummary":"string","relatedIds":["string"]}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiInvoice-anomalies","tags":["ai","Finance"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":7,"maximum":365,"default":90}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"minAmount","schema":{"type":"number","minimum":0,"default":0}}],"summary":"AI invoice anomaly detection","description":"W24-C — scans recent invoices for suspicious patterns: duplicates, round amounts, new-customer high-value, rapid sequences. Enhanced with claude-haiku-4-5. Die Befunde entstehen aus FESTEN Regeln, nicht aus dem Modell: es ergaenzt nur `aiSummary` je Befund und `summary`. Faellt es aus oder ist keine KI konfiguriert, bleiben beide null und `aiEnriched` false — die Befunde stehen trotzdem. `lookbackDays` (7…365, Vorgabe 90) legt das Zeitfenster fest, `minAmount` blendet kleine Betraege aus, `limit` (1…100, Vorgabe 20) begrenzt die Befunde. `scannedCount` nennt die geprueften Rechnungen — ein leerer Bericht heiszt „in diesem Ausschnitt nichts gefunden\", nicht „alles in Ordnung\". Ein Befund ist ein Hinweis, kein Nachweis: rein lesend, es wird nichts markiert, gesperrt oder gespeichert."}},"/api/v1/ai/supplier-comparison":{"get":{"responses":{"200":{"description":"Price comparison report. `potentialSavingPct` is the SPREAD between the cheapest and dearest supplier for that line, not money already saved. Lines are grouped by article id where there is one, otherwise by the normalised line text — so two differently worded free-text lines count as two articles. An empty `articles` list means nothing met `minOrders` — not an error.","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"integer","description":"Echoed from the query"},"analysedArticles":{"type":"integer"},"totalOrders":{"type":"integer"},"articles":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","description":"The line-item text the comparison groups on"},"articleId":{"type":["string","null"],"description":"Null for free-text lines with no article"},"unit":{"type":"string"},"bestSupplier":{"type":"string"},"bestPrice":{"type":"number"},"worstPrice":{"type":"number"},"potentialSavingPct":{"type":"number","description":"Best against worst price — a spread, not a realised saving"},"supplierCount":{"type":"integer"},"pricePoints":{"type":"array","items":{"type":"object","properties":{"supplierId":{"type":"string"},"supplierName":{"type":"string"},"unitPrice":{"type":"number"},"currency":{"type":"string"},"orderCount":{"type":"integer"},"lastOrderedAt":{"type":"string"},"avgUnitPrice":{"type":"number"},"minUnitPrice":{"type":"number"},"maxUnitPrice":{"type":"number"}},"required":["supplierId","supplierName","unitPrice","currency","orderCount","lastOrderedAt","avgUnitPrice","minUnitPrice","maxUnitPrice"]}},"aiSummary":{"type":["string","null"],"description":"German, from the model; null when no model ran or it said nothing here"}},"required":["description","articleId","unit","bestSupplier","bestPrice","worstPrice","potentialSavingPct","supplierCount","pricePoints","aiSummary"]}},"summary":{"type":["string","null"],"description":"Null unless the model produced one"},"aiEnriched":{"type":"boolean","description":"False means the rule-based numbers stand alone — no model ran, or it failed"},"generatedAt":{"type":"string"}},"required":["lookbackDays","analysedArticles","totalOrders","articles","summary","aiEnriched","generatedAt"]},"example":{"lookbackDays":0,"analysedArticles":0,"totalOrders":0,"articles":[{"description":"string","articleId":"string","unit":"string","bestSupplier":"string","bestPrice":0,"worstPrice":0,"potentialSavingPct":0,"supplierCount":0,"pricePoints":[{"supplierId":"string","supplierName":"string","unitPrice":0,"currency":"string","orderCount":0,"lastOrderedAt":"string","avgUnitPrice":0,"minUnitPrice":0,"maxUnitPrice":0}],"aiSummary":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiSupplier-comparison","tags":["ai","purchasing"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":30,"maximum":730,"default":180}},{"in":"query","name":"topN","schema":{"type":"number","minimum":1,"maximum":50,"default":10}},{"in":"query","name":"minOrders","schema":{"type":"number","minimum":1,"maximum":10,"default":2}}],"summary":"AI supplier price comparison","description":"W25-A — compares prices paid across suppliers for the same items. Identifies cheapest supplier and potential savings."}},"/api/v1/ai/sales-forecast":{"get":{"responses":{"200":{"description":"Sales forecast report. The projection is ARITHMETIC — expected revenue per opportunity multiplied by its stage probability, bucketed by expected close month over `horizonMonths`; the AI only adds `summary` and `recommendations`. Without a configured model the report still ships with `aiEnriched: false`, `summary: null` and an empty recommendation list. `currency` is echoed from the query — amounts are NOT converted. `overdueOpportunities` counts deals whose expected close date has passed while they are still open — those are folded into the CURRENT month bucket, so that bucket mixes overdue with genuinely due deals. Deals closing beyond the horizon are counted in the pipeline totals but land in no bucket at all.","content":{"application/json":{"schema":{"type":"object","properties":{"horizonMonths":{"type":"number"},"currency":{"type":"string"},"openOpportunities":{"type":"number"},"totalPipelineValue":{"type":"number"},"weightedPipelineValue":{"type":"number"},"stageSnapshot":{"type":"array","items":{"type":"object","properties":{"stage":{"type":"string"},"count":{"type":"number"},"totalValue":{"type":"number"},"weightedValue":{"type":"number"}},"required":["stage","count","totalValue","weightedValue"],"additionalProperties":false}},"monthlyBuckets":{"type":"array","items":{"type":"object","properties":{"month":{"type":"string"},"opportunities":{"type":"number"},"totalValue":{"type":"number"},"weightedValue":{"type":"number"},"byStage":{"type":"object","additionalProperties":{"type":"object","properties":{"count":{"type":"number"},"weighted":{"type":"number"}},"required":["count","weighted"],"additionalProperties":false}}},"required":["month","opportunities","totalValue","weightedValue","byStage"],"additionalProperties":false}},"overdueOpportunities":{"type":"number"},"summary":{"type":["string","null"]},"recommendations":{"type":"array","items":{"type":"string"}},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["horizonMonths","currency","openOpportunities","totalPipelineValue","weightedPipelineValue","stageSnapshot","monthlyBuckets","overdueOpportunities","summary","recommendations","aiEnriched","generatedAt"],"additionalProperties":false},"example":{"horizonMonths":0,"currency":"string","openOpportunities":0,"totalPipelineValue":0,"weightedPipelineValue":0,"stageSnapshot":[{"stage":"string","count":0,"totalValue":0,"weightedValue":0}],"monthlyBuckets":[{"month":"string","opportunities":0,"totalValue":0,"weightedValue":0,"byStage":{"beispiel":{"count":0,"weighted":0}}}],"overdueOpportunities":0,"summary":"string","recommendations":["string"],"aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiSales-forecast","tags":["ai","CRM"],"parameters":[{"in":"query","name":"horizonMonths","schema":{"type":"number","minimum":1,"maximum":12,"default":3}},{"in":"query","name":"currency","schema":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"}}],"summary":"AI sales pipeline forecast","description":"W25-B — projects monthly revenue from open pipeline (Verkaufschancen) using probability-weighted stage analysis."}},"/api/v1/ai/workload":{"get":{"responses":{"200":{"description":"Workload report. The numbers are rule-based and always present; `aiEnriched` says whether the German advice in `summary`/`aiSummary` came from a model. An empty `employees` list means no timesheet rows in the window — not an error.","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackWeeks":{"type":"integer","description":"Echoed from the query"},"targetHoursWeek":{"type":"number"},"employees":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string"},"userName":{"type":"string"},"totalHours":{"type":"number","description":"Sum over the whole window"},"avgWeeklyHours":{"type":"number"},"targetHours":{"type":"number","description":"Target for the WHOLE window, not per week"},"status":{"type":"string","description":"overloaded | ok | underloaded | no_data"},"deviationPct":{"type":"number","description":"Positive = above target, negative = below"},"projects":{"type":"array","items":{"type":"object","properties":{"projectId":{"type":["string","null"]},"projectName":{"type":"string","description":"\"Kein Projekt\" when the entry carries none"},"hours":{"type":"number"}},"required":["projectId","projectName","hours"]},"description":"Sorted by hours, descending"},"aiSummary":{"type":["string","null"],"description":"German advice from the model; null when no model ran or it said nothing"}},"required":["userId","userName","totalHours","avgWeeklyHours","targetHours","status","deviationPct","projects","aiSummary"]},"description":"Sorted by deviation, most overloaded first"},"overloaded":{"type":"integer"},"underloaded":{"type":"integer"},"projectConcentration":{"type":"array","items":{"type":"object","properties":{"projectId":{"type":["string","null"]},"projectName":{"type":"string"},"totalHours":{"type":"number"},"employeeCount":{"type":"integer"},"soloRisk":{"type":"boolean","description":"Exactly one person logged hours — single point of failure"}},"required":["projectId","projectName","totalHours","employeeCount","soloRisk"]},"description":"Sorted by hours, descending"},"summary":{"type":["string","null"],"description":"Null unless the model produced one"},"aiEnriched":{"type":"boolean","description":"False means the rule-based numbers stand alone — no model ran, or it failed"},"generatedAt":{"type":"string"}},"required":["lookbackWeeks","targetHoursWeek","employees","overloaded","underloaded","projectConcentration","summary","aiEnriched","generatedAt"]},"example":{"lookbackWeeks":0,"targetHoursWeek":0,"employees":[{"userId":"string","userName":"string","totalHours":0,"avgWeeklyHours":0,"targetHours":0,"status":"string","deviationPct":0,"projects":[{"projectId":"string","projectName":"string","hours":0}],"aiSummary":"string"}],"overloaded":0,"underloaded":0,"projectConcentration":[{"projectId":"string","projectName":"string","totalHours":0,"employeeCount":0,"soloRisk":true}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiWorkload","tags":["ai","HR"],"parameters":[{"in":"query","name":"lookbackWeeks","schema":{"type":"number","minimum":1,"maximum":26,"default":4}},{"in":"query","name":"targetHoursWeek","schema":{"type":"number","minimum":1,"maximum":80,"default":40}},{"in":"query","name":"overloadThreshold","schema":{"type":"number","minimum":1,"maximum":100,"default":15}},{"in":"query","name":"underloadThreshold","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"summary":"AI employee workload analysis","description":"W25-C — analyses timesheet data to detect overloaded/underloaded employees and project concentration risk."}},"/api/v1/ai/financial-ratios":{"get":{"responses":{"200":{"description":"Financial ratios report","content":{"application/json":{"schema":{"type":"object","properties":{"periodDays":{"type":"integer"},"currency":{"type":"string"},"ratios":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"value":{"type":"number"},"unit":{"type":"string","description":"'%', 'Tage', 'x' oder der Waehrungscode"},"benchmark":{"type":"string"},"status":{"type":"string","enum":["good","warning","critical","neutral"]}},"required":["key","label","value","unit","benchmark","status"]}},"healthScore":{"type":"number","description":"Overall financial health score 0-10"},"summary":{"type":["string","null"]},"recommendations":{"type":"array","items":{"type":"string"},"description":"AI recommendations when aiEnriched, otherwise the rule-based fallback list"},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["periodDays","currency","ratios","healthScore","summary","recommendations","aiEnriched","generatedAt"]},"example":{"periodDays":0,"currency":"string","ratios":[{"key":"string","label":"string","value":0,"unit":"string","benchmark":"string","status":"good"}],"healthScore":0,"summary":"string","recommendations":["string"],"aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiFinancial-ratios","tags":["ai","Finance"],"parameters":[{"in":"query","name":"periodDays","schema":{"type":"number","minimum":30,"maximum":365,"default":90}},{"in":"query","name":"currency","schema":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"}}],"summary":"AI financial ratio analysis","description":"W25-D — computes gross margin, DSO, DPO, AR turnover from invoice+purchasing data. CFO-style interpretation via claude-haiku-4-5."}},"/api/v1/ai/contract-renewal":{"get":{"responses":{"200":{"description":"Contract renewal alert report","content":{"application/json":{"schema":{"type":"object","properties":{"horizonDays":{"type":"integer"},"alertCount":{"type":"integer"},"criticalCount":{"type":"integer"},"highCount":{"type":"integer"},"mediumCount":{"type":"integer"},"alerts":{"type":"array","items":{"type":"object","properties":{"contractId":{"type":"string"},"contractNumber":{"type":"string"},"title":{"type":"string"},"counterpartyName":{"type":["string","null"]},"counterpartyType":{"type":"string"},"value":{"type":["number","null"]},"currency":{"type":"string"},"endDate":{"type":["string","null"],"description":"ISO date (YYYY-MM-DD)"},"noticePeriodDays":{"type":"integer"},"autoRenewal":{"type":"boolean"},"daysUntilExpiry":{"type":["integer","null"]},"cancelDeadline":{"type":["string","null"],"description":"Last day to cancel before auto-renewal rolls over (YYYY-MM-DD)"},"daysUntilCancel":{"type":["integer","null"]},"urgency":{"type":"string","enum":["critical","high","medium","low"]},"reasons":{"type":"array","items":{"type":"string"}},"aiSummary":{"type":["string","null"],"description":"German recommendation — null unless the LLM enrichment ran"}},"required":["contractId","contractNumber","title","counterpartyName","counterpartyType","value","currency","endDate","noticePeriodDays","autoRenewal","daysUntilExpiry","cancelDeadline","daysUntilCancel","urgency","reasons","aiSummary"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["horizonDays","alertCount","criticalCount","highCount","mediumCount","alerts","summary","aiEnriched","generatedAt"]},"example":{"horizonDays":0,"alertCount":0,"criticalCount":0,"highCount":0,"mediumCount":0,"alerts":[{"contractId":"string","contractNumber":"string","title":"string","counterpartyName":"string","counterpartyType":"string","value":0,"currency":"string","endDate":"string","noticePeriodDays":0,"autoRenewal":true,"daysUntilExpiry":0,"cancelDeadline":"string","daysUntilCancel":0,"urgency":"critical","reasons":["string"],"aiSummary":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiContract-renewal","tags":["ai","contracts"],"parameters":[{"in":"query","name":"horizonDays","schema":{"type":"number","minimum":7,"maximum":365,"default":90}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"summary":"AI contract renewal monitoring","description":"W25-E — scans active contracts for upcoming expirations and auto-renewal deadlines. Priority alerts with claude-haiku-4-5 action recommendations."}},"/api/v1/ai/defect-patterns":{"get":{"responses":{"200":{"description":"Defect pattern report","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"number"},"totalDefects":{"type":"number"},"openDefects":{"type":"number"},"resolvedDefects":{"type":"number"},"criticalOpen":{"type":"number"},"avgResolutionDays":{"type":["number","null"]},"hotspots":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["project","location","priority"]},"defectCount":{"type":"number"},"openCount":{"type":"number"},"criticalCount":{"type":"number"},"avgResolutionDays":{"type":["number","null"]}},"required":["key","label","type","defectCount","openCount","criticalCount","avgResolutionDays"],"additionalProperties":false}},"recurringKeywords":{"type":"array","items":{"type":"object","properties":{"word":{"type":"string"},"count":{"type":"number"}},"required":["word","count"],"additionalProperties":false}},"overdueDefects":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"priority":{"type":"string"},"daysOverdue":{"type":"number"},"projectId":{"type":["string","null"]},"location":{"type":["string","null"]}},"required":["id","title","priority","daysOverdue","projectId","location"],"additionalProperties":false}},"summary":{"type":["string","null"]},"recommendations":{"type":"array","items":{"type":"string"}},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["lookbackDays","totalDefects","openDefects","resolvedDefects","criticalOpen","avgResolutionDays","hotspots","recurringKeywords","overdueDefects","summary","recommendations","aiEnriched","generatedAt"],"additionalProperties":false},"example":{"lookbackDays":0,"totalDefects":0,"openDefects":0,"resolvedDefects":0,"criticalOpen":0,"avgResolutionDays":0,"hotspots":[{"key":"string","label":"string","type":"project","defectCount":0,"openCount":0,"criticalCount":0,"avgResolutionDays":0}],"recurringKeywords":[{"word":"string","count":0}],"overdueDefects":[{"id":"string","title":"string","priority":"string","daysOverdue":0,"projectId":"string","location":"string"}],"summary":"string","recommendations":["string"],"aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiDefect-patterns","tags":["ai","QM"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":7,"maximum":365,"default":90}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":50,"default":10}}],"summary":"AI defect pattern analysis","description":"W25-F — analyses Mängel data to detect quality hotspots, recurring issues, and overdue critical defects."}},"/api/v1/ai/project-profitability":{"get":{"responses":{"200":{"description":"Project profitability report. Every nullable number here means NOT MEASURABLE — no budget recorded, no progress entered — and must not be read as 0. `aiEnriched` says whether the German prose came from a model.","content":{"application/json":{"schema":{"type":"object","properties":{"analysedProjects":{"type":"integer"},"overBudget":{"type":"integer"},"atRisk":{"type":"integer"},"onTrack":{"type":"integer"},"totalBudget":{"type":"number"},"totalSpent":{"type":"number"},"avgUtilizationPct":{"type":["number","null"],"description":"Null when no project carries a budget"},"projects":{"type":"array","items":{"type":"object","properties":{"projectId":{"type":"string"},"projectName":{"type":"string"},"projectNumber":{"type":"string"},"phase":{"type":["string","null"]},"budget":{"type":["number","null"],"description":"Null = no budget recorded, not a budget of 0"},"spent":{"type":["number","null"]},"utilizationPct":{"type":["number","null"],"description":"Spent against budget; null without a budget"},"progressPercent":{"type":["number","null"]},"projectedFinalCost":{"type":["number","null"],"description":"Spent extrapolated over progress; null while progress is 0 or unknown"},"projectedOverrun":{"type":["number","null"],"description":"Projected final cost minus budget; negative = below budget"},"daysUntilEnd":{"type":["number","null"],"description":"Negative once the end date has passed"},"risk":{"type":"string","description":"over_budget | at_risk | on_track | no_data"},"riskReason":{"type":"string","description":"German, rule-based — always present"},"aiSummary":{"type":["string","null"],"description":"German, from the model; null when no model ran or it said nothing here"}},"required":["projectId","projectName","projectNumber","phase","budget","spent","utilizationPct","progressPercent","projectedFinalCost","projectedOverrun","daysUntilEnd","risk","riskReason","aiSummary"]}},"summary":{"type":["string","null"],"description":"Null unless the model produced one"},"aiEnriched":{"type":"boolean","description":"False means the rule-based numbers stand alone — no model ran, or it failed"},"generatedAt":{"type":"string"}},"required":["analysedProjects","overBudget","atRisk","onTrack","totalBudget","totalSpent","avgUtilizationPct","projects","summary","aiEnriched","generatedAt"]},"example":{"analysedProjects":0,"overBudget":0,"atRisk":0,"onTrack":0,"totalBudget":0,"totalSpent":0,"avgUtilizationPct":0,"projects":[{"projectId":"string","projectName":"string","projectNumber":"string","phase":"string","budget":0,"spent":0,"utilizationPct":0,"progressPercent":0,"projectedFinalCost":0,"projectedOverrun":0,"daysUntilEnd":0,"risk":"string","riskReason":"string","aiSummary":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiProject-profitability","tags":["ai","Projects"],"parameters":[{"in":"query","name":"phases","schema":{"type":"string","default":"planning,in_progress,review"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":50,"default":15}}],"summary":"AI project profitability analysis","description":"W25-G — budget utilization, over-run risk, and projected final cost per project."}},"/api/v1/ai/clv-analysis":{"get":{"responses":{"200":{"description":"CLV analysis report. Segments, churn risk and the 12-month projection are computed ARITHMETICALLY from invoice history — the AI only adds the German retention wording. When no model is configured or the model call fails, the report still ships with `aiEnriched: false`, `summary: null` and every `aiSummary` null; nothing else changes. `topCustomers` is capped by `topN`, while the counts above it cover every customer that passed `lookbackMonths` and `minRevenue` — the two do not have to add up.","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackMonths":{"type":"number"},"totalCustomers":{"type":"number"},"championCount":{"type":"number"},"loyalCount":{"type":"number"},"atRiskCount":{"type":"number"},"lostCount":{"type":"number"},"newCount":{"type":"number"},"avgClv12m":{"type":"number"},"totalRevenue":{"type":"number"},"topCustomers":{"type":"array","items":{"type":"object","properties":{"customerId":{"type":"string"},"customerName":{"type":"string"},"totalRevenue":{"type":"number"},"invoiceCount":{"type":"number"},"avgOrderValue":{"type":"number"},"tenureMonths":{"type":"number"},"recencyMonths":{"type":"number"},"purchaseFrequency":{"type":"number"},"projectedClv12m":{"type":"number"},"segment":{"type":"string","enum":["champion","loyal","at_risk","lost","new"]},"churnRisk":{"type":"string","enum":["high","medium","low"]},"aiSummary":{"type":["string","null"]}},"required":["customerId","customerName","totalRevenue","invoiceCount","avgOrderValue","tenureMonths","recencyMonths","purchaseFrequency","projectedClv12m","segment","churnRisk","aiSummary"],"additionalProperties":false}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["lookbackMonths","totalCustomers","championCount","loyalCount","atRiskCount","lostCount","newCount","avgClv12m","totalRevenue","topCustomers","summary","aiEnriched","generatedAt"],"additionalProperties":false},"example":{"lookbackMonths":0,"totalCustomers":0,"championCount":0,"loyalCount":0,"atRiskCount":0,"lostCount":0,"newCount":0,"avgClv12m":0,"totalRevenue":0,"topCustomers":[{"customerId":"string","customerName":"string","totalRevenue":0,"invoiceCount":0,"avgOrderValue":0,"tenureMonths":0,"recencyMonths":0,"purchaseFrequency":0,"projectedClv12m":0,"segment":"champion","churnRisk":"high","aiSummary":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiClv-analysis","tags":["ai","CRM"],"parameters":[{"in":"query","name":"lookbackMonths","schema":{"type":"number","minimum":3,"maximum":36,"default":12}},{"in":"query","name":"topN","schema":{"type":"number","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"minRevenue","schema":{"type":"number","minimum":0,"default":0}}],"summary":"AI customer lifetime value analysis","description":"W25-H — CLV segmentation (champion/loyal/at_risk/lost/new), purchase frequency, projected 12-month revenue, churn risk. AI retention strategy recommendations."}},"/api/v1/ai/inventory-turnover":{"get":{"responses":{"200":{"description":"Inventory turnover report","content":{"application/json":{"schema":{"type":"object","properties":{"deadStockDays":{"type":"number"},"lowStockDays":{"type":"number"},"totalItems":{"type":"number"},"deadStockCount":{"type":"number"},"slowMoverCount":{"type":"number"},"reorderRiskCount":{"type":"number"},"okCount":{"type":"number"},"totalDeadStockValue":{"type":"number"},"items":{"type":"array","items":{"type":"object","properties":{"itemId":{"type":"string"},"sku":{"type":"string"},"name":{"type":"string"},"category":{"type":["string","null"]},"currentStock":{"type":"number"},"unitCost":{"type":["number","null"]},"stockValue":{"type":["number","null"]},"daysSinceMovement":{"type":["number","null"]},"avgDailyUsage":{"type":["number","null"]},"coverageDays":{"type":["number","null"]},"turnoverRatio":{"type":["number","null"]},"status":{"type":"string","enum":["dead_stock","slow_mover","reorder_risk","ok"]},"statusReason":{"type":"string"},"aiSummary":{"type":["string","null"]}},"required":["itemId","sku","name","category","currentStock","unitCost","stockValue","daysSinceMovement","avgDailyUsage","coverageDays","turnoverRatio","status","statusReason","aiSummary"],"additionalProperties":false}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["deadStockDays","lowStockDays","totalItems","deadStockCount","slowMoverCount","reorderRiskCount","okCount","totalDeadStockValue","items","summary","aiEnriched","generatedAt"],"additionalProperties":false},"example":{"deadStockDays":0,"lowStockDays":0,"totalItems":0,"deadStockCount":0,"slowMoverCount":0,"reorderRiskCount":0,"okCount":0,"totalDeadStockValue":0,"items":[{"itemId":"string","sku":"string","name":"string","category":"string","currentStock":0,"unitCost":0,"stockValue":0,"daysSinceMovement":0,"avgDailyUsage":0,"coverageDays":0,"turnoverRatio":0,"status":"dead_stock","statusReason":"string","aiSummary":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiInventory-turnover","tags":["ai","Inventory"],"parameters":[{"in":"query","name":"deadStockDays","schema":{"type":"number","minimum":14,"maximum":365,"default":90}},{"in":"query","name":"lowStockDays","schema":{"type":"number","minimum":1,"maximum":90,"default":14}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"summary":"AI inventory turnover & dead stock analysis","description":"W25-I — identifies dead stock, slow movers, reorder risk. Capital-tied-up calculation + AI procurement/clearance recommendations."}},"/api/v1/ai/tax-optimizer":{"get":{"responses":{"200":{"description":"Tax optimization report","content":{"application/json":{"schema":{"type":"object","properties":{"periodDays":{"type":"integer","minimum":30,"maximum":365,"description":"Ausgewerteter Zeitraum in Tagen"},"taxYear":{"type":"integer","minimum":2020,"maximum":2030,"description":"Betrachtetes Steuerjahr"},"revenue":{"type":"number","description":"Umsatz aus den Rechnungen des Zeitraums in EUR"},"totalExpenses":{"type":"number","description":"Aufwendungen des Zeitraums in EUR"},"vatCollected":{"type":"number","description":"Vereinnahmte Umsatzsteuer in EUR"},"vatPaid":{"type":"number","description":"Gezahlte Vorsteuer in EUR"},"gwgCandidates":{"type":"integer","minimum":0,"description":"Anzahl der Anschaffungen zwischen 250 und 800 EUR netto (GWG-Kandidaten)"},"tips":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string","enum":["vat_anomaly","gwg_opportunity","iab_opportunity","expense_deduction","timing_optimization","general"],"description":"Themengruppe des Hinweises"},"priority":{"type":"string","enum":["high","medium","low"],"description":"Dringlichkeit des Hinweises"},"title":{"type":"string","description":"Kurztitel des Hinweises"},"description":{"type":"string","description":"Erlaeuterung in deutscher Sprache"},"estimatedSaving":{"type":["number","null"],"description":"Grob geschaetzte Jahresersparnis in EUR; null wenn nicht bezifferbar"},"legalRef":{"type":["string","null"],"description":"Fundstelle im deutschen Steuerrecht; null wenn keine angegeben ist"}},"required":["category","priority","title","description","estimatedSaving","legalRef"]},"description":"Regelbasierte Hinweise, ergaenzt um die des Sprachmodells"},"summary":{"type":["string","null"],"description":"Gesamteinschaetzung des Sprachmodells; null ohne hinterlegten Schluessel"},"aiEnriched":{"type":"boolean","description":"true, wenn das Sprachmodell etwas beigesteuert hat"},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Berechnung"}},"required":["periodDays","taxYear","revenue","totalExpenses","vatCollected","vatPaid","gwgCandidates","tips","summary","aiEnriched","generatedAt"]},"example":{"periodDays":30,"taxYear":2020,"revenue":0,"totalExpenses":0,"vatCollected":0,"vatPaid":0,"gwgCandidates":0,"tips":[{"category":"vat_anomaly","priority":"high","title":"string","description":"string","estimatedSaving":0,"legalRef":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"message":{"type":"string"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","message","retryAfter"]}}}}},"operationId":"getApiV1AiTax-optimizer","tags":["ai","Finance"],"parameters":[{"in":"query","name":"periodDays","schema":{"type":"number","minimum":30,"maximum":365,"default":90}},{"in":"query","name":"taxYear","schema":{"type":"number","minimum":2020,"maximum":2030,"description":"Tax year to analyse. Defaults to the current year."}}],"summary":"AI tax optimization tips","description":"W25-J — surfaces German tax optimization opportunities (VAT anomalies, GWG, IAB, timing) from invoice + purchasing data. German-language steuerliche Handlungsempfehlungen via claude-haiku-4-5."}},"/api/v1/ai/hr-analytics":{"get":{"responses":{"200":{"description":"HR analytics report","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"number"},"overtimeThreshold":{"type":"number"},"totalEmployees":{"type":"number"},"overtimeFlagCount":{"type":"number"},"highAbsenceCount":{"type":"number"},"lowBillableCount":{"type":"number"},"avgBillableRatio":{"type":["number","null"]},"totalHoursLogged":{"type":"number"},"employees":{"type":"array","items":{"type":"object","properties":{"employeeId":{"type":"string"},"employeeName":{"type":"string"},"department":{"type":["string","null"]},"totalHours":{"type":"number"},"avgWeeklyHours":{"type":"number"},"billableHours":{"type":"number"},"billableRatio":{"type":["number","null"]},"overtimeWeeks":{"type":"number"},"absenceDays":{"type":"number"},"absenceRate":{"type":["number","null"]},"tenureMonths":{"type":["number","null"]},"flags":{"type":"array","items":{"type":"string"}},"aiSummary":{"type":["string","null"]}},"required":["employeeId","employeeName","department","totalHours","avgWeeklyHours","billableHours","billableRatio","overtimeWeeks","absenceDays","absenceRate","tenureMonths","flags","aiSummary"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string","format":"date-time"}},"required":["lookbackDays","overtimeThreshold","totalEmployees","overtimeFlagCount","highAbsenceCount","lowBillableCount","avgBillableRatio","totalHoursLogged","employees","summary","aiEnriched","generatedAt"]},"example":{"lookbackDays":0,"overtimeThreshold":0,"totalEmployees":0,"overtimeFlagCount":0,"highAbsenceCount":0,"lowBillableCount":0,"avgBillableRatio":0,"totalHoursLogged":0,"employees":[{"employeeId":"string","employeeName":"string","department":"string","totalHours":0,"avgWeeklyHours":0,"billableHours":0,"billableRatio":0,"overtimeWeeks":0,"absenceDays":0,"absenceRate":0,"tenureMonths":0,"flags":["string"],"aiSummary":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient role"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiHr-analytics","tags":["ai","HR"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":14,"maximum":365,"default":90}},{"in":"query","name":"overtimeThreshold","schema":{"type":"number","minimum":35,"maximum":60,"default":45}}],"summary":"AI HR analytics","description":"W25-K — overtime patterns, absence rates, billable ratio, tenure distribution. German-language HR recommendations via claude-haiku-4-5."}},"/api/v1/ai/price-recommendations":{"get":{"responses":{"200":{"description":"Price recommendation report. An empty `recommendations` list is a valid answer — it means no product cleared `minTransactions` in the window.","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"integer","description":"The window actually analysed, echoed back"},"totalProducts":{"type":"integer","description":"Number of entries in `recommendations`"},"negativeMarginCount":{"type":"integer"},"lowMarginCount":{"type":"integer","description":"Margin between 0 and the requested lowMarginPct"},"increaseCount":{"type":"integer"},"holdCount":{"type":"integer"},"totalRevenueImpact":{"type":"number","description":"Sum of the per-product yearly impact, rounded to cents"},"recommendations":{"type":"array","items":{"type":"object","properties":{"productId":{"type":["string","null"]},"productName":{"type":"string"},"category":{"type":["string","null"]},"avgSellingPrice":{"type":"number"},"avgUnitCost":{"type":["number","null"],"description":"null when no cost is recorded"},"marginPct":{"type":["number","null"],"description":"null when the cost is unknown"},"salesVolume":{"type":"number"},"totalRevenue":{"type":"number"},"avgDiscountPct":{"type":["number","null"]},"action":{"type":"string","enum":["increase","decrease","hold","review_cost"]},"actionReason":{"type":"string"},"suggestedPrice":{"type":["number","null"],"description":"null when the action is \"hold\""},"revenueImpact":{"type":["number","null"],"description":"Estimated yearly effect of following the action"},"aiSummary":{"type":["string","null"],"description":"null without AI or beyond the products it covered"}},"required":["productId","productName","category","avgSellingPrice","avgUnitCost","marginPct","salesVolume","totalRevenue","avgDiscountPct","action","actionReason","suggestedPrice","revenueImpact","aiSummary"]}},"summary":{"type":["string","null"],"description":"null when the AI step did not run"},"aiEnriched":{"type":"boolean","description":"False means the report is purely rule-based"},"generatedAt":{"type":"string"}},"required":["lookbackDays","totalProducts","negativeMarginCount","lowMarginCount","increaseCount","holdCount","totalRevenueImpact","recommendations","summary","aiEnriched","generatedAt"]},"example":{"lookbackDays":0,"totalProducts":0,"negativeMarginCount":0,"lowMarginCount":0,"increaseCount":0,"holdCount":0,"totalRevenueImpact":0,"recommendations":[{"productId":"string","productName":"string","category":"string","avgSellingPrice":0,"avgUnitCost":0,"marginPct":0,"salesVolume":0,"totalRevenue":0,"avgDiscountPct":0,"action":"increase","actionReason":"string","suggestedPrice":0,"revenueImpact":0,"aiSummary":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiPrice-recommendations","tags":["ai","Sales"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":14,"maximum":365,"default":90}},{"in":"query","name":"minTransactions","schema":{"type":"number","minimum":1,"maximum":50,"default":3}},{"in":"query","name":"lowMarginPct","schema":{"type":"number","minimum":0,"maximum":50,"default":20}}],"summary":"AI price recommendations","description":"W25-L — margin analysis per product, negative-margin detection, price increase candidates, discount analysis. German-language pricing strategy via claude-haiku-4-5."}},"/api/v1/ai/lead-intelligence":{"get":{"responses":{"200":{"description":"Lead intelligence report. Score, conversion probability, temperature and `nextBestAction` are computed ARITHMETICALLY from stage, value, age and last activity; the AI only writes the German closing strategy. Without a configured model the report still ships with `aiEnriched: false` and every `aiSummary` null. WATCH `degraded`: it is present only when the lead query FAILED — without it, an empty `leads` list would look the same as a tenant with no open opportunities. `leads` is capped by `limit`, while the counts above it cover every lead that passed `minScore` — the two do not have to add up.","content":{"application/json":{"schema":{"type":"object","properties":{"totalLeads":{"type":"number"},"hotCount":{"type":"number"},"warmCount":{"type":"number"},"coldCount":{"type":"number"},"stalledCount":{"type":"number"},"totalPipelineValue":{"type":"number"},"weightedPipelineValue":{"type":"number"},"leads":{"type":"array","items":{"type":"object","properties":{"leadId":{"type":"string"},"leadName":{"type":"string"},"company":{"type":["string","null"]},"stage":{"type":"string"},"dealValue":{"type":["number","null"]},"source":{"type":["string","null"]},"score":{"type":"number"},"conversionPct":{"type":"number"},"temperature":{"type":"string","enum":["hot","warm","cold","stalled"]},"stageDays":{"type":"number"},"lastActivityDays":{"type":["number","null"]},"isStalled":{"type":"boolean"},"nextBestAction":{"type":"string"},"aiSummary":{"type":["string","null"]}},"required":["leadId","leadName","company","stage","dealValue","source","score","conversionPct","temperature","stageDays","lastActivityDays","isStalled","nextBestAction","aiSummary"],"additionalProperties":false}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"},"degraded":{"type":"boolean","const":true}},"required":["totalLeads","hotCount","warmCount","coldCount","stalledCount","totalPipelineValue","weightedPipelineValue","leads","summary","aiEnriched","generatedAt"],"additionalProperties":false},"example":{"totalLeads":0,"hotCount":0,"warmCount":0,"coldCount":0,"stalledCount":0,"totalPipelineValue":0,"weightedPipelineValue":0,"leads":[{"leadId":"string","leadName":"string","company":"string","stage":"string","dealValue":0,"source":"string","score":0,"conversionPct":0,"temperature":"hot","stageDays":0,"lastActivityDays":0,"isStalled":true,"nextBestAction":"string","aiSummary":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string","degraded":true}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiLead-intelligence","tags":["ai","CRM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"stalledDays","schema":{"type":"number","minimum":3,"maximum":90,"default":14}},{"in":"query","name":"minScore","schema":{"type":"number","minimum":0,"maximum":100,"default":0}}],"summary":"AI lead intelligence & scoring","description":"W25-M — real-time multi-signal lead scoring, conversion probability, stalled deal detection, next-best-action. German-language closing strategy via claude-haiku-4-5."}},"/api/v1/ai/payment-prediction":{"get":{"responses":{"200":{"description":"Payment prediction report","content":{"application/json":{"schema":{"type":"object","properties":{"horizonDays":{"type":"integer","minimum":14,"maximum":180,"description":"Vorschauzeitraum in Tagen"},"openInvoiceCount":{"type":"integer","minimum":0,"description":"Anzahl der zurueckgegebenen offenen Rechnungen"},"totalOpenAmount":{"type":"number","description":"Summe der offenen Betraege in EUR"},"reliableAmount":{"type":"number","description":"Davon aus Rechnungen der Stufe reliable"},"atRiskAmount":{"type":"number","description":"Davon aus Rechnungen der Stufe at_risk"},"badDebtAmount":{"type":"number","description":"Davon aus Rechnungen der Stufe bad_debt"},"cashFlowBuckets":{"type":"array","items":{"type":"object","properties":{"weekStart":{"type":"string","description":"Erster Tag der Woche (YYYY-MM-DD)"},"expected":{"type":"number","description":"Erwarteter Zahlungseingang dieser Woche in EUR"},"pessimistic":{"type":"number","description":"Untere Schaetzung in EUR"},"optimistic":{"type":"number","description":"Obere Schaetzung in EUR"}},"required":["weekStart","expected","pessimistic","optimistic"]},"description":"Erwartete Eingaenge je Woche im Vorschauzeitraum"},"openInvoices":{"type":"array","items":{"type":"object","properties":{"invoiceId":{"type":"string","description":"Kennung der Rechnung"},"invoiceNumber":{"type":"string","description":"Rechnungsnummer"},"customerId":{"type":"string","description":"Kennung des Kunden"},"customerName":{"type":"string","description":"Kundenname"},"amount":{"type":"number","description":"Offener Betrag in EUR"},"dueDate":{"type":"string","description":"Faelligkeitsdatum (YYYY-MM-DD)"},"daysUntilDue":{"type":"integer","description":"Tage bis zur Faelligkeit; negativ wenn bereits ueberfaellig"},"predictedDaysLate":{"type":"number","description":"Erwarteter Verzug in Tagen aus der Kundenhistorie"},"predictedPaymentDate":{"type":"string","description":"Erwarteter Zahlungseingang (YYYY-MM-DD)"},"riskTier":{"type":"string","enum":["reliable","slow_payer","at_risk","bad_debt"],"description":"Risikostufe, abgeleitet aus Zahlungsverzug und Verzugsquote der Vergangenheit"},"riskScore":{"type":"integer","minimum":0,"maximum":100,"description":"Risikopunktwert 0-100, aus Stufe und Verzug gerechnet"},"aiSummary":{"type":["string","null"],"description":"Einschaetzung des Sprachmodells zu dieser Rechnung; null wenn nicht erzeugt"}},"required":["invoiceId","invoiceNumber","customerId","customerName","amount","dueDate","daysUntilDue","predictedDaysLate","predictedPaymentDate","riskTier","riskScore","aiSummary"]},"description":"Offene Rechnungen, absteigend nach riskScore"},"customerProfiles":{"type":"array","items":{"type":"object","properties":{"customerId":{"type":"string","description":"Kennung des Kunden"},"customerName":{"type":"string","description":"Kundenname"},"avgDaysLate":{"type":"number","description":"Mittlerer Zahlungsverzug in Tagen aus den bezahlten Rechnungen"},"lateFrequencyPct":{"type":"number","description":"Anteil verspaetet bezahlter Rechnungen in Prozent"},"paidInvoiceCount":{"type":"integer","minimum":0,"description":"Anzahl der ausgewerteten bezahlten Rechnungen"},"riskTier":{"type":"string","enum":["reliable","slow_payer","at_risk","bad_debt"],"description":"Risikostufe, abgeleitet aus Zahlungsverzug und Verzugsquote der Vergangenheit"}},"required":["customerId","customerName","avgDaysLate","lateFrequencyPct","paidInvoiceCount","riskTier"]},"description":"Zahlungsprofile der beteiligten Kunden, riskanteste Stufe zuerst"},"summary":{"type":["string","null"],"description":"Deutschsprachige Mahnstrategie des Sprachmodells; null ohne hinterlegten Schluessel"},"aiEnriched":{"type":"boolean","description":"true, wenn das Sprachmodell etwas beigesteuert hat"},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Berechnung"}},"required":["horizonDays","openInvoiceCount","totalOpenAmount","reliableAmount","atRiskAmount","badDebtAmount","cashFlowBuckets","openInvoices","customerProfiles","summary","aiEnriched","generatedAt"]},"example":{"horizonDays":14,"openInvoiceCount":0,"totalOpenAmount":0,"reliableAmount":0,"atRiskAmount":0,"badDebtAmount":0,"cashFlowBuckets":[{"weekStart":"string","expected":0,"pessimistic":0,"optimistic":0}],"openInvoices":[{"invoiceId":"string","invoiceNumber":"string","customerId":"string","customerName":"string","amount":0,"dueDate":"string","daysUntilDue":0,"predictedDaysLate":0,"predictedPaymentDate":"string","riskTier":"reliable","riskScore":0,"aiSummary":"string"}],"customerProfiles":[{"customerId":"string","customerName":"string","avgDaysLate":0,"lateFrequencyPct":0,"paidInvoiceCount":0,"riskTier":"reliable"}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"message":{"type":"string"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","message","retryAfter"]}}}}},"operationId":"getApiV1AiPayment-prediction","tags":["ai","Finance"],"parameters":[{"in":"query","name":"horizonDays","schema":{"type":"number","minimum":14,"maximum":180,"default":90}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"summary":"AI payment behavior prediction","description":"W25-N — predicts late payment risk per open invoice based on customer history. Cash flow buckets + risk tiers. AI collection strategy via claude-haiku-4-5."}},"/api/v1/ai/dunning-optimizer":{"get":{"responses":{"200":{"description":"Dunning optimizer report. Also the answer when the tenant has nothing overdue or the invoice query fails — the report is then empty rather than an error.","content":{"application/json":{"schema":{"type":"object","properties":{"totalOverdue":{"type":"integer","description":"Number of entries — after the limit was applied"},"totalAmount":{"type":"number","description":"Sum of all listed overdue invoices, rounded to cents"},"buckets":{"type":"array","items":{"type":"object","properties":{"bucket":{"type":"string","enum":["1_14","15_30","31_60","60_plus"]},"label":{"type":"string","description":"German label of the bucket, e.g. \"15–30 Tage\""},"count":{"type":"integer"},"totalAmount":{"type":"number"}},"required":["bucket","label","count","totalAmount"]},"description":"Always all four buckets, in ascending order of days overdue"},"entries":{"type":"array","items":{"type":"object","properties":{"invoiceId":{"type":"string"},"invoiceNumber":{"type":"string"},"customerId":{"type":"string","description":"\"unknown\" when the invoice carries no customer"},"customerName":{"type":"string","description":"\"Unbekannt\" when the invoice carries no name"},"amount":{"type":"number"},"dueDate":{"type":"string"},"daysOverdue":{"type":"integer"},"overdueBucket":{"type":"string","enum":["1_14","15_30","31_60","60_plus"]},"dunningLevel":{"type":"string","enum":["friendly_reminder","first_notice","second_notice","legal_action"]},"customerLtv":{"type":["number","null"],"description":"Sum of the customer's paid invoices; null when unknown"},"priorityScore":{"type":"integer","description":"0..100 from amount, days overdue and LTV — higher means act sooner"},"actionReason":{"type":"string","description":"German one-liner explaining the recommended level"},"aiSummary":{"type":["string","null"],"description":"Drafted dunning text; null without AI or beyond the top eight"}},"required":["invoiceId","invoiceNumber","customerId","customerName","amount","dueDate","daysOverdue","overdueBucket","dunningLevel","customerLtv","priorityScore","actionReason","aiSummary"]},"description":"Sorted by priorityScore, highest first"},"summary":{"type":["string","null"],"description":"Overall assessment; null when the AI step did not run"},"aiEnriched":{"type":"boolean","description":"False means the report is purely rule-based"},"generatedAt":{"type":"string"}},"required":["totalOverdue","totalAmount","buckets","entries","summary","aiEnriched","generatedAt"]},"example":{"totalOverdue":0,"totalAmount":0,"buckets":[{"bucket":"1_14","label":"string","count":0,"totalAmount":0}],"entries":[{"invoiceId":"string","invoiceNumber":"string","customerId":"string","customerName":"string","amount":0,"dueDate":"string","daysOverdue":0,"overdueBucket":"1_14","dunningLevel":"friendly_reminder","customerLtv":0,"priorityScore":0,"actionReason":"string","aiSummary":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiDunning-optimizer","tags":["ai","Finance"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":30}}],"summary":"AI dunning optimizer","description":"W25-O — recommends optimal dunning level per overdue invoice (reminder/1st/2nd/legal) considering customer LTV. AI drafts personalised German dunning text via claude-haiku-4-5."}},"/api/v1/ai/supplier-analysis":{"get":{"responses":{"200":{"description":"Supplier analysis report","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackMonths":{"type":"number"},"totalSuppliers":{"type":"number"},"totalSpend":{"type":"number"},"categories":{"type":"array","items":{"type":"object","properties":{"tier":{"type":"string","enum":["strategic","reliable","review_needed","unreliable"]},"label":{"type":"string"},"count":{"type":"number"},"totalSpend":{"type":"number"}},"required":["tier","label","count","totalSpend"]}},"suppliers":{"type":"array","items":{"type":"object","properties":{"supplierId":{"type":"string"},"supplierName":{"type":"string"},"totalOrders":{"type":"number"},"totalSpend":{"type":"number"},"avgOrderValue":{"type":"number"},"orderFrequency":{"type":"number"},"onTimeRate":{"type":"number"},"avgDeliveryDays":{"type":"number"},"priceTrendPct":{"type":"number"},"singleSourceItems":{"type":"number"},"tier":{"type":"string","enum":["strategic","reliable","review_needed","unreliable"]},"importanceScore":{"type":"number"},"riskFlags":{"type":"array","items":{"type":"string"}},"aiRecommendation":{"type":["string","null"]}},"required":["supplierId","supplierName","totalOrders","totalSpend","avgOrderValue","orderFrequency","onTimeRate","avgDeliveryDays","priceTrendPct","singleSourceItems","tier","importanceScore","riskFlags","aiRecommendation"]}},"topRiskSuppliers":{"type":"array","items":{"type":"object","properties":{"supplierId":{"type":"string"},"supplierName":{"type":"string"},"totalOrders":{"type":"number"},"totalSpend":{"type":"number"},"avgOrderValue":{"type":"number"},"orderFrequency":{"type":"number"},"onTimeRate":{"type":"number"},"avgDeliveryDays":{"type":"number"},"priceTrendPct":{"type":"number"},"singleSourceItems":{"type":"number"},"tier":{"type":"string","enum":["strategic","reliable","review_needed","unreliable"]},"importanceScore":{"type":"number"},"riskFlags":{"type":"array","items":{"type":"string"}},"aiRecommendation":{"type":["string","null"]}},"required":["supplierId","supplierName","totalOrders","totalSpend","avgOrderValue","orderFrequency","onTimeRate","avgDeliveryDays","priceTrendPct","singleSourceItems","tier","importanceScore","riskFlags","aiRecommendation"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string","format":"date-time"}},"required":["lookbackMonths","totalSuppliers","totalSpend","categories","suppliers","topRiskSuppliers","summary","aiEnriched","generatedAt"]},"example":{"lookbackMonths":0,"totalSuppliers":0,"totalSpend":0,"categories":[{"tier":"strategic","label":"string","count":0,"totalSpend":0}],"suppliers":[{"supplierId":"string","supplierName":"string","totalOrders":0,"totalSpend":0,"avgOrderValue":0,"orderFrequency":0,"onTimeRate":0,"avgDeliveryDays":0,"priceTrendPct":0,"singleSourceItems":0,"tier":"strategic","importanceScore":0,"riskFlags":["string"],"aiRecommendation":"string"}],"topRiskSuppliers":[{"supplierId":"string","supplierName":"string","totalOrders":0,"totalSpend":0,"avgOrderValue":0,"orderFrequency":0,"onTimeRate":0,"avgDeliveryDays":0,"priceTrendPct":0,"singleSourceItems":0,"tier":"strategic","importanceScore":0,"riskFlags":["string"],"aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiSupplier-analysis","tags":["ai","purchasing"],"parameters":[{"in":"query","name":"lookbackMonths","schema":{"type":"number","minimum":1,"maximum":36,"default":12}},{"in":"query","name":"minOrders","schema":{"type":"number","minimum":1,"maximum":20,"default":2}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}}],"summary":"AI supplier performance analysis","description":"W25-P — scores suppliers on on-time delivery, price trend, order frequency, and single-source risk. Tier: strategic/reliable/review_needed/unreliable. AI relationship recommendations via claude-haiku-4-5."}},"/api/v1/ai/product-performance":{"get":{"responses":{"200":{"description":"Product performance report","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"integer"},"totalProducts":{"type":"integer"},"totalRevenue":{"type":"number"},"categories":{"type":"array","items":{"type":"object","properties":{"tier":{"type":"string","enum":["star","steady","declining","dormant"]},"label":{"type":"string"},"count":{"type":"integer"},"totalRevenue":{"type":"number"}},"required":["tier","label","count","totalRevenue"]}},"products":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string"},"productName":{"type":"string"},"revenue":{"type":"number"},"unitsSold":{"type":"number"},"avgPrice":{"type":"number"},"avgMarginPct":{"type":["number","null"],"description":"null when no cost price is stored on the line items"},"revenueTrendPct":{"type":"number","description":"Revenue change vs the prior period of the same length (%)"},"currentStock":{"type":["number","null"],"description":"null when the product has no inventory row"},"daysOfStock":{"type":["number","null"],"description":"null without stock data or without sales velocity"},"tier":{"type":"string","enum":["star","steady","declining","dormant"]},"portfolioScore":{"type":"number","description":"0-100: higher = more valuable in the portfolio"},"insights":{"type":"array","items":{"type":"string"}},"aiRecommendation":{"type":["string","null"],"description":"German measure — null unless the LLM enrichment ran"}},"required":["productId","productName","revenue","unitsSold","avgPrice","avgMarginPct","revenueTrendPct","currentStock","daysOfStock","tier","portfolioScore","insights","aiRecommendation"]}},"topStars":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string"},"productName":{"type":"string"},"revenue":{"type":"number"},"unitsSold":{"type":"number"},"avgPrice":{"type":"number"},"avgMarginPct":{"type":["number","null"],"description":"null when no cost price is stored on the line items"},"revenueTrendPct":{"type":"number","description":"Revenue change vs the prior period of the same length (%)"},"currentStock":{"type":["number","null"],"description":"null when the product has no inventory row"},"daysOfStock":{"type":["number","null"],"description":"null without stock data or without sales velocity"},"tier":{"type":"string","enum":["star","steady","declining","dormant"]},"portfolioScore":{"type":"number","description":"0-100: higher = more valuable in the portfolio"},"insights":{"type":"array","items":{"type":"string"}},"aiRecommendation":{"type":["string","null"],"description":"German measure — null unless the LLM enrichment ran"}},"required":["productId","productName","revenue","unitsSold","avgPrice","avgMarginPct","revenueTrendPct","currentStock","daysOfStock","tier","portfolioScore","insights","aiRecommendation"]},"description":"Up to 5 products in the star tier"},"atRiskProducts":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string"},"productName":{"type":"string"},"revenue":{"type":"number"},"unitsSold":{"type":"number"},"avgPrice":{"type":"number"},"avgMarginPct":{"type":["number","null"],"description":"null when no cost price is stored on the line items"},"revenueTrendPct":{"type":"number","description":"Revenue change vs the prior period of the same length (%)"},"currentStock":{"type":["number","null"],"description":"null when the product has no inventory row"},"daysOfStock":{"type":["number","null"],"description":"null without stock data or without sales velocity"},"tier":{"type":"string","enum":["star","steady","declining","dormant"]},"portfolioScore":{"type":"number","description":"0-100: higher = more valuable in the portfolio"},"insights":{"type":"array","items":{"type":"string"}},"aiRecommendation":{"type":["string","null"],"description":"German measure — null unless the LLM enrichment ran"}},"required":["productId","productName","revenue","unitsSold","avgPrice","avgMarginPct","revenueTrendPct","currentStock","daysOfStock","tier","portfolioScore","insights","aiRecommendation"]},"description":"Up to 5 declining products or products with under 14 days of stock"},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["lookbackDays","totalProducts","totalRevenue","categories","products","topStars","atRiskProducts","summary","aiEnriched","generatedAt"]},"example":{"lookbackDays":0,"totalProducts":0,"totalRevenue":0,"categories":[{"tier":"star","label":"string","count":0,"totalRevenue":0}],"products":[{"productId":"string","productName":"string","revenue":0,"unitsSold":0,"avgPrice":0,"avgMarginPct":0,"revenueTrendPct":0,"currentStock":0,"daysOfStock":0,"tier":"star","portfolioScore":0,"insights":["string"],"aiRecommendation":"string"}],"topStars":[{"productId":"string","productName":"string","revenue":0,"unitsSold":0,"avgPrice":0,"avgMarginPct":0,"revenueTrendPct":0,"currentStock":0,"daysOfStock":0,"tier":"star","portfolioScore":0,"insights":["string"],"aiRecommendation":"string"}],"atRiskProducts":[{"productId":"string","productName":"string","revenue":0,"unitsSold":0,"avgPrice":0,"avgMarginPct":0,"revenueTrendPct":0,"currentStock":0,"daysOfStock":0,"tier":"star","portfolioScore":0,"insights":["string"],"aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiProduct-performance","tags":["ai","Sales"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":7,"maximum":365,"default":90}},{"in":"query","name":"minTransactions","schema":{"type":"number","minimum":1,"maximum":20,"default":2}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"summary":"AI product performance analysis","description":"W25-Q — scores products on revenue trend, margin, and sales velocity. Tiers: star/steady/declining/dormant. Stock runway + reorder alerts. AI portfolio strategy via claude-haiku-4-5."}},"/api/v1/ai/employee-turnover":{"get":{"responses":{"200":{"description":"Employee turnover risk report. `riskScore` and `riskTier` are computed ARITHMETICALLY from absence, hours, tenure and billable ratio — `riskFactors` names the inputs that pushed the score up. The AI only writes the HR recommendation; when no model is configured or the call fails, the report still ships with `aiEnriched: false`, `summary: null` and every `aiRecommendation` null. This is a STATISTICAL hint about working patterns, never a statement about a person.","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"number"},"totalEmployees":{"type":"number"},"highRiskCount":{"type":"number"},"criticalRiskCount":{"type":"number"},"avgRiskScore":{"type":"number"},"departmentRisks":{"type":"array","items":{"type":"object","properties":{"department":{"type":"string"},"employeeCount":{"type":"number"},"highRiskCount":{"type":"number"},"avgRiskScore":{"type":"number"}},"required":["department","employeeCount","highRiskCount","avgRiskScore"],"additionalProperties":false}},"employees":{"type":"array","items":{"type":"object","properties":{"employeeId":{"type":"string"},"employeeName":{"type":"string"},"department":{"type":["string","null"]},"tenureMonths":{"type":"number"},"sickDays":{"type":"number"},"sickDaysVsAvgDelta":{"type":"number"},"avgWeeklyHours":{"type":"number"},"billableRatioPct":{"type":["number","null"]},"inTenureCliff":{"type":"boolean"},"riskTier":{"type":"string","enum":["low","medium","high","critical"]},"riskScore":{"type":"number"},"riskFactors":{"type":"array","items":{"type":"string"}},"aiRecommendation":{"type":["string","null"]}},"required":["employeeId","employeeName","department","tenureMonths","sickDays","sickDaysVsAvgDelta","avgWeeklyHours","billableRatioPct","inTenureCliff","riskTier","riskScore","riskFactors","aiRecommendation"],"additionalProperties":false}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["lookbackDays","totalEmployees","highRiskCount","criticalRiskCount","avgRiskScore","departmentRisks","employees","summary","aiEnriched","generatedAt"],"additionalProperties":false},"example":{"lookbackDays":0,"totalEmployees":0,"highRiskCount":0,"criticalRiskCount":0,"avgRiskScore":0,"departmentRisks":[{"department":"string","employeeCount":0,"highRiskCount":0,"avgRiskScore":0}],"employees":[{"employeeId":"string","employeeName":"string","department":"string","tenureMonths":0,"sickDays":0,"sickDaysVsAvgDelta":0,"avgWeeklyHours":0,"billableRatioPct":0,"inTenureCliff":true,"riskTier":"low","riskScore":0,"riskFactors":["string"],"aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiEmployee-turnover","tags":["ai","HR"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":14,"maximum":365,"default":90}},{"in":"query","name":"overtimeThreshold","schema":{"type":"number","minimum":35,"maximum":80,"default":45}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":30}}],"summary":"AI employee turnover risk analysis","description":"W25-R — predicts flight risk per employee from absence spikes, overtime burden, tenure cliff zones, and low billable ratios. Tier: low/medium/high/critical. AI HR action recommendations via claude-haiku-4-5."}},"/api/v1/ai/cash-gap":{"get":{"responses":{"200":{"description":"Cash gap report","content":{"application/json":{"schema":{"type":"object","properties":{"horizonWeeks":{"type":"integer","minimum":1,"maximum":26,"description":"Ausgewerteter Vorschauzeitraum in Wochen"},"openingBalance":{"type":"number","description":"Uebergebenes Startguthaben in EUR"},"totalExpectedInflow":{"type":"number","description":"Summe aller erwarteten Zuflüsse im Zeitraum"},"totalExpectedOutflow":{"type":"number","description":"Summe aller erwarteten Abflüsse im Zeitraum"},"gapWeekCount":{"type":"integer","minimum":0,"description":"Anzahl der Wochen mit negativem Saldo"},"maxShortfall":{"type":"number","description":"Groesste Unterdeckung im Zeitraum in EUR"},"weeklyFlows":{"type":"array","items":{"type":"object","properties":{"weekStart":{"type":"string","description":"Erster Tag der Woche (YYYY-MM-DD), gezaehlt ab heute"},"expectedInflow":{"type":"number","description":"Erwartete Zuflüsse dieser Woche in EUR, auf zwei Stellen gerundet"},"expectedOutflow":{"type":"number","description":"Erwartete Abflüsse dieser Woche in EUR, auf zwei Stellen gerundet"},"netFlow":{"type":"number","description":"expectedInflow minus expectedOutflow"},"cumulativeBalance":{"type":"number","description":"Aufgelaufener Saldo ab openingBalance nach dieser Woche"},"isGapWeek":{"type":"boolean","description":"true, wenn cumulativeBalance unter null faellt"}},"required":["weekStart","expectedInflow","expectedOutflow","netFlow","cumulativeBalance","isGapWeek"]},"description":"Eine Zeile je Woche, chronologisch"},"gapDetails":{"type":"array","items":{"type":"object","properties":{"weekStart":{"type":"string","description":"Erster Tag der Woche mit Fehlbetrag (YYYY-MM-DD)"},"shortfall":{"type":"number","description":"Betrag der Unterdeckung in EUR (Betrag des negativen Saldos)"},"overdueReceivables":{"type":"array","items":{"type":"object","properties":{"invoiceNumber":{"type":"string","description":"Rechnungsnummer"},"amount":{"type":"number","description":"Rechnungsbetrag in EUR"},"daysOverdue":{"type":"integer","description":"Tage seit Faelligkeit"}},"required":["invoiceNumber","amount","daysOverdue"]},"description":"Hoechstens fuenf ueberfaellige Rechnungen, die in dieser Woche erwartet werden"},"largestPayables":{"type":"array","items":{"type":"object","properties":{"poNumber":{"type":"string","description":"Bestellnummer; ersatzweise die Kennung der Bestellung"},"amount":{"type":"number","description":"Bestellwert in EUR"},"supplierName":{"type":"string","description":"Lieferant; \"Unbekannter Lieferant\" wenn nicht erfasst"}},"required":["poNumber","amount","supplierName"]},"description":"Die drei groessten faelligen Bestellungen dieser Woche, absteigend"},"suggestedActions":{"type":"array","items":{"type":"string"},"description":"Regelbasierte Vorschlaege (Mahnlauf, Zahlungsziel, Kontokorrent, Factoring)"}},"required":["weekStart","shortfall","overdueReceivables","largestPayables","suggestedActions"]},"description":"Nur die Wochen mit negativem Saldo"},"summary":{"type":["string","null"],"description":"Deutschsprachige Einschaetzung des Sprachmodells; null wenn kein Schluessel hinterlegt ist oder die Antwort unbrauchbar war"},"aiEnriched":{"type":"boolean","description":"true, wenn summary vom Sprachmodell stammt"},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Berechnung"}},"required":["horizonWeeks","openingBalance","totalExpectedInflow","totalExpectedOutflow","gapWeekCount","maxShortfall","weeklyFlows","gapDetails","summary","aiEnriched","generatedAt"]},"example":{"horizonWeeks":1,"openingBalance":0,"totalExpectedInflow":0,"totalExpectedOutflow":0,"gapWeekCount":0,"maxShortfall":0,"weeklyFlows":[{"weekStart":"string","expectedInflow":0,"expectedOutflow":0,"netFlow":0,"cumulativeBalance":0,"isGapWeek":true}],"gapDetails":[{"weekStart":"string","shortfall":0,"overdueReceivables":[{"invoiceNumber":"string","amount":0,"daysOverdue":0}],"largestPayables":[{"poNumber":"string","amount":0,"supplierName":"string"}],"suggestedActions":["string"]}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"message":{"type":"string"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","message","retryAfter"]}}}}},"operationId":"getApiV1AiCash-gap","tags":["ai","Finance"],"parameters":[{"in":"query","name":"horizonWeeks","schema":{"type":"number","minimum":1,"maximum":26,"default":12}},{"in":"query","name":"openingBalance","schema":{"type":"number","minimum":0,"default":0}}],"summary":"AI cash gap detector","description":"W25-S — forecasts weekly net cash flow over next N weeks by comparing expected receivables vs payables. Surfaces gap weeks and recommends bridge actions. AI liquidity strategy via claude-haiku-4-5."}},"/api/v1/ai/quality-predictor":{"get":{"responses":{"200":{"description":"Quality predictor report","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"integer","minimum":7,"maximum":365,"description":"Ausgewerteter Zeitraum in Tagen"},"totalProducts":{"type":"integer","minimum":0,"description":"Anzahl der ausgewerteten Produkte"},"totalDefects":{"type":"integer","minimum":0,"description":"Summe aller Fehlerereignisse im Zeitraum"},"totalCopq":{"type":"number","description":"Summe der geschaetzten Fehlerkosten in EUR"},"categories":{"type":"array","items":{"type":"object","properties":{"tier":{"type":"string","enum":["critical","elevated","acceptable","low"],"description":"Risikostufe aus Fehlerquote und Trend"},"label":{"type":"string","description":"Deutsche Beschriftung der Stufe (Kritisch, Erhöht, Akzeptabel, Gering)"},"count":{"type":"integer","minimum":0,"description":"Anzahl Produkte in dieser Stufe"},"totalCopq":{"type":"number","description":"Summe der geschaetzten Fehlerkosten dieser Stufe in EUR"}},"required":["tier","label","count","totalCopq"]},"description":"Je ein Eintrag fuer critical, elevated, acceptable und low — auch bei null Produkten"},"products":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","description":"Kennung des Produkts"},"productName":{"type":"string","description":"Produktbezeichnung"},"totalUnits":{"type":"number","description":"Im Zeitraum produzierte bzw. gelieferte Einheiten"},"defectCount":{"type":"integer","minimum":0,"description":"Anzahl der Fehlerereignisse im Zeitraum"},"defectRatePer100":{"type":"number","description":"Fehler je 100 Einheiten"},"trendPct":{"type":"number","description":"Veraenderung der Fehlerquote gegenueber dem gleich langen Vorzeitraum in Prozent"},"copqEstimate":{"type":"number","description":"Geschaetzte Fehlerkosten in EUR (Nacharbeit, Ruecklaeufer, Gewaehrleistung)"},"topDefectType":{"type":["string","null"],"description":"Haeufigste Fehlerart; null wenn keine erfasst ist"},"rootCause":{"type":"string","enum":["material","process","supplier","unknown"],"description":"Aus der Fehlerart abgeleitete Ursachengruppe; unknown wenn kein Muster passt"},"riskTier":{"type":"string","enum":["critical","elevated","acceptable","low"],"description":"Risikostufe aus Fehlerquote und Trend"},"urgencyScore":{"type":"number","description":"Dringlichkeit 0-100, hoeher heisst dringender"},"recommendations":{"type":"array","items":{"type":"string"},"description":"Regelbasierte Massnahmenvorschlaege"},"aiRecommendation":{"type":["string","null"],"description":"Massnahme des Sprachmodells zu diesem Produkt; null wenn keine erzeugt wurde"}},"required":["productId","productName","totalUnits","defectCount","defectRatePer100","trendPct","copqEstimate","topDefectType","rootCause","riskTier","urgencyScore","recommendations","aiRecommendation"]},"description":"Die ausgewerteten Produkte"},"criticalProducts":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","description":"Kennung des Produkts"},"productName":{"type":"string","description":"Produktbezeichnung"},"totalUnits":{"type":"number","description":"Im Zeitraum produzierte bzw. gelieferte Einheiten"},"defectCount":{"type":"integer","minimum":0,"description":"Anzahl der Fehlerereignisse im Zeitraum"},"defectRatePer100":{"type":"number","description":"Fehler je 100 Einheiten"},"trendPct":{"type":"number","description":"Veraenderung der Fehlerquote gegenueber dem gleich langen Vorzeitraum in Prozent"},"copqEstimate":{"type":"number","description":"Geschaetzte Fehlerkosten in EUR (Nacharbeit, Ruecklaeufer, Gewaehrleistung)"},"topDefectType":{"type":["string","null"],"description":"Haeufigste Fehlerart; null wenn keine erfasst ist"},"rootCause":{"type":"string","enum":["material","process","supplier","unknown"],"description":"Aus der Fehlerart abgeleitete Ursachengruppe; unknown wenn kein Muster passt"},"riskTier":{"type":"string","enum":["critical","elevated","acceptable","low"],"description":"Risikostufe aus Fehlerquote und Trend"},"urgencyScore":{"type":"number","description":"Dringlichkeit 0-100, hoeher heisst dringender"},"recommendations":{"type":"array","items":{"type":"string"},"description":"Regelbasierte Massnahmenvorschlaege"},"aiRecommendation":{"type":["string","null"],"description":"Massnahme des Sprachmodells zu diesem Produkt; null wenn keine erzeugt wurde"}},"required":["productId","productName","totalUnits","defectCount","defectRatePer100","trendPct","copqEstimate","topDefectType","rootCause","riskTier","urgencyScore","recommendations","aiRecommendation"]},"description":"Teilmenge von products mit riskTier=critical"},"summary":{"type":["string","null"],"description":"Gesamteinschaetzung des Sprachmodells; null ohne hinterlegten Schluessel"},"aiEnriched":{"type":"boolean","description":"true, wenn das Sprachmodell etwas beigesteuert hat"},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Berechnung"}},"required":["lookbackDays","totalProducts","totalDefects","totalCopq","categories","products","criticalProducts","summary","aiEnriched","generatedAt"]},"example":{"lookbackDays":7,"totalProducts":0,"totalDefects":0,"totalCopq":0,"categories":[{"tier":"critical","label":"string","count":0,"totalCopq":0}],"products":[{"productId":"string","productName":"string","totalUnits":0,"defectCount":0,"defectRatePer100":0,"trendPct":0,"copqEstimate":0,"topDefectType":"string","rootCause":"material","riskTier":"critical","urgencyScore":0,"recommendations":["string"],"aiRecommendation":"string"}],"criticalProducts":[{"productId":"string","productName":"string","totalUnits":0,"defectCount":0,"defectRatePer100":0,"trendPct":0,"copqEstimate":0,"topDefectType":"string","rootCause":"material","riskTier":"critical","urgencyScore":0,"recommendations":["string"],"aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"message":{"type":"string"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","message","retryAfter"]}}}}},"operationId":"getApiV1AiQuality-predictor","tags":["ai","Quality"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":7,"maximum":365,"default":90}},{"in":"query","name":"minDefects","schema":{"type":"number","minimum":1,"maximum":50,"default":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"summary":"AI quality defect predictor","description":"W25-T — analyses defect/complaint data per product to predict quality risk (critical/elevated/acceptable/low). Defect rate trend, COPQ estimate, root cause classification. AI preventive actions via claude-haiku-4-5."}},"/api/v1/ai/resource-optimizer":{"get":{"responses":{"200":{"description":"Resource optimizer report. Empty lists are a valid answer — the tenant may simply have no time entries in the window.","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"integer","description":"The window actually analysed, echoed back"},"weeklyCapacity":{"type":"number","description":"Assumed hours per week per employee, echoed back"},"totalEmployees":{"type":"integer","description":"Number of entries in `employees`, after the limit"},"overloadedCount":{"type":"integer"},"idleCount":{"type":"integer"},"avgUtilisationPct":{"type":"number","description":"Mean over the listed employees, one decimal; 0 when the list is empty"},"employees":{"type":"array","items":{"type":"object","properties":{"employeeId":{"type":"string"},"employeeName":{"type":"string"},"department":{"type":["string","null"]},"capacityHours":{"type":"number","description":"Capacity for the whole window, from weeklyCapacity"},"loggedHours":{"type":"number"},"utilisationPct":{"type":"number"},"status":{"type":"string","enum":["overloaded","optimal","underutilised","idle"],"description":">=110 overloaded, >=70 optimal, >=20 underutilised, below that idle"},"projectCount":{"type":"integer"},"projects":{"type":"array","items":{"type":"object","properties":{"projectId":{"type":"string"},"projectName":{"type":"string"},"hours":{"type":"number"}},"required":["projectId","projectName","hours"]}},"aiRecommendation":{"type":["string","null"],"description":"null when the AI step did not run or skipped this row"}},"required":["employeeId","employeeName","department","capacityHours","loggedHours","utilisationPct","status","projectCount","projects","aiRecommendation"]}},"projectHealth":{"type":"array","items":{"type":"object","properties":{"projectId":{"type":"string"},"projectName":{"type":"string"},"status":{"type":"string"},"budgetHours":{"type":["number","null"]},"loggedHours":{"type":"number"},"remainingBudgetHours":{"type":["number","null"]},"teamSize":{"type":"integer"},"budgetAtRisk":{"type":"boolean","description":"Budget more than 80 percent used while the project is not finished"},"aiRecommendation":{"type":["string","null"]}},"required":["projectId","projectName","status","budgetHours","loggedHours","remainingBudgetHours","teamSize","budgetAtRisk","aiRecommendation"]}},"reallocationAlerts":{"type":"array","items":{"type":"string"},"description":"Plain-text hints where load could be shifted"},"summary":{"type":["string","null"],"description":"null when the AI step did not run"},"aiEnriched":{"type":"boolean","description":"False means the report is purely rule-based"},"generatedAt":{"type":"string"}},"required":["lookbackDays","weeklyCapacity","totalEmployees","overloadedCount","idleCount","avgUtilisationPct","employees","projectHealth","reallocationAlerts","summary","aiEnriched","generatedAt"]},"example":{"lookbackDays":0,"weeklyCapacity":0,"totalEmployees":0,"overloadedCount":0,"idleCount":0,"avgUtilisationPct":0,"employees":[{"employeeId":"string","employeeName":"string","department":"string","capacityHours":0,"loggedHours":0,"utilisationPct":0,"status":"overloaded","projectCount":0,"projects":[{"projectId":"string","projectName":"string","hours":0}],"aiRecommendation":"string"}],"projectHealth":[{"projectId":"string","projectName":"string","status":"string","budgetHours":0,"loggedHours":0,"remainingBudgetHours":0,"teamSize":0,"budgetAtRisk":true,"aiRecommendation":"string"}],"reallocationAlerts":["string"],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiResource-optimizer","tags":["ai","Projects"],"parameters":[{"in":"query","name":"weeklyCapacity","schema":{"type":"number","minimum":20,"maximum":60,"default":40}},{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":7,"maximum":90,"default":30}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}}],"summary":"AI project resource optimizer","description":"W25-U — maps employee utilisation (overloaded/optimal/underutilised/idle) and project budget health. Surfaces reallocation opportunities. AI staffing recommendations via claude-haiku-4-5."}},"/api/v1/ai/customer-segments":{"get":{"responses":{"200":{"description":"Customer segmentation report. The scores are quantile-based over THIS result set, so they are relative to the customers in the window — not absolute. With no invoices in the window every list is empty and `totalCustomers` is 0; that is a valid answer, not an error.","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackMonths":{"type":"integer","description":"Echoed from the query"},"totalCustomers":{"type":"integer"},"totalRevenue":{"type":"number"},"segments":{"type":"array","items":{"type":"object","properties":{"segment":{"type":"string","description":"vip | loyal | potential_loyalist | new_customer | promising | at_risk | cant_lose | hibernating | lost"},"label":{"type":"string","description":"German name of the segment"},"count":{"type":"integer"},"totalRevenue":{"type":"number"},"avgMonetary":{"type":"number"},"description":{"type":"string","description":"German, fixed per segment"}},"required":["segment","label","count","totalRevenue","avgMonetary","description"]},"description":"Only segments that actually have customers"},"customers":{"type":"array","items":{"type":"object","properties":{"customerId":{"type":"string","description":"\"unknown\" when the invoice carries none"},"customerName":{"type":"string"},"recencyDays":{"type":"number","description":"Days since the last invoice"},"frequency":{"type":"number","description":"Invoices in the window"},"monetary":{"type":"number","description":"Total spend in the window"},"rScore":{"type":"number","description":"1–5, higher is better (fewer days since last order)"},"fScore":{"type":"number","description":"1–5"},"mScore":{"type":"number","description":"1–5"},"rfmTotal":{"type":"number","description":"R+F+M, so 3–15"},"segment":{"type":"string"},"segmentLabel":{"type":"string"},"suggestedAction":{"type":"string","description":"German, rule-based — always present"},"aiRecommendation":{"type":["string","null"],"description":"German, from the model; null when no model ran or it said nothing here"}},"required":["customerId","customerName","recencyDays","frequency","monetary","rScore","fScore","mScore","rfmTotal","segment","segmentLabel","suggestedAction","aiRecommendation"]}},"summary":{"type":["string","null"],"description":"Null unless the model produced one"},"aiEnriched":{"type":"boolean","description":"False means the RFM numbers stand alone — no model ran, or it failed"},"generatedAt":{"type":"string"}},"required":["lookbackMonths","totalCustomers","totalRevenue","segments","customers","summary","aiEnriched","generatedAt"]},"example":{"lookbackMonths":0,"totalCustomers":0,"totalRevenue":0,"segments":[{"segment":"string","label":"string","count":0,"totalRevenue":0,"avgMonetary":0,"description":"string"}],"customers":[{"customerId":"string","customerName":"string","recencyDays":0,"frequency":0,"monetary":0,"rScore":0,"fScore":0,"mScore":0,"rfmTotal":0,"segment":"string","segmentLabel":"string","suggestedAction":"string","aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiCustomer-segments","tags":["ai","CRM"],"parameters":[{"in":"query","name":"lookbackMonths","schema":{"type":"number","minimum":3,"maximum":60,"default":24}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}}],"summary":"AI customer segmentation (RFM)","description":"W25-V — segments customers by Recency/Frequency/Monetary score into vip/loyal/potential_loyalist/new_customer/promising/at_risk/cant_lose/hibernating/lost. AI personalised outreach strategies per segment via claude-haiku-4-5."}},"/api/v1/ai/pipeline-velocity":{"get":{"responses":{"200":{"description":"Pipeline velocity report","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"number"},"velocityScore":{"type":"number"},"avgSalesCycleDays":{"type":"number"},"overallConversionPct":{"type":"number"},"totalPipelineValue":{"type":"number"},"weightedPipelineValue":{"type":"number"},"stageMetrics":{"type":"array","items":{"type":"object","properties":{"stage":{"type":"string"},"avgDwellDays":{"type":"number"},"conversionRatePct":{"type":"number"},"activeDeals":{"type":"number"},"activeValue":{"type":"number"},"isBottleneck":{"type":"boolean"}},"required":["stage","avgDwellDays","conversionRatePct","activeDeals","activeValue","isBottleneck"],"additionalProperties":false}},"bottleneckStage":{"type":["string","null"]},"openDeals":{"type":"array","items":{"type":"object","properties":{"dealId":{"type":"string"},"dealTitle":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"dealValue":{"type":"number"},"currentStage":{"type":"string"},"daysInStage":{"type":"number"},"isStalled":{"type":"boolean"},"stageProbabilityPct":{"type":"number"},"weightedValue":{"type":"number"},"aiRecommendation":{"type":["string","null"]}},"required":["dealId","dealTitle","customerId","customerName","dealValue","currentStage","daysInStage","isStalled","stageProbabilityPct","weightedValue","aiRecommendation"],"additionalProperties":false}},"stalledDeals":{"type":"array","items":{"type":"object","properties":{"dealId":{"type":"string"},"dealTitle":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"dealValue":{"type":"number"},"currentStage":{"type":"string"},"daysInStage":{"type":"number"},"isStalled":{"type":"boolean"},"stageProbabilityPct":{"type":"number"},"weightedValue":{"type":"number"},"aiRecommendation":{"type":["string","null"]}},"required":["dealId","dealTitle","customerId","customerName","dealValue","currentStage","daysInStage","isStalled","stageProbabilityPct","weightedValue","aiRecommendation"],"additionalProperties":false}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["lookbackDays","velocityScore","avgSalesCycleDays","overallConversionPct","totalPipelineValue","weightedPipelineValue","stageMetrics","bottleneckStage","openDeals","stalledDeals","summary","aiEnriched","generatedAt"],"additionalProperties":false},"example":{"lookbackDays":0,"velocityScore":0,"avgSalesCycleDays":0,"overallConversionPct":0,"totalPipelineValue":0,"weightedPipelineValue":0,"stageMetrics":[{"stage":"string","avgDwellDays":0,"conversionRatePct":0,"activeDeals":0,"activeValue":0,"isBottleneck":true}],"bottleneckStage":"string","openDeals":[{"dealId":"string","dealTitle":"string","customerId":"string","customerName":"string","dealValue":0,"currentStage":"string","daysInStage":0,"isStalled":true,"stageProbabilityPct":0,"weightedValue":0,"aiRecommendation":"string"}],"stalledDeals":[{"dealId":"string","dealTitle":"string","customerId":"string","customerName":"string","dealValue":0,"currentStage":"string","daysInStage":0,"isStalled":true,"stageProbabilityPct":0,"weightedValue":0,"aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiPipeline-velocity","tags":["ai","Sales"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":30,"maximum":365,"default":180}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":30}}],"summary":"AI sales pipeline velocity","description":"W25-W — analyses pipeline stage dwell times, conversion rates, and deal velocity score. Identifies bottleneck stages and stalled deals. AI acceleration recommendations via claude-haiku-4-5."}},"/api/v1/ai/margin-erosion":{"get":{"responses":{"200":{"description":"Margin erosion report","content":{"application/json":{"schema":{"type":"object","properties":{"currentDays":{"type":"integer"},"baselineDays":{"type":"integer"},"totalCustomers":{"type":"integer"},"totalRevenueAtRisk":{"type":"number"},"categories":{"type":"array","items":{"type":"object","properties":{"tier":{"type":"string","enum":["critical","warning","stable","improving"]},"label":{"type":"string"},"count":{"type":"integer"},"totalAtRisk":{"type":"number","description":"0 for the 'stable' and 'improving' buckets"}},"required":["tier","label","count","totalAtRisk"]}},"customers":{"type":"array","items":{"type":"object","properties":{"customerId":{"type":"string"},"customerName":{"type":"string"},"currentRevenue":{"type":"number"},"baselineRevenue":{"type":"number"},"currentMarginPct":{"type":"number"},"baselineMarginPct":{"type":"number"},"marginDeltaPts":{"type":"number","description":"Margin change in percentage points (current − baseline)"},"revenueAtRisk":{"type":"number"},"avgDiscountCurrent":{"type":"number"},"avgDiscountBaseline":{"type":"number"},"erosionDriver":{"type":"string","enum":["volume_discount","cost_increase","price_concession","mix_shift","unknown"]},"erosionTier":{"type":"string","enum":["critical","warning","stable","improving"]},"aiRecommendation":{"type":["string","null"],"description":"German measure — null unless the LLM enrichment ran"}},"required":["customerId","customerName","currentRevenue","baselineRevenue","currentMarginPct","baselineMarginPct","marginDeltaPts","revenueAtRisk","avgDiscountCurrent","avgDiscountBaseline","erosionDriver","erosionTier","aiRecommendation"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["currentDays","baselineDays","totalCustomers","totalRevenueAtRisk","categories","customers","summary","aiEnriched","generatedAt"]},"example":{"currentDays":0,"baselineDays":0,"totalCustomers":0,"totalRevenueAtRisk":0,"categories":[{"tier":"critical","label":"string","count":0,"totalAtRisk":0}],"customers":[{"customerId":"string","customerName":"string","currentRevenue":0,"baselineRevenue":0,"currentMarginPct":0,"baselineMarginPct":0,"marginDeltaPts":0,"revenueAtRisk":0,"avgDiscountCurrent":0,"avgDiscountBaseline":0,"erosionDriver":"volume_discount","erosionTier":"critical","aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiMargin-erosion","tags":["ai","Finance"],"parameters":[{"in":"query","name":"currentDays","schema":{"type":"number","minimum":14,"maximum":180,"default":90}},{"in":"query","name":"baselineDays","schema":{"type":"number","minimum":14,"maximum":180,"default":90}},{"in":"query","name":"minRevenue","schema":{"type":"number","minimum":0,"maximum":100000,"default":500}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"summary":"AI margin erosion detector","description":"W25-X — compares current vs baseline margin per customer, detects erosion drivers (volume_discount/cost_increase/price_concession/mix_shift), quantifies revenue-at-risk. AI margin recovery strategy via claude-haiku-4-5."}},"/api/v1/ai/demand-forecast":{"get":{"responses":{"200":{"description":"Demand forecast report","content":{"application/json":{"schema":{"type":"object","properties":{"forecastWeeks":{"type":"number"},"historyWeeks":{"type":"number"},"totalProducts":{"type":"number"},"stockoutRiskCount":{"type":"number"},"products":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string"},"productName":{"type":"string"},"avgWeeklyDemand":{"type":"number"},"trendPerWeek":{"type":"number"},"currentStock":{"type":["number","null"]},"forecastedUnits":{"type":"number"},"weeksUntilStockout":{"type":["number","null"]},"suggestedReorderQty":{"type":"number"},"isStockoutRisk":{"type":"boolean"},"weeklyDemand":{"type":"array","items":{"type":"object","properties":{"weekStart":{"type":"string"},"units":{"type":"number"},"isForecast":{"type":"boolean"}},"required":["weekStart","units","isForecast"]}},"aiRecommendation":{"type":["string","null"]}},"required":["productId","productName","avgWeeklyDemand","trendPerWeek","currentStock","forecastedUnits","weeksUntilStockout","suggestedReorderQty","isStockoutRisk","weeklyDemand","aiRecommendation"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["forecastWeeks","historyWeeks","totalProducts","stockoutRiskCount","products","summary","aiEnriched","generatedAt"]},"example":{"forecastWeeks":0,"historyWeeks":0,"totalProducts":0,"stockoutRiskCount":0,"products":[{"productId":"string","productName":"string","avgWeeklyDemand":0,"trendPerWeek":0,"currentStock":0,"forecastedUnits":0,"weeksUntilStockout":0,"suggestedReorderQty":0,"isStockoutRisk":true,"weeklyDemand":[{"weekStart":"string","units":0,"isForecast":true}],"aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiDemand-forecast","tags":["ai","Inventory"],"parameters":[{"in":"query","name":"forecastWeeks","schema":{"type":"number","minimum":1,"maximum":12,"default":4}},{"in":"query","name":"historyWeeks","schema":{"type":"number","minimum":4,"maximum":52,"default":12}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}}],"summary":"AI demand forecasting","description":"W25-Y — forecasts product demand using exponential smoothing + trend from order history. Weeks-until-stockout, suggested reorder qty. Stockout risk alerts. AI procurement recommendations via claude-haiku-4-5. Die Zahlen entstehen aus FESTEN Rechenregeln, nicht aus dem Modell: es ergaenzt nur `aiRecommendation` je Artikel und `summary`. Faellt es aus oder ist keine KI konfiguriert, bleiben beide null und `aiEnriched` false. `historyWeeks` (4…52, Vorgabe 12) legt fest, wie weit zurueckgeschaut wird, `forecastWeeks` (1…12, Vorgabe 4) wie weit nach vorn, `limit` (1…100, Vorgabe 20) wie viele Artikel betrachtet werden — `totalProducts` nennt diese Zahl, nicht den Bestand. `weeklyDemand` enthaelt Vergangenheit und Vorhersage gemischt; `isForecast` trennt sie. Rein lesend: es wird nichts bestellt, nichts reserviert und nichts gespeichert."}},"/api/v1/ai/budget-variance":{"get":{"responses":{"200":{"description":"Budget variance report","content":{"application/json":{"schema":{"type":"object","properties":{"phases":{"type":"array","items":{"type":"string"}},"totalProjects":{"type":"number"},"totalBudget":{"type":"number"},"totalActual":{"type":"number"},"totalVarianceEur":{"type":"number"},"overrunCount":{"type":"number"},"atRiskCount":{"type":"number"},"products":{"type":"array","items":{"type":"object","properties":{"projectId":{"type":"string"},"projectName":{"type":"string"},"projectNumber":{"type":["string","null"]},"phase":{"type":"string"},"budget":{"type":"number"},"actualSpend":{"type":"number"},"committedCost":{"type":"number"},"utilization":{"type":"number"},"varianceEur":{"type":"number"},"burnRatePerWeek":{"type":"number"},"eac":{"type":"number"},"vac":{"type":"number"},"progressPercent":{"type":"number"},"cpi":{"type":"number"},"riskTier":{"type":"string","enum":["overrun","at_risk","on_track","underspend","no_data"]},"aiRecommendation":{"type":["string","null"]},"categories":{"type":["object","null"],"properties":{"labour":{"type":"number"},"materials":{"type":"number"},"subcontractors":{"type":"number"},"other":{"type":"number"}},"required":["labour","materials","subcontractors","other"]}},"required":["projectId","projectName","projectNumber","phase","budget","actualSpend","committedCost","utilization","varianceEur","burnRatePerWeek","eac","vac","progressPercent","cpi","riskTier","aiRecommendation","categories"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string","format":"date-time"}},"required":["phases","totalProjects","totalBudget","totalActual","totalVarianceEur","overrunCount","atRiskCount","products","summary","aiEnriched","generatedAt"]},"example":{"phases":["string"],"totalProjects":0,"totalBudget":0,"totalActual":0,"totalVarianceEur":0,"overrunCount":0,"atRiskCount":0,"products":[{"projectId":"string","projectName":"string","projectNumber":"string","phase":"string","budget":0,"actualSpend":0,"committedCost":0,"utilization":0,"varianceEur":0,"burnRatePerWeek":0,"eac":0,"vac":0,"progressPercent":0,"cpi":0,"riskTier":"overrun","aiRecommendation":"string","categories":{"labour":0,"materials":0,"subcontractors":0,"other":0}}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiBudget-variance","tags":["ai","Projects","Finance"],"parameters":[{"in":"query","name":"phases","schema":{"type":"string","default":"planning,in_progress,review"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"minBudget","schema":{"type":"number","minimum":0,"maximum":10000000,"default":0}}],"summary":"AI budget variance analysis","description":"W26-A — compares actual vs planned spend per project. EAC, VAC, CPI, burn rate, risk tier. AI corrective actions via claude-haiku-4-5."}},"/api/v1/ai/working-capital":{"get":{"responses":{"200":{"description":"Working capital report","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"number"},"ccc":{"type":"number"},"targetCcc":{"type":"number"},"workingCapitalEur":{"type":"number"},"totalImprovementEur":{"type":"number"},"levers":{"type":"array","items":{"type":"object","properties":{"metric":{"type":"string","enum":["dso","dpo","dio"]},"label":{"type":"string"},"currentDays":{"type":"number"},"benchmarkDays":{"type":"number"},"gapDays":{"type":"number"},"improvementEur":{"type":"number"},"priority":{"type":"string","enum":["high","medium","low","on_target"]},"description":{"type":"string"},"aiRecommendation":{"type":"string"}},"required":["metric","label","currentDays","benchmarkDays","gapDays","improvementEur","priority","description"]}},"topCustomersByDso":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"dso":{"type":"number"},"outstandingEur":{"type":"number"}},"required":["name","dso","outstandingEur"]}},"topSuppliersByDpo":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"dpo":{"type":"number"},"payableEur":{"type":"number"}},"required":["name","dpo","payableEur"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string","format":"date-time"}},"required":["lookbackDays","ccc","targetCcc","workingCapitalEur","totalImprovementEur","levers","topCustomersByDso","topSuppliersByDpo","summary","aiEnriched","generatedAt"]},"example":{"lookbackDays":0,"ccc":0,"targetCcc":0,"workingCapitalEur":0,"totalImprovementEur":0,"levers":[{"metric":"dso","label":"string","currentDays":0,"benchmarkDays":0,"gapDays":0,"improvementEur":0,"priority":"high","description":"string","aiRecommendation":"string"}],"topCustomersByDso":[{"name":"string","dso":0,"outstandingEur":0}],"topSuppliersByDpo":[{"name":"string","dpo":0,"payableEur":0}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiWorking-capital","tags":["ai","Finance"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":30,"maximum":365,"default":90}},{"in":"query","name":"benchmarkDso","schema":{"type":"number","minimum":1,"maximum":180,"default":30}},{"in":"query","name":"benchmarkDpo","schema":{"type":"number","minimum":1,"maximum":180,"default":45}},{"in":"query","name":"benchmarkDio","schema":{"type":"number","minimum":1,"maximum":180,"default":30}}],"summary":"AI working capital optimizer","description":"W26-B — DSO/DPO/DIO analysis, CCC, EUR improvement potential per lever, top customers by DSO, top suppliers by DPO. AI recommendations via claude-haiku-4-5."}},"/api/v1/ai/vendor-risk":{"get":{"responses":{"200":{"description":"Vendor risk report. `riskScore` is the ARITHMETIC sum of four sub-scores of 0–25 each — delivery, quality, concentration, price trend — where a HIGHER value always means WORSE. The AI only writes the mitigation wording; without a configured model the report still ships with `aiEnriched: false`, `summary: null` and every `aiRecommendation` null. Only suppliers with at least `minOrders` orders inside `lookbackDays` are scored at all, and `suppliers` is capped by `limit` — a supplier missing from the list is not thereby low-risk.","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackDays":{"type":"number"},"totalSuppliers":{"type":"number"},"criticalCount":{"type":"number"},"highCount":{"type":"number"},"totalSpendEur":{"type":"number"},"suppliers":{"type":"array","items":{"type":"object","properties":{"supplierId":{"type":"string"},"supplierName":{"type":"string"},"totalOrders":{"type":"number"},"totalSpendEur":{"type":"number"},"spendSharePct":{"type":"number"},"deliveryScore":{"type":"number"},"deliveryOnTimePct":{"type":"number"},"qualityScore":{"type":"number"},"defectRatePct":{"type":"number"},"concentrationScore":{"type":"number"},"singleSourceItemCount":{"type":"number"},"priceScore":{"type":"number"},"priceTrendPct":{"type":"number"},"riskScore":{"type":"number"},"riskTier":{"type":"string","enum":["critical","high","moderate","low"]},"aiRecommendation":{"type":["string","null"]}},"required":["supplierId","supplierName","totalOrders","totalSpendEur","spendSharePct","deliveryScore","deliveryOnTimePct","qualityScore","defectRatePct","concentrationScore","singleSourceItemCount","priceScore","priceTrendPct","riskScore","riskTier","aiRecommendation"],"additionalProperties":false}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["lookbackDays","totalSuppliers","criticalCount","highCount","totalSpendEur","suppliers","summary","aiEnriched","generatedAt"],"additionalProperties":false},"example":{"lookbackDays":0,"totalSuppliers":0,"criticalCount":0,"highCount":0,"totalSpendEur":0,"suppliers":[{"supplierId":"string","supplierName":"string","totalOrders":0,"totalSpendEur":0,"spendSharePct":0,"deliveryScore":0,"deliveryOnTimePct":0,"qualityScore":0,"defectRatePct":0,"concentrationScore":0,"singleSourceItemCount":0,"priceScore":0,"priceTrendPct":0,"riskScore":0,"riskTier":"critical","aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiVendor-risk","tags":["ai","Procurement"],"parameters":[{"in":"query","name":"lookbackDays","schema":{"type":"number","minimum":30,"maximum":365,"default":180}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}},{"in":"query","name":"minOrders","schema":{"type":"number","minimum":1,"maximum":50,"default":3}}],"summary":"AI vendor risk scorecard","description":"W26-C — composite supplier risk score (delivery + quality + concentration + price). Tier: critical/high/moderate/low. AI mitigation strategy via claude-haiku-4-5."}},"/api/v1/ai/receivables-aging":{"get":{"responses":{"200":{"description":"Receivables aging report","content":{"application/json":{"schema":{"type":"object","properties":{"totalReceivablesEur":{"type":"number"},"totalProvisionEur":{"type":"number"},"concentrationPct":{"type":"number"},"buckets":{"type":"array","items":{"type":"object","properties":{"bucket":{"type":"string","enum":["current","1_30","31_60","61_90","91_180","180_plus"]},"label":{"type":"string"},"totalEur":{"type":"number"},"invoiceCount":{"type":"number"},"collectionProbPct":{"type":"number"},"suggestedProvisionEur":{"type":"number"}},"required":["bucket","label","totalEur","invoiceCount","collectionProbPct","suggestedProvisionEur"],"additionalProperties":false}},"customers":{"type":"array","items":{"type":"object","properties":{"customerId":{"type":"string"},"customerName":{"type":"string"},"totalOpenEur":{"type":"number"},"oldestDaysOverdue":{"type":"number"},"bucket":{"type":"string","enum":["current","1_30","31_60","61_90","91_180","180_plus"]},"invoiceCount":{"type":"number"},"lastPaymentDate":{"type":["string","null"]},"aiRecommendation":{"type":["string","null"]}},"required":["customerId","customerName","totalOpenEur","oldestDaysOverdue","bucket","invoiceCount","lastPaymentDate","aiRecommendation"],"additionalProperties":false}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["totalReceivablesEur","totalProvisionEur","concentrationPct","buckets","customers","summary","aiEnriched","generatedAt"],"additionalProperties":false},"example":{"totalReceivablesEur":0,"totalProvisionEur":0,"concentrationPct":0,"buckets":[{"bucket":"current","label":"string","totalEur":0,"invoiceCount":0,"collectionProbPct":0,"suggestedProvisionEur":0}],"customers":[{"customerId":"string","customerName":"string","totalOpenEur":0,"oldestDaysOverdue":0,"bucket":"current","invoiceCount":0,"lastPaymentDate":"string","aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiReceivables-aging","tags":["ai","Finance"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":30}}],"summary":"AI receivables aging analysis","description":"W26-D — aging buckets for open receivables with collection probability, bad-debt provision suggestions, and per-customer detail. AI collection actions via claude-haiku-4-5."}},"/api/v1/ai/win-loss":{"get":{"responses":{"200":{"description":"Win/loss report — `degraded: true` marks a partially failed query run","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"integer"},"totalWon":{"type":"integer"},"totalLost":{"type":"integer"},"winRatePct":{"type":"number"},"avgDealSizeWon":{"type":"number"},"avgDealSizeLost":{"type":"number"},"avgCycleDaysWon":{"type":"number"},"avgCycleDaysLost":{"type":"number"},"wonRevenueEur":{"type":"number"},"lostRevenueEur":{"type":"number"},"priorWinRatePct":{"type":["number","null"],"description":"null when the prior period has no closed deals"},"winRateTrendPts":{"type":["number","null"],"description":"Positive = improving; null without a comparison period"},"improvementPotentialEur":{"type":"number","description":"Revenue potential if the win rate improved by 5 points"},"bySource":{"type":"array","items":{"type":"object","properties":{"dimension":{"type":"string"},"won":{"type":"integer"},"lost":{"type":"integer"},"winRate":{"type":"number"},"wonRevenue":{"type":"number"},"lostRevenue":{"type":"number"}},"required":["dimension","won","lost","winRate","wonRevenue","lostRevenue"]}},"stageDropOff":{"type":"array","items":{"type":"object","properties":{"stage":{"type":"string","description":"Verlustgrund (`verloren_grund`) — der Schluessel heiszt aus Kompatibilitaetsgruenden weiterhin `stage`"},"lostCount":{"type":"integer"},"lostRevenue":{"type":"number"},"pctOfLost":{"type":"number","description":"% of all lost deals that died here"}},"required":["stage","lostCount","lostRevenue","pctOfLost"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"},"degraded":{"type":"boolean","const":true,"description":"Nur gesetzt, wenn mindestens eine Teilabfrage scheiterte — trennt „leer\" von „kaputt\""}},"required":["days","totalWon","totalLost","winRatePct","avgDealSizeWon","avgDealSizeLost","avgCycleDaysWon","avgCycleDaysLost","wonRevenueEur","lostRevenueEur","priorWinRatePct","winRateTrendPts","improvementPotentialEur","bySource","stageDropOff","summary","aiEnriched","generatedAt"]},"example":{"days":0,"totalWon":0,"totalLost":0,"winRatePct":0,"avgDealSizeWon":0,"avgDealSizeLost":0,"avgCycleDaysWon":0,"avgCycleDaysLost":0,"wonRevenueEur":0,"lostRevenueEur":0,"priorWinRatePct":0,"winRateTrendPts":0,"improvementPotentialEur":0,"bySource":[{"dimension":"string","won":0,"lost":0,"winRate":0,"wonRevenue":0,"lostRevenue":0}],"stageDropOff":[{"stage":"string","lostCount":0,"lostRevenue":0,"pctOfLost":0}],"summary":"string","aiEnriched":true,"generatedAt":"string","degraded":true}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiWin-loss","tags":["ai","CRM","Sales"],"parameters":[{"in":"query","name":"days","schema":{"type":"number","minimum":30,"maximum":365,"default":180}},{"in":"query","name":"priorDays","schema":{"type":"number","minimum":30,"maximum":365}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}}],"summary":"AI win/loss analysis","description":"W26-E — win rate, deal-size comparison (won vs lost), cycle time, stage drop-off map, source breakdown. AI sales optimisation via claude-haiku-4-5."}},"/api/v1/ai/inventory-abc":{"get":{"responses":{"200":{"description":"Inventory ABC/XYZ report","content":{"application/json":{"schema":{"type":"object","properties":{"lookbackWeeks":{"type":"integer","minimum":4,"maximum":52,"description":"Ausgewerteter Zeitraum der Auftragshistorie in Wochen"},"totalItems":{"type":"integer","minimum":0,"description":"Anzahl der klassifizierten Artikel"},"totalValueEur":{"type":"number","description":"Summe der Jahreswerte aller ausgewerteten Artikel in EUR"},"classSummary":{"type":"array","items":{"type":"object","properties":{"abcClass":{"type":"string","enum":["A","B","C"],"description":"Die zusammengefasste Klasse"},"itemCount":{"type":"integer","minimum":0,"description":"Anzahl Artikel in dieser Klasse"},"totalValueEur":{"type":"number","description":"Summe des Jahreswerts dieser Klasse in EUR"},"valuePct":{"type":"number","description":"Anteil dieser Klasse am Gesamtwert in Prozent"}},"required":["abcClass","itemCount","totalValueEur","valuePct"]},"description":"Je ein Eintrag fuer A, B und C — auch bei null Artikeln"},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":"string","description":"Kennung des Artikels"},"articleName":{"type":"string","description":"Artikelbezeichnung"},"articleNumber":{"type":["string","null"],"description":"Artikelnummer; null wenn nicht erfasst"},"currentStock":{"type":"number","description":"Aktueller Lagerbestand; 0 wenn kein Bestandssatz vorliegt"},"annualConsumptionQty":{"type":"number","description":"Auf ein Jahr hochgerechnete Verbrauchsmenge aus dem Betrachtungszeitraum"},"annualValueEur":{"type":"number","description":"Auf ein Jahr hochgerechneter Verbrauchswert in EUR"},"valueRank":{"type":"integer","minimum":1,"description":"Rang nach Wert, 1 = hoechster"},"cumulativeValuePct":{"type":"number","description":"Kumulierter Wertanteil bis zu diesem Rang in Prozent"},"abcClass":{"type":"string","enum":["A","B","C"],"description":"A bis 80 %, B bis 95 %, C darueber (kumulierter Wertanteil)"},"demandCoV":{"type":"number","description":"Variationskoeffizient der Wochennachfrage"},"xyzClass":{"type":"string","enum":["X","Y","Z"],"description":"X bei CoV <= 0,25, Y bis 0,50, Z darueber"},"combinedClass":{"type":"string","enum":["AX","AY","AZ","BX","BY","BZ","CX","CY","CZ"],"description":"ABC- und XYZ-Klasse aneinandergehaengt"},"strategy":{"type":"string","enum":["continuous_review","periodic_review","just_in_time","simplify"],"description":"Abgeleitete Nachschubstrategie zur kombinierten Klasse"},"suggestedSafetyStock":{"type":"number","description":"Vorgeschlagener Sicherheitsbestand, aufgerundet auf ganze Einheiten"},"aiRecommendation":{"type":["string","null"],"description":"Empfehlung des Sprachmodells; nur bei einzelnen Artikeln gesetzt, sonst null"}},"required":["articleId","articleName","articleNumber","currentStock","annualConsumptionQty","annualValueEur","valueRank","cumulativeValuePct","abcClass","demandCoV","xyzClass","combinedClass","strategy","suggestedSafetyStock","aiRecommendation"]},"description":"Die Artikel, absteigend nach Jahreswert"},"summary":{"type":["string","null"],"description":"Gesamteinschaetzung des Sprachmodells; null ohne hinterlegten Schluessel oder bei unbrauchbarer Antwort"},"aiEnriched":{"type":"boolean","description":"true, wenn das Sprachmodell Empfehlungen beigesteuert hat"},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Berechnung"}},"required":["lookbackWeeks","totalItems","totalValueEur","classSummary","items","summary","aiEnriched","generatedAt"]},"example":{"lookbackWeeks":4,"totalItems":0,"totalValueEur":0,"classSummary":[{"abcClass":"A","itemCount":0,"totalValueEur":0,"valuePct":0}],"items":[{"articleId":"string","articleName":"string","articleNumber":"string","currentStock":0,"annualConsumptionQty":0,"annualValueEur":0,"valueRank":1,"cumulativeValuePct":0,"abcClass":"A","demandCoV":0,"xyzClass":"X","combinedClass":"AX","strategy":"continuous_review","suggestedSafetyStock":0,"aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"message":{"type":"string"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","message","retryAfter"]}}}}},"operationId":"getApiV1AiInventory-abc","tags":["ai","Inventory"],"parameters":[{"in":"query","name":"lookbackWeeks","schema":{"type":"number","minimum":4,"maximum":52,"default":26}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"minValue","schema":{"type":"number","minimum":0,"maximum":1000000,"default":0}}],"summary":"AI inventory ABC/XYZ classification","description":"W26-F — ABC (value) + XYZ (demand regularity) combined classification. Replenishment strategy, safety stock, Pareto analysis. AI recommendations via claude-haiku-4-5."}},"/api/v1/ai/price-elasticity":{"get":{"responses":{"200":{"description":"Price elasticity report","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"number"},"totalProductGroups":{"type":"number"},"inelasticOpportunities":{"type":"number"},"elasticOpportunities":{"type":"number"},"topInelastic":{"type":"array","items":{"type":"object","properties":{"productName":{"type":"string"},"productId":{"type":"string"},"category":{"type":["string","null"]},"lineCount":{"type":"number"},"avgUnitPrice":{"type":"number"},"minUnitPrice":{"type":"number"},"maxUnitPrice":{"type":"number"},"priceDispersionPct":{"type":"number"},"avgQuantityPerOrder":{"type":"number"},"totalRevenue":{"type":"number"},"elasticityCoef":{"type":["number","null"]},"elasticityClass":{"type":"string","enum":["highly_elastic","elastic","unitary","inelastic","highly_inelastic","insufficient_data"]},"avgDiscountPct":{"type":"number"},"suggestion":{"type":"string"},"aiRecommendation":{"type":["string","null"]}},"required":["productName","productId","category","lineCount","avgUnitPrice","minUnitPrice","maxUnitPrice","priceDispersionPct","avgQuantityPerOrder","totalRevenue","elasticityCoef","elasticityClass","avgDiscountPct","suggestion","aiRecommendation"]}},"topElastic":{"type":"array","items":{"type":"object","properties":{"productName":{"type":"string"},"productId":{"type":"string"},"category":{"type":["string","null"]},"lineCount":{"type":"number"},"avgUnitPrice":{"type":"number"},"minUnitPrice":{"type":"number"},"maxUnitPrice":{"type":"number"},"priceDispersionPct":{"type":"number"},"avgQuantityPerOrder":{"type":"number"},"totalRevenue":{"type":"number"},"elasticityCoef":{"type":["number","null"]},"elasticityClass":{"type":"string","enum":["highly_elastic","elastic","unitary","inelastic","highly_inelastic","insufficient_data"]},"avgDiscountPct":{"type":"number"},"suggestion":{"type":"string"},"aiRecommendation":{"type":["string","null"]}},"required":["productName","productId","category","lineCount","avgUnitPrice","minUnitPrice","maxUnitPrice","priceDispersionPct","avgQuantityPerOrder","totalRevenue","elasticityCoef","elasticityClass","avgDiscountPct","suggestion","aiRecommendation"]}},"allGroups":{"type":"array","items":{"type":"object","properties":{"productName":{"type":"string"},"productId":{"type":"string"},"category":{"type":["string","null"]},"lineCount":{"type":"number"},"avgUnitPrice":{"type":"number"},"minUnitPrice":{"type":"number"},"maxUnitPrice":{"type":"number"},"priceDispersionPct":{"type":"number"},"avgQuantityPerOrder":{"type":"number"},"totalRevenue":{"type":"number"},"elasticityCoef":{"type":["number","null"]},"elasticityClass":{"type":"string","enum":["highly_elastic","elastic","unitary","inelastic","highly_inelastic","insufficient_data"]},"avgDiscountPct":{"type":"number"},"suggestion":{"type":"string"},"aiRecommendation":{"type":["string","null"]}},"required":["productName","productId","category","lineCount","avgUnitPrice","minUnitPrice","maxUnitPrice","priceDispersionPct","avgQuantityPerOrder","totalRevenue","elasticityCoef","elasticityClass","avgDiscountPct","suggestion","aiRecommendation"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["days","totalProductGroups","inelasticOpportunities","elasticOpportunities","topInelastic","topElastic","allGroups","summary","aiEnriched","generatedAt"]},"example":{"days":0,"totalProductGroups":0,"inelasticOpportunities":0,"elasticOpportunities":0,"topInelastic":[{"productName":"string","productId":"string","category":"string","lineCount":0,"avgUnitPrice":0,"minUnitPrice":0,"maxUnitPrice":0,"priceDispersionPct":0,"avgQuantityPerOrder":0,"totalRevenue":0,"elasticityCoef":0,"elasticityClass":"highly_elastic","avgDiscountPct":0,"suggestion":"string","aiRecommendation":"string"}],"topElastic":[{"productName":"string","productId":"string","category":"string","lineCount":0,"avgUnitPrice":0,"minUnitPrice":0,"maxUnitPrice":0,"priceDispersionPct":0,"avgQuantityPerOrder":0,"totalRevenue":0,"elasticityCoef":0,"elasticityClass":"highly_elastic","avgDiscountPct":0,"suggestion":"string","aiRecommendation":"string"}],"allGroups":[{"productName":"string","productId":"string","category":"string","lineCount":0,"avgUnitPrice":0,"minUnitPrice":0,"maxUnitPrice":0,"priceDispersionPct":0,"avgQuantityPerOrder":0,"totalRevenue":0,"elasticityCoef":0,"elasticityClass":"highly_elastic","avgDiscountPct":0,"suggestion":"string","aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiPrice-elasticity","tags":["ai","Sales","Finance"],"parameters":[{"in":"query","name":"days","schema":{"type":"number","minimum":30,"maximum":365,"default":180}},{"in":"query","name":"minItems","schema":{"type":"number","minimum":3,"maximum":50,"default":10}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":40}}],"summary":"AI price elasticity analysis","description":"W26-G — arc-elasticity per product group, price dispersion, discount depth, and revenue-optimal pricing recommendations via claude-haiku-4-5. Elastizitaet, Einstufung und Vorschlag entstehen aus FESTEN Rechenregeln, nicht aus dem Modell: es ergaenzt nur `aiRecommendation` bei hoechstens sechs Gruppen und `summary`. Faellt es aus oder ist keine KI konfiguriert, bleiben beide null und `aiEnriched` false. `days` (30…365, Vorgabe 180) legt das Zeitfenster fest, `minItems` (3…50, Vorgabe 10) wie viele Auftragspositionen eine Gruppe mindestens braucht, `limit` (1…100, Vorgabe 40) wie viele Gruppen zurueckkommen. Streute der Preis zu wenig, ist `elasticityCoef` null und die Einstufung „insufficient_data\" — das ist kein Messwert. `topInelastic` und `topElastic` sind ein Auszug aus `allGroups`, keine zusaetzlichen Gruppen. Rein lesend: es wird kein Preis geaendert und nichts gespeichert."}},"/api/v1/ai/sla-compliance":{"get":{"responses":{"200":{"description":"SLA compliance report","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"integer"},"overallCompliancePct":{"type":"number"},"totalClosed":{"type":"integer"},"totalBreached":{"type":"integer"},"priorCompliancePct":{"type":["number","null"],"description":"null when the prior period has no closed tickets"},"complianceTrendPts":{"type":["number","null"],"description":"Change in percentage points vs the prior period; null without a comparison"},"byPriority":{"type":"array","items":{"type":"object","properties":{"priority":{"type":"string","enum":["P1","P2","P3","P4","unknown"]},"responseTargetHrs":{"type":"number","description":"SLA target in hours for the first response"},"resolutionTargetHrs":{"type":"number","description":"SLA target in hours for the resolution"},"totalClosed":{"type":"integer"},"responseBreaches":{"type":"integer"},"resolutionBreaches":{"type":"integer"},"responseCompliancePct":{"type":"number"},"resolutionCompliancePct":{"type":"number"},"avgResponseHrs":{"type":"number"},"avgResolutionHrs":{"type":"number"}},"required":["priority","responseTargetHrs","resolutionTargetHrs","totalClosed","responseBreaches","resolutionBreaches","responseCompliancePct","resolutionCompliancePct","avgResponseHrs","avgResolutionHrs"]}},"atRiskTickets":{"type":"array","items":{"type":"object","properties":{"ticketId":{"type":"string"},"subject":{"type":"string"},"priority":{"type":"string","enum":["P1","P2","P3","P4","unknown"]},"assignee":{"type":["string","null"]},"hoursOpen":{"type":"number"},"resolutionDeadlineHrs":{"type":"number"},"remainingHrs":{"type":"number","description":"Negative = SLA already breached"},"status":{"type":"string"}},"required":["ticketId","subject","priority","assignee","hoursOpen","resolutionDeadlineHrs","remainingHrs","status"]}},"teamStats":{"type":"array","items":{"type":"object","properties":{"assignee":{"type":"string"},"closedTickets":{"type":"integer"},"breachedTickets":{"type":"integer"},"breachRatePct":{"type":"number"},"avgResolutionHrs":{"type":"number"}},"required":["assignee","closedTickets","breachedTickets","breachRatePct","avgResolutionHrs"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["days","overallCompliancePct","totalClosed","totalBreached","priorCompliancePct","complianceTrendPts","byPriority","atRiskTickets","teamStats","summary","aiEnriched","generatedAt"]},"example":{"days":0,"overallCompliancePct":0,"totalClosed":0,"totalBreached":0,"priorCompliancePct":0,"complianceTrendPts":0,"byPriority":[{"priority":"P1","responseTargetHrs":0,"resolutionTargetHrs":0,"totalClosed":0,"responseBreaches":0,"resolutionBreaches":0,"responseCompliancePct":0,"resolutionCompliancePct":0,"avgResponseHrs":0,"avgResolutionHrs":0}],"atRiskTickets":[{"ticketId":"string","subject":"string","priority":"P1","assignee":"string","hoursOpen":0,"resolutionDeadlineHrs":0,"remainingHrs":0,"status":"string"}],"teamStats":[{"assignee":"string","closedTickets":0,"breachedTickets":0,"breachRatePct":0,"avgResolutionHrs":0}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiSla-compliance","tags":["ai","support","Operations"],"parameters":[{"in":"query","name":"days","schema":{"type":"number","minimum":7,"maximum":365,"default":90}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}}],"summary":"AI SLA compliance analysis","description":"W26-H — ticket SLA compliance rate, breach breakdown by priority, at-risk open tickets, and team performance. AI SLA improvement recommendations via claude-haiku-4-5."}},"/api/v1/ai/onboarding-risk":{"get":{"responses":{"200":{"description":"Onboarding risk report","content":{"application/json":{"schema":{"type":"object","properties":{"daysWindow":{"type":"number"},"totalNewHires":{"type":"number"},"criticalCount":{"type":"number"},"highRiskCount":{"type":"number"},"avgCompletionPct":{"type":"number"},"employees":{"type":"array","items":{"type":"object","properties":{"employeeId":{"type":"string"},"employeeName":{"type":"string"},"department":{"type":["string","null"]},"manager":{"type":["string","null"]},"hireDate":{"type":"string"},"daysSinceHire":{"type":"number"},"expectedPhase":{"type":"string"},"totalTasks":{"type":"number"},"completedTasks":{"type":"number"},"completionPct":{"type":"number"},"expectedCompletionPct":{"type":"number"},"overdueTaskCount":{"type":"number"},"hasBuddy":{"type":"boolean"},"hasEquipment":{"type":"boolean"},"has1on1Scheduled":{"type":"boolean"},"riskScore":{"type":"number"},"riskLevel":{"type":"string","enum":["critical","high","medium","low","on_track"]},"riskFactors":{"type":"array","items":{"type":"string"}},"aiRecommendation":{"type":["string","null"]}},"required":["employeeId","employeeName","department","manager","hireDate","daysSinceHire","expectedPhase","totalTasks","completedTasks","completionPct","expectedCompletionPct","overdueTaskCount","hasBuddy","hasEquipment","has1on1Scheduled","riskScore","riskLevel","riskFactors","aiRecommendation"]}},"byDepartment":{"type":"array","items":{"type":"object","properties":{"department":{"type":"string"},"newHireCount":{"type":"number"},"avgCompletionPct":{"type":"number"},"atRiskCount":{"type":"number"},"avgDaysSinceHire":{"type":"number"}},"required":["department","newHireCount","avgCompletionPct","atRiskCount","avgDaysSinceHire"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string","format":"date-time"}},"required":["daysWindow","totalNewHires","criticalCount","highRiskCount","avgCompletionPct","employees","byDepartment","summary","aiEnriched","generatedAt"]},"example":{"daysWindow":0,"totalNewHires":0,"criticalCount":0,"highRiskCount":0,"avgCompletionPct":0,"employees":[{"employeeId":"string","employeeName":"string","department":"string","manager":"string","hireDate":"string","daysSinceHire":0,"expectedPhase":"string","totalTasks":0,"completedTasks":0,"completionPct":0,"expectedCompletionPct":0,"overdueTaskCount":0,"hasBuddy":true,"hasEquipment":true,"has1on1Scheduled":true,"riskScore":0,"riskLevel":"critical","riskFactors":["string"],"aiRecommendation":"string"}],"byDepartment":[{"department":"string","newHireCount":0,"avgCompletionPct":0,"atRiskCount":0,"avgDaysSinceHire":0}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiOnboarding-risk","tags":["ai","HR","Operations"],"parameters":[{"in":"query","name":"daysWindow","schema":{"type":"number","minimum":30,"maximum":365,"default":180}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}}],"summary":"AI employee onboarding risk analysis","description":"W26-I — onboarding completion vs expectation, overdue tasks, missing checklist items, early-departure risk signals. AI recommendations via claude-haiku-4-5."}},"/api/v1/ai/churn-prediction":{"get":{"responses":{"200":{"description":"Churn prediction report","content":{"application/json":{"schema":{"type":"object","properties":{"totalAnalysed":{"type":"integer"},"criticalCount":{"type":"integer"},"highRiskCount":{"type":"integer"},"revenueAtRiskEur":{"type":"number"},"customers":{"type":"array","items":{"type":"object","properties":{"customerId":{"type":"string"},"customerName":{"type":"string"},"segment":{"type":["string","null"]},"assignedTo":{"type":["string","null"]},"lifetimeRevenue":{"type":"number"},"lastOrderDate":{"type":["string","null"]},"churnScore":{"type":"number"},"churnRisk":{"type":"string","enum":["critical","high","medium","low","healthy"]},"signals":{"type":"object","properties":{"revenueTrendPct":{"type":"number"},"daysSinceLastOrder":{"type":"number"},"orderFrequencyDrop":{"type":"number"},"overdueInvoicePct":{"type":"number"},"avgDaysLate":{"type":"number"},"openTicketCount":{"type":"number"},"daysSinceContact":{"type":"number"},"contractExpiringDays":{"type":["number","null"]}},"required":["revenueTrendPct","daysSinceLastOrder","orderFrequencyDrop","overdueInvoicePct","avgDaysLate","openTicketCount","daysSinceContact","contractExpiringDays"]},"topRiskFactors":{"type":"array","items":{"type":"string"}},"aiRecommendation":{"type":["string","null"]}},"required":["customerId","customerName","segment","assignedTo","lifetimeRevenue","lastOrderDate","churnScore","churnRisk","signals","topRiskFactors","aiRecommendation"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string"}},"required":["totalAnalysed","criticalCount","highRiskCount","revenueAtRiskEur","customers","summary","aiEnriched","generatedAt"]},"example":{"totalAnalysed":0,"criticalCount":0,"highRiskCount":0,"revenueAtRiskEur":0,"customers":[{"customerId":"string","customerName":"string","segment":"string","assignedTo":"string","lifetimeRevenue":0,"lastOrderDate":"string","churnScore":0,"churnRisk":"critical","signals":{"revenueTrendPct":0,"daysSinceLastOrder":0,"orderFrequencyDrop":0,"overdueInvoicePct":0,"avgDaysLate":0,"openTicketCount":0,"daysSinceContact":0,"contractExpiringDays":0},"topRiskFactors":["string"],"aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiChurn-prediction","tags":["ai","CRM","Sales"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"minScore","schema":{"type":"number","minimum":0,"maximum":100,"default":0}}],"summary":"AI customer churn prediction","description":"W26-J — composite churn score per customer from revenue trend, recency, payment risk, support signals, and contract expiry. AI retention recommendations via claude-haiku-4-5. Der Wert selbst entsteht aus FESTEN Regeln, nicht aus dem Modell: das Modell ergaenzt nur `aiRecommendation` je Kunde und `summary`. Scheitert es oder ist keine KI konfiguriert, bleiben beide null und `aiEnriched` false — die Zahlen stimmen trotzdem. `limit` (1…200, Vorgabe 50) begrenzt die betrachteten Kunden, `minScore` (0…100) blendet unauffaellige aus. Rein lesend: es wird nichts gespeichert, und ein zweiter Aufruf kann andere Werte liefern, weil sie jedes Mal neu gerechnet werden."}},"/api/v1/ai/profit-center":{"get":{"responses":{"200":{"description":"Profit center report","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"number"},"totalRevenueEur":{"type":"number"},"totalDirectCosts":{"type":"number"},"totalOverhead":{"type":"number"},"totalNetEur":{"type":"number"},"overallMarginPct":{"type":"number"},"starCount":{"type":"number"},"weakCount":{"type":"number"},"lossMakerCount":{"type":"number"},"centers":{"type":"array","items":{"type":"object","properties":{"costCenterId":{"type":"string"},"costCenterName":{"type":"string"},"department":{"type":["string","null"]},"revenueEur":{"type":"number"},"directCostsEur":{"type":"number"},"grossMarginEur":{"type":"number"},"grossMarginPct":{"type":"number"},"overheadAllocationEur":{"type":"number"},"netContributionEur":{"type":"number"},"netContributionPct":{"type":"number"},"headcountCostEur":{"type":["number","null"]},"revenuePerHeadEur":{"type":["number","null"]},"priorGrossMarginPct":{"type":["number","null"]},"marginTrendPts":{"type":["number","null"]},"tier":{"type":"string","enum":["star","strong","average","weak","loss_maker"]},"aiRecommendation":{"type":["string","null"]}},"required":["costCenterId","costCenterName","department","revenueEur","directCostsEur","grossMarginEur","grossMarginPct","overheadAllocationEur","netContributionEur","netContributionPct","headcountCostEur","revenuePerHeadEur","priorGrossMarginPct","marginTrendPts","tier","aiRecommendation"]}},"summary":{"type":["string","null"]},"aiEnriched":{"type":"boolean"},"generatedAt":{"type":"string","format":"date-time"},"degraded":{"type":"boolean","const":true}},"required":["days","totalRevenueEur","totalDirectCosts","totalOverhead","totalNetEur","overallMarginPct","starCount","weakCount","lossMakerCount","centers","summary","aiEnriched","generatedAt"]},"example":{"days":0,"totalRevenueEur":0,"totalDirectCosts":0,"totalOverhead":0,"totalNetEur":0,"overallMarginPct":0,"starCount":0,"weakCount":0,"lossMakerCount":0,"centers":[{"costCenterId":"string","costCenterName":"string","department":"string","revenueEur":0,"directCostsEur":0,"grossMarginEur":0,"grossMarginPct":0,"overheadAllocationEur":0,"netContributionEur":0,"netContributionPct":0,"headcountCostEur":0,"revenuePerHeadEur":0,"priorGrossMarginPct":0,"marginTrendPts":0,"tier":"star","aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"2026-01-01T12:00:00.000Z","degraded":true}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiProfit-center","tags":["ai","Finance","Operations"],"parameters":[{"in":"query","name":"days","schema":{"type":"number","minimum":30,"maximum":365,"default":180}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":50,"default":20}}],"summary":"AI profit center analysis","description":"W26-K — gross and net contribution margin per cost center/department with trend, overhead allocation, and headcount ROI. AI strategic recommendations via claude-haiku-4-5."}},"/api/v1/ai/capacity-planning":{"get":{"responses":{"200":{"description":"Capacity planning report. Counts and FTE gaps are rule-based and always present; `aiEnriched` says whether the German prose came from a model. An empty `groups` list means nothing to analyse — not an error.","content":{"application/json":{"schema":{"type":"object","properties":{"forecastWeeks":{"type":"integer","description":"Echoed from the query"},"totalGroupsAnalysed":{"type":"integer"},"overloadedCount":{"type":"integer"},"atRiskCount":{"type":"integer"},"underutilisedCount":{"type":"integer"},"idleCount":{"type":"integer"},"totalGapFte":{"type":"number","description":"FTE shortage summed over the overloaded groups"},"totalSurplusFte":{"type":"number","description":"FTE surplus summed over the idle/underused groups"},"groups":{"type":"array","items":{"type":"object","properties":{"groupId":{"type":"string"},"groupName":{"type":"string"},"department":{"type":["string","null"]},"resourceType":{"type":"string","description":"headcount | machine | mixed"},"availableHrsPerWeek":{"type":"number"},"usedHrsPerWeek":{"type":"number"},"utilisationPct":{"type":"number"},"forecastedDemandHrs":{"type":"number","description":"Demand per week over the forecast window"},"gapHrsPerWeek":{"type":"number","description":"Demand minus available; negative = surplus"},"status":{"type":"string","description":"overloaded | at_risk | optimal | underutilised | idle"},"gapFte":{"type":"number","description":"The gap expressed in full-time equivalents (40h week)"},"recommendation":{"type":"string","description":"German, rule-based — always present"},"aiRecommendation":{"type":["string","null"],"description":"German, from the model; null when no model ran or it said nothing here"}},"required":["groupId","groupName","department","resourceType","availableHrsPerWeek","usedHrsPerWeek","utilisationPct","forecastedDemandHrs","gapHrsPerWeek","status","gapFte","recommendation","aiRecommendation"]}},"summary":{"type":["string","null"],"description":"Null unless the model produced one"},"aiEnriched":{"type":"boolean","description":"False means the rule-based numbers stand alone — no model ran, or it failed"},"generatedAt":{"type":"string"}},"required":["forecastWeeks","totalGroupsAnalysed","overloadedCount","atRiskCount","underutilisedCount","idleCount","totalGapFte","totalSurplusFte","groups","summary","aiEnriched","generatedAt"]},"example":{"forecastWeeks":0,"totalGroupsAnalysed":0,"overloadedCount":0,"atRiskCount":0,"underutilisedCount":0,"idleCount":0,"totalGapFte":0,"totalSurplusFte":0,"groups":[{"groupId":"string","groupName":"string","department":"string","resourceType":"string","availableHrsPerWeek":0,"usedHrsPerWeek":0,"utilisationPct":0,"forecastedDemandHrs":0,"gapHrsPerWeek":0,"status":"string","gapFte":0,"recommendation":"string","aiRecommendation":"string"}],"summary":"string","aiEnriched":true,"generatedAt":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiCapacity-planning","tags":["ai","HR","Operations"],"parameters":[{"in":"query","name":"forecastWeeks","schema":{"type":"number","minimum":1,"maximum":26,"default":8}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":50,"default":20}}],"summary":"AI capacity planning analysis","description":"W26-L — resource utilisation vs forecasted demand, bottleneck and idle identification, FTE gap and hiring/outsourcing recommendations via claude-haiku-4-5."}},"/api/v1/ai/knowledge-base/stats":{"get":{"responses":{"200":{"description":"KB stats","content":{"application/json":{"schema":{"type":"object","properties":{"totalChunks":{"type":"integer","minimum":0,"description":"All chunks of the tenant, including source types not listed below"},"bySourceType":{"type":"object","properties":{"customer":{"type":"object","properties":{"chunks":{"type":"integer","minimum":0},"lastIndexed":{"type":["string","null"],"description":"Timestamp of the newest chunk of this source type; null while nothing is indexed"}},"required":["chunks","lastIndexed"]},"product":{"type":"object","properties":{"chunks":{"type":"integer","minimum":0},"lastIndexed":{"type":["string","null"],"description":"Timestamp of the newest chunk of this source type; null while nothing is indexed"}},"required":["chunks","lastIndexed"]},"invoice":{"type":"object","properties":{"chunks":{"type":"integer","minimum":0},"lastIndexed":{"type":["string","null"],"description":"Timestamp of the newest chunk of this source type; null while nothing is indexed"}},"required":["chunks","lastIndexed"]},"document":{"type":"object","properties":{"chunks":{"type":"integer","minimum":0},"lastIndexed":{"type":["string","null"],"description":"Timestamp of the newest chunk of this source type; null while nothing is indexed"}},"required":["chunks","lastIndexed"]}},"required":["customer","product","invoice","document"]},"lastIndexed":{"type":["string","null"],"description":"Newest indexing timestamp across all source types"},"provider":{"type":"string","description":"Embedding provider that actually writes — the tenant choice wins over the system one"},"providerModel":{"type":"string"},"generatedAt":{"type":"string","format":"date-time"}},"required":["totalChunks","bySourceType","lastIndexed","provider","providerModel","generatedAt"]},"example":{"totalChunks":0,"bySourceType":{"customer":{"chunks":0,"lastIndexed":"string"},"product":{"chunks":0,"lastIndexed":"string"},"invoice":{"chunks":0,"lastIndexed":"string"},"document":{"chunks":0,"lastIndexed":"string"}},"lastIndexed":"string","provider":"string","providerModel":"string","generatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiKnowledge-baseStats","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Knowledge Base statistics","description":"W27-A5 — chunk counts per source type, last index timestamp, embedding provider."}},"/api/v1/ai/knowledge-base/reindex":{"post":{"responses":{"200":{"description":"Re-index queued","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean","const":true,"description":"Accepted, not finished — the job runs after the answer was sent"},"sourceTypes":{"type":"array","items":{"type":"string","enum":["customer","product","invoice","document"]}},"full":{"type":"boolean"},"estimatedMinutes":{"type":"integer","minimum":0,"description":"Rough estimate: 1.5 minutes per source type, rounded up"},"startedAt":{"type":"string","format":"date-time"}},"required":["queued","sourceTypes","full","estimatedMinutes","startedAt"]},"example":{"queued":true,"sourceTypes":["customer"],"full":true,"estimatedMinutes":0,"startedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Kein Mandantenkontext"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1AiKnowledge-baseReindex","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Trigger knowledge base re-index","description":"W27-A5 — queues an async re-index job for all or specified source types.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sourceTypes":{"type":"array","items":{"type":"string","enum":["customer","product","invoice","document"]}},"full":{"type":"boolean","default":false}}},"example":{"sourceTypes":["customer"],"full":true}}}}}},"/api/v1/ai/knowledge-base/source/{type}/{sourceId}":{"delete":{"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"integer","minimum":0,"description":"Number of chunks removed for this one source entity"},"sourceType":{"type":"string","enum":["customer","product","invoice","document","help"]},"sourceId":{"type":"string"}},"required":["deleted","sourceType","sourceId"]},"example":{"deleted":0,"sourceType":"customer","sourceId":"string"}}}},"400":{"description":"Unknown source type"},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"deleteApiV1AiKnowledge-baseSourceByTypeBySourceId","tags":["ai","Knowledge Base"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"type","required":true},{"schema":{"type":"string"},"in":"path","name":"sourceId","required":true}],"summary":"Remove all RAG chunks for one source entity","description":"W27-A5 — deletes all tenant_rag_documents chunks for a specific source."}},"/api/v1/ai/knowledge-base/config":{"get":{"responses":{"200":{"description":"Config","content":{"application/json":{"schema":{"type":"object","properties":{"ragEnabled":{"type":"boolean"},"indexCustomers":{"type":"boolean"},"indexProducts":{"type":"boolean"},"indexInvoices":{"type":"boolean"},"indexDocuments":{"type":"boolean"},"chunkSize":{"anyOf":[{"type":"number","const":256},{"type":"number","const":512},{"type":"number","const":1024}]},"customProvider":{"type":["string","null"],"enum":["voyage","openai",null]},"hasCustomKey":{"type":"boolean","description":"Whether a provider key is stored — the key itself is never returned"},"lastIndexedAt":{"type":"object","additionalProperties":{"type":"string"},"description":"Per source type: timestamp of the last index run"},"chunkCounts":{"type":"object","additionalProperties":{"type":"number"},"description":"Per source type: chunk count of the last index run"}},"required":["ragEnabled","indexCustomers","indexProducts","indexInvoices","indexDocuments","chunkSize","customProvider","hasCustomKey","lastIndexedAt","chunkCounts"]},"example":{"ragEnabled":true,"indexCustomers":true,"indexProducts":true,"indexInvoices":true,"indexDocuments":true,"chunkSize":256,"customProvider":"voyage","hasCustomKey":true,"lastIndexedAt":{"beispiel":"string"},"chunkCounts":{"beispiel":0}}}}},"400":{"description":"Kein Mandantenkontext"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1AiKnowledge-baseConfig","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Load tenant RAG configuration","description":"Reads public.tenant_rag_config for the current tenant. When no row exists — and also when the database cannot be reached — the defaults answer instead: RAG on, all four source types indexed, chunk size 512, no custom provider. The stored provider key is never returned; hasCustomKey only says whether one exists. lastIndexedAt and chunkCounts are keyed by source type."},"put":{"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"400":{"description":"Kein Mandantenkontext oder Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"}},"operationId":"putApiV1AiKnowledge-baseConfig","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Update RAG configuration","description":"Admin only. Only the fields present in the body are written; omitted fields keep their stored value. customProvider: null clears the provider choice but leaves the stored key in place — DELETE /config/provider-key removes that. The answer is a bare acknowledgement and does not echo the resulting configuration.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ragEnabled":{"type":"boolean"},"indexCustomers":{"type":"boolean"},"indexProducts":{"type":"boolean"},"indexInvoices":{"type":"boolean"},"indexDocuments":{"type":"boolean"},"chunkSize":{"anyOf":[{"type":"number","const":256},{"type":"number","const":512},{"type":"number","const":1024}]},"customProvider":{"type":["string","null"],"enum":["voyage","openai",null]}}},"example":{"ragEnabled":true,"indexCustomers":true,"indexProducts":true,"indexInvoices":true,"indexDocuments":true,"chunkSize":256,"customProvider":"voyage"}}}}}},"/api/v1/ai/knowledge-base/config/provider-key":{"put":{"responses":{"200":{"description":"Stored","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"arn":{"type":"string","description":"ARN of the secret, trailing segment masked"}},"required":["ok","arn"]},"example":{"ok":true,"arn":"string"}}}},"400":{"description":"Kein Mandantenkontext oder Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"}},"operationId":"putApiV1AiKnowledge-baseConfigProvider-key","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Store custom embedding provider API key","description":"Admin only. The key goes into AWS Secrets Manager under nemix/tenants/<slug>/embed-key: an existing secret is overwritten, a missing one is created and tagged. Provider and resulting ARN are then written to the tenant RAG configuration, which switches embedding to that provider. The answer returns the ARN with its trailing segment masked; the key itself is never echoed.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["voyage","openai"]},"apiKey":{"type":"string","minLength":10,"maxLength":512}},"required":["provider","apiKey"]},"example":{"provider":"voyage","apiKey":"stringxxxx"}}}}},"delete":{"responses":{"200":{"description":"Removed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"400":{"description":"Kein Mandantenkontext"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"}},"operationId":"deleteApiV1AiKnowledge-baseConfigProvider-key","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Remove custom embedding provider key","description":"Admin only. Schedules the tenant secret in AWS Secrets Manager for deletion with a seven-day recovery window and clears custom_provider and custom_key_arn in public.tenant_rag_config, so embedding falls back to the system provider. A secret that is already gone is not treated as an error."}},"/api/v1/ai/knowledge-base/clear":{"delete":{"responses":{"200":{"description":"Cleared and re-index queued","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"integer","minimum":0,"description":"Number of chunks removed"},"reindexQueued":{"type":"boolean","const":true}},"required":["deleted","reindexQueued"]},"example":{"deleted":0,"reindexQueued":true}}}},"400":{"description":"Kein Mandantenkontext"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"},"503":{"description":"Database unavailable"}},"operationId":"deleteApiV1AiKnowledge-baseClear","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Delete all RAG chunks and re-index","description":"Admin only, and it deletes without asking again. Every chunk of the tenant is removed from public.tenant_rag_documents and the stored chunk counts and index timestamps are reset. A full re-index is queued right afterwards and runs in the background, so reindexQueued: true means started, not finished."}},"/api/v1/ai/knowledge-base/bulk-tag":{"post":{"responses":{"200":{"description":"Flags written; embedding or chunk cleanup runs in the background","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer","minimum":0,"description":"Records whose rag_enabled flag was written"},"indexed":{"type":"integer","minimum":0,"description":"Records queued for embedding — 0 when the flag was switched off"}},"required":["updated","indexed"]},"example":{"updated":0,"indexed":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiKnowledge-baseBulk-tag","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Bulk-set rag_enabled flag on entity records","description":"W29 — toggles rag_enabled on multiple customers / products / invoices at once. When ragEnabled=true, immediately fires embedding for each record.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sourceType":{"type":"string","enum":["customer","product","invoice"]},"ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"ragEnabled":{"type":"boolean"}},"required":["sourceType","ids","ragEnabled"]},"example":{"sourceType":"customer","ids":["00000000-0000-4000-8000-000000000000"],"ragEnabled":true}}}}}},"/api/v1/ai/knowledge-base/documents":{"get":{"responses":{"200":{"description":"Document list","content":{"application/json":{"schema":{"type":"object","properties":{"documents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","description":"Falls back to \"Unbenanntes Dokument\" when the document has no name"},"classifiedType":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"ragEnabled":{"type":"boolean"},"chunkCount":{"type":"integer","minimum":0},"createdAt":{"type":["string","null"]}},"required":["id","name","classifiedType","pipelineStatus","ragEnabled","chunkCount","createdAt"]}}},"required":["documents"]},"example":{"documents":[{"id":"string","name":"string","classifiedType":"string","pipelineStatus":"string","ragEnabled":true,"chunkCount":0,"createdAt":"string"}]}}}},"400":{"description":"Tenant required"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiKnowledge-baseDocuments","tags":["ai","Knowledge Base"],"parameters":[],"summary":"List DMS documents with per-document RAG indexing status","description":"Returns processed DMS documents and whether each one is currently RAG-indexed."}},"/api/v1/ai/knowledge-base/document/{id}/rag":{"patch":{"responses":{"200":{"description":"Enabled: embedding queued. Disabled: chunks removed.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ragEnabled":{"type":"boolean","const":true},"indexed":{"type":"boolean","const":true,"description":"Embedding was queued, not completed"},"documentId":{"type":"string"}},"required":["ragEnabled","indexed","documentId"]},{"type":"object","properties":{"ragEnabled":{"type":"boolean","const":false},"removed":{"type":"boolean","const":true,"description":"The chunks of this document were deleted"},"documentId":{"type":"string"}},"required":["ragEnabled","removed","documentId"]}]},"example":{"ragEnabled":true,"indexed":true,"documentId":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Document not found"},"503":{"description":"Database unavailable"}},"operationId":"patchApiV1AiKnowledge-baseDocumentByIdRag","tags":["ai","Knowledge Base"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Enable or disable RAG indexing for a single DMS document","description":"Sets rag_enabled on the document. When enabled, immediately embeds the document; when disabled, removes its chunks from the vector store.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ragEnabled":{"type":"boolean"}},"required":["ragEnabled"]},"example":{"ragEnabled":true}}}}}},"/api/v1/ai/knowledge-base/reindex/{type}":{"post":{"responses":{"200":{"description":"Queued","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean","const":true},"sourceType":{"type":"string","enum":["customer","product","invoice","document"]}},"required":["queued","sourceType"]},"example":{"queued":true,"sourceType":"customer"}}}},"400":{"description":"Invalid source type"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"}},"operationId":"postApiV1AiKnowledge-baseReindexByType","tags":["ai","Knowledge Base"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"type","required":true}],"summary":"Re-index a single source type","description":"Admin only. Accepts customer, product, invoice or document; anything else is rejected with 400. The work starts after the answer has been sent, so queued: true means accepted, not finished — and it is skipped silently when RAG is switched off for the tenant. Afterwards the chunk count for that source type is written back into the tenant RAG configuration."}},"/api/v1/ai/knowledge-items":{"get":{"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"tags":{"type":"array","items":{"type":"string"},"description":"Empty array when the row has no tags"},"category":{"type":["string","null"]},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"indexedAt":{"type":["string","null"],"description":"null while the item has never been embedded"},"chunkCount":{"type":"integer","minimum":0},"status":{"type":"string","description":"Indexing lifecycle: pending, indexed or failed"},"indexError":{"type":["string","null"],"description":"Reason of the last failed embedding, truncated"}},"required":["id","title","content","tags","category","createdBy","createdAt","updatedAt","indexedAt","chunkCount","status","indexError"]}},"total":{"type":"integer","minimum":0,"description":"All items of the tenant — NOT narrowed by search or tag"},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"required":["items","total","limit","offset"]},"example":{"items":[{"id":"string","title":"string","content":"string","tags":["string"],"category":"string","createdBy":"string","createdAt":"string","updatedAt":"string","indexedAt":"string","chunkCount":0,"status":"string","indexError":"string"}],"total":0,"limit":1,"offset":0}}}},"400":{"description":"Kein Mandantenkontext"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiKnowledge-items","tags":["ai","Knowledge Base"],"parameters":[],"summary":"List manual knowledge items","description":"Reads public.knowledge_items for the current tenant, most recently changed first. search matches title and content case-insensitively, tag requires an exact entry in the tags array. limit defaults to 50 and is capped at 200, offset starts at 0. total counts ALL items of the tenant and ignores both filters — it does not shrink when search or tag narrow the list."},"post":{"responses":{"201":{"description":"Created — status is pending until the background embedding finished","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"tags":{"type":"array","items":{"type":"string"},"description":"Empty array when the row has no tags"},"category":{"type":["string","null"]},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"indexedAt":{"type":["string","null"],"description":"null while the item has never been embedded"},"chunkCount":{"type":"integer","minimum":0},"status":{"type":"string","description":"Indexing lifecycle: pending, indexed or failed"},"indexError":{"type":["string","null"],"description":"Reason of the last failed embedding, truncated"}},"required":["id","title","content","tags","category","createdBy","createdAt","updatedAt","indexedAt","chunkCount","status","indexError"]},"example":{"id":"string","title":"string","content":"string","tags":["string"],"category":"string","createdBy":"string","createdAt":"string","updatedAt":"string","indexedAt":"string","chunkCount":0,"status":"string","indexError":"string"}}}},"400":{"description":"Kein Mandantenkontext, Validierungsfehler oder abgelehnte Daten"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Insert lieferte keine Zeile zurueck"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiKnowledge-items","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Create a manual knowledge item","description":"Stores the item and triggers embedding into the tenant vector store (source_type='manual').","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":50000},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":60},"maxItems":20},"category":{"type":"string","minLength":1,"maxLength":60}},"required":["title","content"]},"example":{"title":"string","content":"string","tags":["string"],"category":"string"}}}}}},"/api/v1/ai/knowledge-items/{id}":{"get":{"responses":{"200":{"description":"Item","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"tags":{"type":"array","items":{"type":"string"},"description":"Empty array when the row has no tags"},"category":{"type":["string","null"]},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"indexedAt":{"type":["string","null"],"description":"null while the item has never been embedded"},"chunkCount":{"type":"integer","minimum":0},"status":{"type":"string","description":"Indexing lifecycle: pending, indexed or failed"},"indexError":{"type":["string","null"],"description":"Reason of the last failed embedding, truncated"}},"required":["id","title","content","tags","category","createdBy","createdAt","updatedAt","indexedAt","chunkCount","status","indexError"]},"example":{"id":"string","title":"string","content":"string","tags":["string"],"category":"string","createdBy":"string","createdAt":"string","updatedAt":"string","indexedAt":"string","chunkCount":0,"status":"string","indexError":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiKnowledge-itemsById","tags":["ai","Knowledge Base"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get one knowledge item","description":"Reads one row from public.knowledge_items, scoped to the tenant, in the same shape the list returns. An id that belongs to another tenant cannot be told apart from a missing one: both answer 404. status and indexError show how the last embedding attempt ended."},"patch":{"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"tags":{"type":"array","items":{"type":"string"},"description":"Empty array when the row has no tags"},"category":{"type":["string","null"]},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"indexedAt":{"type":["string","null"],"description":"null while the item has never been embedded"},"chunkCount":{"type":"integer","minimum":0},"status":{"type":"string","description":"Indexing lifecycle: pending, indexed or failed"},"indexError":{"type":["string","null"],"description":"Reason of the last failed embedding, truncated"}},"required":["id","title","content","tags","category","createdBy","createdAt","updatedAt","indexedAt","chunkCount","status","indexError"]},"example":{"id":"string","title":"string","content":"string","tags":["string"],"category":"string","createdBy":"string","createdAt":"string","updatedAt":"string","indexedAt":"string","chunkCount":0,"status":"string","indexError":"string"}}}},"400":{"description":"Kein Mandantenkontext oder leerer Rumpf (no_changes)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"},"503":{"description":"Database unavailable"}},"operationId":"patchApiV1AiKnowledge-itemsById","tags":["ai","Knowledge Base"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update a knowledge item","description":"Admin only, and a true partial update: only the fields present in the body are written, an empty body is refused with 400. Changing title or content resets the lifecycle to pending, clears a previous index_error and re-embeds in the background — the answer therefore reports pending, not a finished index. Changing only tags or category does not re-embed.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":50000},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":60},"maxItems":20},"category":{"type":["string","null"],"minLength":1,"maxLength":60}}},"example":{"title":"string","content":"string","tags":["string"],"category":"string"}}}}},"delete":{"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["deleted","id"]},"example":{"deleted":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"},"503":{"description":"Database unavailable"}},"operationId":"deleteApiV1AiKnowledge-itemsById","tags":["ai","Knowledge Base"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete a knowledge item and its vector chunks","description":"Admin only and final: the row leaves public.knowledge_items, there is no soft delete and no restore. Afterwards the manual chunks of that item are dropped from the vector store — that second step is best-effort, so the item can be gone while its chunks stay behind. An unknown id deletes nothing and gives 404."}},"/api/v1/ai/knowledge-items/upload":{"post":{"responses":{"201":{"description":"Created — the item plus what the extraction produced. status is pending until the background embedding finished.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"tags":{"type":"array","items":{"type":"string"},"description":"Empty array when the row has no tags"},"category":{"type":["string","null"]},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"indexedAt":{"type":["string","null"],"description":"null while the item has never been embedded"},"chunkCount":{"type":"integer","minimum":0},"status":{"type":"string","description":"Indexing lifecycle: pending, indexed or failed"},"indexError":{"type":["string","null"],"description":"Reason of the last failed embedding, truncated"},"sourceFilename":{"type":"string"},"extractedChars":{"type":"integer","minimum":0,"description":"Characters extracted from the file, before truncation"},"truncated":{"type":"boolean","description":"true when the text was cut at 50 000 characters"}},"required":["id","title","content","tags","category","createdBy","createdAt","updatedAt","indexedAt","chunkCount","status","indexError","sourceFilename","extractedChars","truncated"]},"example":{"id":"string","title":"string","content":"string","tags":["string"],"category":"string","createdBy":"string","createdAt":"string","updatedAt":"string","indexedAt":"string","chunkCount":0,"status":"string","indexError":"string","sourceFilename":"string","extractedChars":0,"truncated":true}}}},"400":{"description":"Bad request — unsupported file or no text extracted"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"413":{"description":"File too large"},"415":{"description":"Format bekannt, aber nicht lesbar (DOC/XLS, fehlende Bibliothek)"},"500":{"description":"Insert lieferte keine Zeile zurueck"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiKnowledge-itemsUpload","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Upload a file directly into the knowledge base","description":"Extracts text from an uploaded file (PDF / text / markdown / CSV), creates a knowledge item and embeds it into the tenant vector store."}},"/api/v1/ai/knowledge-items/{id}/reindex":{"post":{"responses":{"200":{"description":"Queued","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean","const":true,"description":"Accepted — the embedding runs after the answer was sent"},"id":{"type":"string"}},"required":["queued","id"]},"example":{"queued":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiKnowledge-itemsByIdReindex","tags":["ai","Knowledge Base"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Re-embed a knowledge item","description":"Admin only. The item is set back to status pending, a previous index_error is cleared and the embedding starts after the answer has been sent — queued: true means accepted, not indexed; ask GET /ai/knowledge-items/{id} for the outcome. The old chunks are dropped first, so during the run the item is briefly not searchable. Is RAG switched off for the tenant, the item ends as indexed with chunkCount 0 and nothing is embedded."}},"/api/v1/ai/knowledge/seed-nemix":{"post":{"responses":{"200":{"description":"Seed completed (created/updated counts returned)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"created":{"type":"integer","description":"Newly inserted knowledge items"},"updated":{"type":"integer","description":"Existing knowledge items re-seeded in place"},"totalChunks":{"type":"integer","description":"Vector chunks written across all documents"},"ragEnabled":{"type":"boolean","description":"False when the tenant RAG config disables embedding"},"docs":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"title":{"type":"string"},"itemId":{"type":"string"},"action":{"type":"string","enum":["created","updated"]},"chunks":{"type":"integer","description":"0 when RAG is disabled or the embed call failed"},"status":{"type":"string","enum":["indexed","failed"]}},"required":["slug","title","itemId","action","chunks","status"]}}},"required":["ok","tenantId","created","updated","totalChunks","ragEnabled","docs"]},"example":{"ok":true,"tenantId":"string","created":0,"updated":0,"totalChunks":0,"ragEnabled":true,"docs":[{"slug":"string","title":"string","itemId":"string","action":"created","chunks":0,"status":"indexed"}]}}}},"401":{"description":"Unauthorized — no tenant context"},"403":{"description":"Forbidden — admin role required"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiKnowledgeSeed-nemix","tags":["ai","Knowledge Base"],"parameters":[],"summary":"Seed built-in Nemix module knowledge into the tenant RAG store","description":"Admin-only. Idempotently inserts/updates the Nemix ERP module documentation (Angebot, Auftrag, Lieferschein, Rechnung/Mahnwesen, Belegkette, Lager, Einkauf, CRM, KI-Workspace) as knowledge items and embeds them so the AI chat can answer how-to questions with citations. Safe to call repeatedly."}},"/api/v1/rag-collections":{"get":{"responses":{"200":{"description":"Die aktiven Sammlungen. Pro Zeile entweder `apiKey` ODER `apiKeyMasked`, je nach Berechtigung.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"source":{"type":"string"},"chunkCount":{"type":"integer"},"active":{"type":"boolean"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"apiKey":{"type":"string"}},"required":["id","name","description","source","chunkCount","active","createdBy","createdAt","updatedAt","apiKey"]},{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"source":{"type":"string"},"chunkCount":{"type":"integer"},"active":{"type":"boolean"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"apiKeyMasked":{"type":"string"}},"required":["id","name","description","source","chunkCount","active","createdBy","createdAt","updatedAt","apiKeyMasked"]}]}}},"required":["data"]},"example":{"data":[{"id":"string","name":"string","description":"string","source":"string","chunkCount":0,"active":true,"createdBy":"string","createdAt":"string","updatedAt":"string","apiKey":"string"}]}}}},"400":{"description":"Kein Mandantenkontext — `error: \"tenant_required\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar (`retryAfter: 5`)."}},"operationId":"getApiV1Rag-collections","tags":["RAG"],"parameters":[],"summary":"Wissenssammlungen auflisten","description":"Die aktiven Wissenssammlungen des Mandanten, neueste zuerst. Abgeschaltete (`active = false`) erscheinen NICHT, und es gibt keinen Schalter, sie zu sehen. Keine Blaetterung.\n\nJE ZEILE ENTSCHEIDET DIE BERECHTIGUNG UEBER DEN FELDNAMEN: wer die Sammlung angelegt hat — oder Administrator ist — bekommt `apiKey` im Klartext, alle anderen `apiKeyMasked`. In EINER Antwort koennen also beide Formen nebeneinander stehen: die eigenen Sammlungen offen, die der Kollegen maskiert. Wer nur auf `apiKey` prueft, sieht bei fremden Zeilen `undefined`.\n\nDer Schluessel ist der Bearer-Credential des Abfrage-Endpunkts — er ist kein Anzeigewert, sondern ein Zugang."},"post":{"responses":{"201":{"description":"Die angelegte Sammlung MIT dem Zugangsschluessel im Klartext.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"source":{"type":"string"},"chunkCount":{"type":"integer"},"active":{"type":"boolean"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"apiKey":{"type":"string"}},"required":["id","name","description","source","chunkCount","active","createdBy","createdAt","updatedAt","apiKey"]},{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"source":{"type":"string"},"chunkCount":{"type":"integer"},"active":{"type":"boolean"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"apiKeyMasked":{"type":"string"}},"required":["id","name","description","source","chunkCount","active","createdBy","createdAt","updatedAt","apiKeyMasked"]}]},"example":{"id":"string","name":"string","description":"string","source":"string","chunkCount":0,"active":true,"createdBy":"string","createdAt":"string","updatedAt":"string","apiKey":"string"}}}},"400":{"description":"Kein Mandantenkontext (`error: \"tenant_required\"`) ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar (`retryAfter: 5`) ODER das `INSERT` lieferte keine Zeile (`error: \"insert_failed\"` — beachte den Status: 503, nicht 500).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"postApiV1Rag-collections","tags":["RAG"],"parameters":[],"summary":"Wissenssammlung anlegen (erzeugt einen Zugangsschluessel)","description":"Legt eine leere, benannte Wissenssammlung fuer den Mandanten an und erzeugt dazu einen Zugangsschluessel (`rc_` + 32 Hexzeichen).\n\nDer Aufruf kostet KEIN Modell-Kontingent und ruft auch keinen Einbettungs-Anbieter auf — die Sammlung ist zunaechst leer. Inhalt kommt erst ueber `POST /api/v1/rag-collections/{id}/ingest` hinein, und DER Aufruf kostet.\n\nDER ERZEUGTE SCHLUESSEL IST EIN ZUGANG, KEIN ANZEIGEWERT. Er ist der Bearer-Credential von `/api/v1/rag-query`: wer ihn hat, kann die Sammlung befragen, OHNE angemeldet zu sein. Diese Antwort zeigt ihn im Klartext — der Ersteller darf seinen eigenen frischen Schluessel sehen. Spaeter geben ihn nur `GET /{id}/key` und die Liste wieder heraus, und auch nur dem Ersteller oder einem Administrator.\n\nES GIBT KEINEN WEG, DEN SCHLUESSEL ZU WECHSELN. Ist er einmal abgeflossen, hilft nur, die ganze Sammlung zu loeschen und neu anzulegen — die aufgenommenen Inhalte gehen dabei mit verloren.\n\nKEINE ROLLENPRUEFUNG: jeder angemeldete Benutzer des Mandanten darf anlegen und bekommt damit einen gueltigen, unangemeldet nutzbaren Schluessel in die Hand.\n\nNamen sind nicht eindeutig — zwei Sammlungen duerfen gleich heissen. `source` steht fest auf `manual`, `active` auf `true`; beides ist ueber diese API nicht zu setzen.\n\nDie Wirkung ist dauerhaft und nur ueber `DELETE /{id}` umkehrbar.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000}},"required":["name"]},"example":{"name":"string","description":"string"}}}}}},"/api/v1/rag-collections/{id}/key":{"get":{"responses":{"200":{"description":"Der Schluessel im Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"apiKey":{"type":"string"}},"required":["id","apiKey"]},"example":{"id":"string","apiKey":"string"}}}},"400":{"description":"Kein Mandantenkontext — `error: \"tenant_required\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Weder Ersteller noch Administrator — `error: \"forbidden\"` plus deutscher Meldung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Nicht gefunden, abgeschaltet ODER fremder Mandant — `error: \"not_found\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar (`retryAfter: 5`)."}},"operationId":"getApiV1Rag-collectionsByIdKey","tags":["RAG"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Den Zugangsschluessel einer Sammlung im Klartext anzeigen","description":"Gibt den `api_key` UNMASKIERT heraus. Er ist der Bearer-Credential des Abfrage-Endpunkts `/api/v1/rag-query` — wer ihn hat, kann die Sammlung befragen, ohne angemeldet zu sein.\n\nDESHALB IST DIESE ROUTE EIGENS GEGATET, obwohl das Modul kein pauschales Rollentor hat: nur der ERSTELLER der Sammlung oder ein Administrator bekommt sie zu sehen, alle anderen einen 403 mit deutschem Klartext. Die Pruefung sitzt im Handler, nicht in einer Middleware.\n\nNur aktive Sammlungen. Der 404 heisst deshalb „gibt es nicht ODER abgeschaltet ODER fremder Mandant\" — drei Faelle, von aussen nicht zu unterscheiden. Das ist hier richtig: die Unterscheidung waere selbst eine Auskunft.\n\nDie Reihenfolge stimmt: `/{id}/key` steht VOR `/{id}` in der Datei, und beide sind ohnehin verschiedene Methoden."}},"/api/v1/rag-collections/{id}":{"delete":{"responses":{"200":{"description":"Geloescht — Sammlung und Dokumente sind fort.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["deleted","id"]},"example":{"deleted":true,"id":"string"}}}},"400":{"description":"Kein Mandantenkontext — `error: \"tenant_required\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Weder Ersteller noch Administrator — `error: \"forbidden\"` plus Klartext. Es wurde NICHTS geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Nicht gefunden ODER fremder Mandant — `error: \"not_found\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar (`retryAfter: 5`)."}},"operationId":"deleteApiV1Rag-collectionsById","tags":["RAG"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Wissenssammlung endgueltig loeschen","description":"HARTE LOESCHUNG MIT CASCADE. Die Sammlung UND alle darin abgelegten Dokumente samt Einbettungen verschwinden. Es gibt kein `deleted_at`, keinen Papierkorb und keinen Weg zurueck — was eingelesen wurde, muss neu eingelesen werden.\n\nNUR DER ERSTELLER ODER EIN ADMINISTRATOR. Dieselbe Schranke wie beim Schluessel (`GET /{id}/key`). Bis zum 17.08.2026 durfte hier JEDER angemeldete Benutzer des Mandanten — den Schluessel ANZUSEHEN war geschuetzt, die ganze Sammlung zu VERNICHTEN nicht. Die schwerere Handlung hatte die schwaechere Schranke.\n\nFUER AUTOMATISIERUNG WICHTIG: ein API-Schluessel traegt die Rolle `api`, die UNTERHALB von `admin` liegt. Ein Schluessel kann eine Sammlung also nur loeschen, wenn sie unter seiner eigenen Kennung angelegt wurde. Vorher konnte jeder Schluessel jede Sammlung des Mandanten vernichten.\n\nAnders als die Leserouten filtert das Loeschen NICHT auf `active`: auch eine abgeschaltete Sammlung wird entfernt.\n\nDer 404 heisst „gibt es nicht ODER fremder Mandant\"."}},"/api/v1/rag-collections/{id}/ingest":{"post":{"responses":{"201":{"description":"Die Abschnitte wurden eingebettet und gespeichert.","content":{"application/json":{"schema":{"type":"object","properties":{"ingested":{"type":"integer"},"chunkCount":{"type":"integer"}},"required":["ingested","chunkCount"],"additionalProperties":false},"example":{"ingested":0,"chunkCount":0}}}},"400":{"description":"Kein Mandantenkontext (`tenant_required`), leerer Text nach dem Trimmen (`empty_text`) ODER der Rumpf verletzt das Pruefschema — dann der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine aktive Sammlung mit dieser Kennung in diesem Mandanten — `error: \"not_found\"`. Eine abgeschaltete Sammlung antwortet ebenso.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar ODER das Einbetten ist gescheitert — die Kennung lautet in BEIDEN Faellen `database_unavailable`. Bereits geschriebene Abschnitte bleiben stehen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1Rag-collectionsByIdIngest","tags":["RAG"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Text aufnehmen (ruft je Abschnitt den Einbettungs-Anbieter)","description":"Zerlegt den uebergebenen Text in Abschnitte von rund 2048 Zeichen, bettet JEDEN Abschnitt ein und speichert ihn mit seinem Vektor in `public.rag_collection_documents`.\n\nDAS KOSTET GELD UND LAEUFT SYNCHRON. Der Einbettungs-Anbieter wird je Abschnitt EINMAL aufgerufen, nacheinander, waehrend die Anfrage offen steht — bei den erlaubten 200.000 Zeichen sind das um die hundert Aufrufe. Ein Sprachmodell wird NICHT befragt; gegen das monatliche KI-Kontingent zaehlt der Aufruf nicht, und in `public.ai_cost_events` erscheint er auch nicht.\n\nEIN FEHLER MITTENDRIN LAESST DIE ARBEIT LIEGEN. Es gibt keine Transaktion: bricht der Einbettungs-Anbieter beim fuenfzigsten Abschnitt ab, sind die ersten neunundvierzig bereits dauerhaft gespeichert. Die Antwort ist dann 503 mit `database_unavailable` — auch wenn die Datenbank in Ordnung war und der Anbieter das Problem hatte. Ein schlichtes Wiederholen nimmt den Text ERNEUT auf: es gibt keine Doppelt-Erkennung, und die schon geschriebenen Abschnitte werden nicht entfernt.\n\nDER AUFRUF IST NICHT EINZELN UMKEHRBAR. Diese API kennt kein Loeschen einzelner Abschnitte; rueckgaengig macht das nur `DELETE /{id}`, das die ganze Sammlung samt allen Abschnitten entfernt.\n\nDIE RAG-EINSTELLUNG DES MANDANTEN WIRD NICHT GEPRUEFT. Anders als `POST /api/v1/ai/knowledge/docs/register`, das bei abgeschaltetem RAG ausdruecklich nichts tut und den Grund nennt, bettet dieser Endpunkt auch dann ein und speichert.\n\nOHNE EINGERICHTETEN ANBIETER ENTSTEHEN AUSSERHALB DER PRODUKTION PLATZHALTER-VEKTOREN. Der Rueckfall ist ein deterministischer Hash-Stub — rechnerisch ein Vektor, semantisch bedeutungslos. Die Antwort sieht aus wie ein gelungener Lauf (`ingested: n`, Status 201), die Sammlung ist danach aber nicht sinnvoll durchsuchbar, und die Platzhalter sind hinterher nicht mehr von echten Vektoren zu unterscheiden. In ECHTER Produktion gibt es diesen Rueckfall nicht: dort scheitert das Einbetten und der Aufruf endet im 503 oben.\n\nDie Sammlung muss dem Mandanten gehoeren UND aktiv sein. Eine abgeschaltete Sammlung ist 404, nicht 403 — nicht zu unterscheiden von einer, die es nicht gibt.\n\n`metadata` wird jedem Abschnitt beigelegt, ergaenzt um `chunkIndex`. Der Inhalt wird nicht geprueft.\n\n`chunkCount` in der Antwort ist die Gesamtzahl der Abschnitte NACH dem Aufruf, `ingested` nur die dieses Aufrufs.\n\nKEINE ROLLENPRUEFUNG: jeder angemeldete Benutzer des Mandanten darf in jede aktive Sammlung schreiben — auch in eine fremde.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":200000},"metadata":{"type":"object","additionalProperties":{}}},"required":["text"]},"example":{"text":"string","metadata":{}}}}}}},"/api/v1/ai/preferences":{"get":{"responses":{"200":{"description":"Preferences payload — the defaults when nothing is stored","content":{"application/json":{"schema":{"type":"object","properties":{"preferred_language":{"type":"string"},"response_style":{"type":"string","enum":["concise","detailed","casual","formal"]},"preferred_currency":{"type":"string"},"preferred_date_format":{"type":"string"},"proactive_suggestions":{"type":"boolean"},"voice_enabled":{"type":"boolean"},"daily_briefing_time":{"type":["string","null"],"description":"Uhrzeit als HH:MM(:SS); null, wenn keine gesetzt ist"},"pinned_facts":{"type":"array","items":{"type":"string"}},"forbidden_topics":{"type":"array","items":{"type":"string"}},"user_id":{"type":"string"},"tenant_id":{"type":"string"}},"required":["preferred_language","response_style","preferred_currency","preferred_date_format","proactive_suggestions","voice_enabled","daily_briefing_time","pinned_facts","forbidden_topics","user_id","tenant_id"]},"example":{"preferred_language":"string","response_style":"concise","preferred_currency":"string","preferred_date_format":"string","proactive_suggestions":true,"voice_enabled":true,"daily_briefing_time":"string","pinned_facts":["string"],"forbidden_topics":["string"],"user_id":"string","tenant_id":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1AiPreferences","tags":["ai"],"parameters":[],"description":"Get the current user's AI preferences (returns defaults when no row exists). Reads public.user_ai_preferences scoped to BOTH the user and the tenant, and creates that table on first use when it is missing. Anything that goes wrong on the way — no database, a failed read — is answered with the defaults instead of an error, so a 200 here does not prove that stored preferences were found.","summary":"Get the current user's AI preferences (returns defaults when no row exists)","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Updated preferences — the complete merged set, not just the changes","content":{"application/json":{"schema":{"type":"object","properties":{"preferred_language":{"type":"string"},"response_style":{"type":"string","enum":["concise","detailed","casual","formal"]},"preferred_currency":{"type":"string"},"preferred_date_format":{"type":"string"},"proactive_suggestions":{"type":"boolean"},"voice_enabled":{"type":"boolean"},"daily_briefing_time":{"type":["string","null"],"description":"Uhrzeit als HH:MM(:SS); null, wenn keine gesetzt ist"},"pinned_facts":{"type":"array","items":{"type":"string"}},"forbidden_topics":{"type":"array","items":{"type":"string"}},"user_id":{"type":"string"},"tenant_id":{"type":"string"},"ok":{"type":"boolean","const":true}},"required":["preferred_language","response_style","preferred_currency","preferred_date_format","proactive_suggestions","voice_enabled","daily_briefing_time","pinned_facts","forbidden_topics","user_id","tenant_id","ok"]},"example":{"preferred_language":"string","response_style":"concise","preferred_currency":"string","preferred_date_format":"string","proactive_suggestions":true,"voice_enabled":true,"daily_briefing_time":"string","pinned_facts":["string"],"forbidden_topics":["string"],"user_id":"string","tenant_id":"string","ok":true}}}},"401":{"description":"Unauthorized"},"500":{"description":"Upsert fehlgeschlagen"},"503":{"description":"Database unavailable"}},"operationId":"putApiV1AiPreferences","tags":["ai"],"parameters":[],"description":"Upsert the current user's AI preferences. The body may carry a subset, but this is NOT a partial update: every field the body omits is written with its DEFAULT value, not with the value that was stored. The row is keyed on the user alone, so a user who belongs to several tenants has one shared set of preferences and the last write moves the row to that tenant. The answer echoes the whole merged set.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"preferred_language":{"type":"string","minLength":2,"maxLength":8},"response_style":{"type":"string","enum":["concise","detailed","casual","formal"]},"preferred_currency":{"type":"string","minLength":3,"maxLength":8},"preferred_date_format":{"type":"string","minLength":3,"maxLength":32},"proactive_suggestions":{"type":"boolean"},"voice_enabled":{"type":"boolean"},"daily_briefing_time":{"type":["string","null"],"pattern":"^\\d{2}:\\d{2}(:\\d{2})?$"},"pinned_facts":{"type":"array","items":{"type":"string","maxLength":500},"maxItems":50},"forbidden_topics":{"type":"array","items":{"type":"string","maxLength":120},"maxItems":50}}},"example":{"preferred_language":"string","response_style":"concise","preferred_currency":"string","preferred_date_format":"string","proactive_suggestions":true,"voice_enabled":true,"daily_briefing_time":null,"pinned_facts":["string"],"forbidden_topics":["string"]}}}},"summary":"Upsert the current user's AI preferences","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/preferences/admin/{userId}":{"get":{"responses":{"200":{"description":"Preferences payload — the defaults when nothing is stored","content":{"application/json":{"schema":{"type":"object","properties":{"preferred_language":{"type":"string"},"response_style":{"type":"string","enum":["concise","detailed","casual","formal"]},"preferred_currency":{"type":"string"},"preferred_date_format":{"type":"string"},"proactive_suggestions":{"type":"boolean"},"voice_enabled":{"type":"boolean"},"daily_briefing_time":{"type":["string","null"],"description":"Uhrzeit als HH:MM(:SS); null, wenn keine gesetzt ist"},"pinned_facts":{"type":"array","items":{"type":"string"}},"forbidden_topics":{"type":"array","items":{"type":"string"}},"user_id":{"type":"string"},"tenant_id":{"type":"string"}},"required":["preferred_language","response_style","preferred_currency","preferred_date_format","proactive_suggestions","voice_enabled","daily_briefing_time","pinned_facts","forbidden_topics","user_id","tenant_id"]},"example":{"preferred_language":"string","response_style":"concise","preferred_currency":"string","preferred_date_format":"string","proactive_suggestions":true,"voice_enabled":true,"daily_briefing_time":"string","pinned_facts":["string"],"forbidden_topics":["string"],"user_id":"string","tenant_id":"string"}}}},"400":{"description":"userId fehlt"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"operationId":"getApiV1AiPreferencesAdminByUserId","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"description":"Admin-only: fetch any user's AI preferences within the tenant. Requires the admin role or higher, or owner. The read is scoped to the calling tenant as well as to the user id, so a user id from another tenant yields the defaults rather than foreign data. A user without a stored row cannot be told apart from one who never changed anything — both come back as the defaults.","summary":"Admin-only: fetch any user's AI preferences within the tenant","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/templates":{"get":{"responses":{"200":{"description":"Die sichtbaren Vorlagen — oder die abgeschwaechte Form mit `degraded: true`, wenn die Datenbank nicht erreichbar war.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"isOwner":{"type":"boolean"},"title":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"systemPrompt":{"type":["string","null"]},"userPrompt":{"type":"string"},"variables":{"type":"array","items":{}},"visibility":{"type":"string"},"approvedBy":{"type":["string","null"]},"approvedAt":{"type":["string","null"]},"useCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","userId","isOwner","title","description","category","systemPrompt","userPrompt","variables","visibility","approvedBy","approvedAt","useCount","createdAt","updatedAt"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"isOwner":{"type":"boolean"},"title":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"systemPrompt":{"type":["string","null"]},"userPrompt":{"type":"string"},"variables":{"type":"array","items":{}},"visibility":{"type":"string"},"approvedBy":{"type":["string","null"]},"approvedAt":{"type":["string","null"]},"useCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","userId","isOwner","title","description","category","systemPrompt","userPrompt","variables","visibility","approvedBy","approvedAt","useCount","createdAt","updatedAt"],"additionalProperties":false}},"degraded":{"type":"boolean","const":true},"error":{"type":"string"}},"required":["items","degraded"],"additionalProperties":false}]},"example":{"items":[{"id":"string","tenantId":"string","userId":"string","isOwner":true,"title":"string","description":"string","category":"string","systemPrompt":"string","userPrompt":"string","variables":[],"visibility":"string","approvedBy":"string","approvedAt":"string","useCount":0,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AiTemplates","tags":["ai","templates"],"parameters":[],"summary":"List own private templates + tenant-shared approved + public approved.","description":"Liest `public.ai_templates`, gefiltert auf den Mandanten. Sichtbar sind\ndie eigenen privaten Vorlagen sowie geteilte (`tenant`) und\noeffentliche (`public`), sofern sie freigegeben sind — eine geteilte,\naber noch nicht freigegebene Vorlage taucht bei Fremden nicht auf.\n\nDie Abfragefelder `category` und `visibility` filtern zusaetzlich. Sie\nwerden NICHT geprueft: ein unbekannter Wert liefert eine leere Liste,\nkeinen 400. Sortiert wird nach Nutzungszahl, dann nach Aenderungsdatum.\nDie Ausgabe ist bei 500 Zeilen abgeschnitten; es gibt weder ein\nSeitenlimit noch eine Gesamtzahl, an der man das Abschneiden erkennt.\n\nACHTUNG: Fehlt der Datenbank-Client oder scheitert die Abfrage,\nantwortet der Endpunkt trotzdem 200 mit leerer Liste und `degraded:\ntrue` — nicht 503. Wer nur den Status prueft, haelt einen Ausfall fuer\n„keine Vorlagen vorhanden\".\n\n`isOwner` steht nicht in der Zeile, sondern wird beim Ausliefern aus dem\nAnfragekontext berechnet.\n`variables` ist eine JSONB-Spalte. Der Inhalt kommt unveraendert aus einer JSONB-Spalte. Es werden KEINE Feldnamen zugesagt — was heute darin steht, hat der Schreibpfad hineingelegt, nicht dieser Vertrag."},"post":{"responses":{"201":{"description":"Die angelegte Vorlage, wie sie in der Datenbank steht.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"isOwner":{"type":"boolean"},"title":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"systemPrompt":{"type":["string","null"]},"userPrompt":{"type":"string"},"variables":{"type":"array","items":{}},"visibility":{"type":"string"},"approvedBy":{"type":["string","null"]},"approvedAt":{"type":["string","null"]},"useCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","userId","isOwner","title","description","category","systemPrompt","userPrompt","variables","visibility","approvedBy","approvedAt","useCount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","isOwner":true,"title":"string","description":"string","category":"string","systemPrompt":"string","userPrompt":"string","variables":[],"visibility":"string","approvedBy":"string","approvedAt":"string","useCount":0,"createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Der Rumpf entspricht nicht dem Pruefschema. Der Koerper kommt roh aus dem Validator.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Das Einfuegen ist gescheitert. `message` traegt den rohen Fehlertext des Treibers.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"db_insert_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiTemplates","tags":["ai","templates"],"parameters":[],"summary":"Create a new AI template (defaults to visibility=private).","description":"Legt eine Zeile in `public.ai_templates` an — Eigentuemer ist der\nanfragende Nutzer, Mandant der aus dem Anfragekontext. Es wird kein\nSprachmodell befragt; der Aufruf kostet kein Kontingent.\n\n`visibility` WIRD ANGENOMMEN UND VERWORFEN: der Handler schreibt immer\n`private`. Eine Vorlage wird erst ueber `POST /{id}/share` geteilt und\ndann freigegeben — ein `visibility: \"tenant\"` beim Anlegen bleibt ohne\nWirkung, ohne dass die Antwort darauf hinweist.\n\n`variables` wird als JSONB gespeichert. Der Schreibpfad prueft die\nStruktur (Name, Beschriftung, Typ), die Datenbank nicht.\n\nScheitert das Einfuegen, kommt 500 mit der rohen Treibermeldung — nicht\nder 503, den die uebrigen Vorlagen-Routen bei Datenbankproblemen liefern.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000},"category":{"type":"string","maxLength":80},"system_prompt":{"type":"string","maxLength":8000},"user_prompt":{"type":"string","minLength":1,"maxLength":8000},"variables":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-zA-Z0-9_]+$","minLength":1,"maxLength":64},"label":{"type":"string","minLength":1,"maxLength":120},"type":{"type":"string","enum":["string","number","date","select","textarea"],"default":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string","maxLength":2000},"options":{"type":"array","items":{"type":"string","maxLength":120},"maxItems":50}},"required":["name","label"]},"maxItems":50,"default":[]},"visibility":{"type":"string","enum":["private","tenant","public"],"default":"private"}},"required":["title","user_prompt"]},"example":{"title":"string","description":"string","category":"string","system_prompt":"string","user_prompt":"string","variables":[],"visibility":"private"}}}}}},"/api/v1/ai/templates/{id}":{"get":{"responses":{"200":{"description":"Die Vorlage.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"isOwner":{"type":"boolean"},"title":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"systemPrompt":{"type":["string","null"]},"userPrompt":{"type":"string"},"variables":{"type":"array","items":{}},"visibility":{"type":"string"},"approvedBy":{"type":["string","null"]},"approvedAt":{"type":["string","null"]},"useCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","userId","isOwner","title","description","category","systemPrompt","userPrompt","variables","visibility","approvedBy","approvedAt","useCount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","isOwner":true,"title":"string","description":"string","category":"string","systemPrompt":"string","userPrompt":"string","variables":[],"visibility":"string","approvedBy":"string","approvedAt":"string","useCount":0,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Fremde Vorlage, die nicht (oder noch nicht freigegeben) geteilt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Keine Vorlage mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AiTemplatesById","tags":["ai","templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine KI-Vorlage lesen","description":"Liefert eine Vorlage aus `public.ai_templates`.\n\nSichtbar ist sie, wenn eine der vier Bedingungen zutrifft: der\nAnfragende ist Administrator, er ist der Eigentuemer, oder die Vorlage\nist auf `tenant` bzw. `public` gestellt UND freigegeben. Eine geteilte,\naber noch nicht freigegebene Vorlage ist fuer Fremde 403.\n\nDie Abfrage filtert nach Mandant; eine Vorlage aus einem anderen\nMandanten ist 404. Ein 404 heisst „fuer dich nicht sichtbar\", nicht zwingend „existiert nicht\": die Abfrage filtert bereits nach Mandant.\n\n`isOwner` gehoert nicht zur Zeile, sondern wird beim Ausliefern aus dem\nAnfragekontext berechnet — dieselbe Vorlage sieht fuer zwei Nutzer\nunterschiedlich aus.\n\n`variables` ist eine JSONB-Spalte. Der Inhalt kommt unveraendert aus einer JSONB-Spalte. Es werden KEINE Feldnamen zugesagt — was heute darin steht, hat der Schreibpfad hineingelegt, nicht dieser Vertrag.\nDer Schreibpfad prueft sie zwar, die Datenbank aber nicht.\n\nUm die Abfrage steht KEIN `try`/`catch`: scheitert sie, kommt der\nzentrale 500 aus `app.onError` — nicht 503."},"patch":{"responses":{"200":{"description":"Die Vorlage im gespeicherten Zustand nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"isOwner":{"type":"boolean"},"title":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"systemPrompt":{"type":["string","null"]},"userPrompt":{"type":"string"},"variables":{"type":"array","items":{}},"visibility":{"type":"string"},"approvedBy":{"type":["string","null"]},"approvedAt":{"type":["string","null"]},"useCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","userId","isOwner","title","description","category","systemPrompt","userPrompt","variables","visibility","approvedBy","approvedAt","useCount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","isOwner":true,"title":"string","description":"string","category":"string","systemPrompt":"string","userPrompt":"string","variables":[],"visibility":"string","approvedBy":"string","approvedAt":"string","useCount":0,"createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Weder Eigentuemer noch Administrator.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Keine Vorlage mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"patchApiV1AiTemplatesById","tags":["ai","templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"KI-Vorlage aendern (visibility im Rumpf wird stillschweigend verworfen)","description":"Aendert einzelne Felder einer Vorlage in `public.ai_templates`. Nur die\nmitgeschickten Felder werden geschrieben; die uebrigen bleiben stehen.\n\nDer Aufruf kostet KEIN Modell-Kontingent: es wird kein Sprachmodell\nbefragt, nur eine Zeile geaendert. Die Wirkung ist dauerhaft und nur\ndadurch umkehrbar, dass man die alten Werte erneut schreibt — einen\nVerlauf gibt es nicht.\n\n`visibility` WIRD ANGENOMMEN UND VERWORFEN. Das Pruefschema laesst das\nFeld zu (es entsteht als `partial()` des Anlege-Schemas), die\nFeldzuordnung des Handlers kennt es aber nicht. Ein\n`{\"visibility\":\"tenant\"}` liefert 200 und aendert nichts — ohne Hinweis.\nGeteilt wird ueber `POST /api/v1/ai/templates/{id}/share`; einen Weg\nzurueck auf `private` gibt es nirgends.\n\nDIE ANTWORT IST DER GESPEICHERTE STAND (`RETURNING *`), nicht der Rumpf —\ndaran laesst sich ablesen, was wirklich angekommen ist.\n\nEIN LEERER RUMPF `{}` IST GUELTIG. Alle Felder sind wahlweise; der\nHandler schreibt dann nur `updated_at = NOW()` und antwortet 200. Der\nAufruf ist also nie ein „nichts zu tun\"-Fehler.\n\n`variables` wird als Ganzes ersetzt, nicht verschmolzen.\nDer Inhalt landet in einer JSONB-Spalte. Der Inhalt kommt unveraendert aus einer JSONB-Spalte. Es werden KEINE Feldnamen zugesagt — was heute darin steht, hat der Schreibpfad hineingelegt, nicht dieser Vertrag.\n\nAendern darf der Eigentuemer oder ein Administrator. Die Pruefung laeuft\nim Handler ueber die Rolle aus dem Anfragekontext, nicht ueber\n`requireMinRole` — deshalb traegt der 403 hier den knappen Koerper.\n\nDie Abfrage filtert nach Mandant; eine Vorlage aus einem anderen\nMandanten ist 404. Ein 404 heisst „fuer dich nicht sichtbar\", nicht zwingend „existiert nicht\": die Abfrage filtert bereits nach Mandant.\n\nUM DIE ABFRAGEN STEHT KEIN `try`/`catch`: scheitert eine, kommt der\nzentrale 500 aus `app.onError` — nicht 503.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000},"category":{"type":"string","maxLength":80},"system_prompt":{"type":"string","maxLength":8000},"user_prompt":{"type":"string","minLength":1,"maxLength":8000},"variables":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-zA-Z0-9_]+$","minLength":1,"maxLength":64},"label":{"type":"string","minLength":1,"maxLength":120},"type":{"type":"string","enum":["string","number","date","select","textarea"],"default":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string","maxLength":2000},"options":{"type":"array","items":{"type":"string","maxLength":120},"maxItems":50}},"required":["name","label"]},"maxItems":50,"default":[]},"visibility":{"type":"string","enum":["private","tenant","public"],"default":"private"}}},"example":{"title":"string","description":"string","category":"string","system_prompt":"string","user_prompt":"string","variables":[],"visibility":"private"}}}}},"delete":{"responses":{"200":{"description":"Geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Weder Eigentuemer noch Administrator.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Keine Vorlage mit dieser Kennung in diesem Mandanten — oder schon geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1AiTemplatesById","tags":["ai","templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"KI-Vorlage endgueltig loeschen","description":"Entfernt die Zeile endgueltig aus `public.ai_templates` — echtes\n`DELETE`, kein `deleted_at`, keine Wiederherstellung. Ein zweiter\nAufruf antwortet mit 404.\n\nLoeschen darf der Eigentuemer oder ein Administrator. Die Pruefung\nlaeuft im Handler ueber die Rolle aus dem Anfragekontext, nicht ueber\n`requireMinRole`.\n\nDAS TRIFFT AUCH GETEILTE VORLAGEN: eine freigegebene `tenant`-Vorlage,\ndie Kollegen benutzen, verschwindet mit diesem Aufruf fuer alle. Es gibt\nkeine Warnung und keine Pruefung auf `useCount`.\n\nUm die Abfragen steht kein `try`/`catch`: scheitert eine, kommt der\nzentrale 500 aus `app.onError`."}},"/api/v1/ai/templates/{id}/share":{"post":{"responses":{"200":{"description":"Zum Teilen eingereicht, Freigabe steht aus. Eine vorhandene Freigabe ist damit weg.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"status":{"type":"string","const":"pending_approval"}},"required":["ok","status"],"additionalProperties":false},"example":{"ok":true,"status":"pending_approval"}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Weder Eigentuemer noch Administrator.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Keine Vorlage mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiTemplatesByIdShare","tags":["ai","templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Vorlage zum Teilen einreichen (sichtbar wird sie erst nach Freigabe)","description":"Setzt `visibility` auf `tenant` und LOESCHT eine bestehende Freigabe\n(`approved_at`, `approved_by` werden auf `null` gesetzt). Sichtbar fuer\nKollegen wird die Vorlage erst, wenn ein Administrator\n`POST /api/v1/ai/templates/{id}/approve` aufruft.\n\nAUF EINER BEREITS FREIGEGEBENEN VORLAGE WIRKT DIESER AUFRUF WIE EIN\nENTZUG: die Freigabe faellt weg, und die Vorlage verschwindet fuer alle\nausser Eigentuemer und Administratoren, bis sie erneut freigegeben ist.\nDie Antwort sagt das nicht — sie lautet in beiden Faellen\n`{ ok: true, status: \"pending_approval\" }`.\n\nES GIBT KEINEN WEG ZURUECK ZU `private`: `PATCH /api/v1/ai/templates/{id}`\nbildet `visibility` bewusst nicht ab, und einen Gegenendpunkt gibt es\nnicht.\n\nEinreichen darf der Eigentuemer oder ein Administrator."}},"/api/v1/ai/templates/{id}/approve":{"post":{"responses":{"200":{"description":"Freigegeben. Enthaelt die Vorlage im neuen Zustand.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"template":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"isOwner":{"type":"boolean"},"title":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"systemPrompt":{"type":["string","null"]},"userPrompt":{"type":"string"},"variables":{"type":"array","items":{}},"visibility":{"type":"string"},"approvedBy":{"type":["string","null"]},"approvedAt":{"type":["string","null"]},"useCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","userId","isOwner","title","description","category","systemPrompt","userPrompt","variables","visibility","approvedBy","approvedAt","useCount","createdAt","updatedAt"],"additionalProperties":false}},"required":["ok","template"],"additionalProperties":false},"example":{"ok":true,"template":{"id":"string","tenantId":"string","userId":"string","isOwner":true,"title":"string","description":"string","category":"string","systemPrompt":"string","userPrompt":"string","variables":[],"visibility":"string","approvedBy":"string","approvedAt":"string","useCount":0,"createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Vorlage nicht vorhanden ODER noch `private`. NICHT: „schon freigegeben\".","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found_or_not_pending"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiTemplatesByIdApprove","tags":["ai","templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Geteilte Vorlage freigeben (auch mehrfach moeglich)","description":"Traegt den freigebenden Administrator und den Zeitpunkt ein. Erst\ndanach sehen Kollegen eine mit `POST /{id}/share` eingereichte Vorlage.\n\nDIE FEHLERKENNUNG IST IRREFUEHREND. Die Bedingung der Abfrage lautet\n`visibility IN ('tenant', 'public')` — von „offen\" oder „noch nicht\nfreigegeben\" steht dort nichts. Daraus folgt:\n\n  · Eine BEREITS freigegebene Vorlage wird erneut freigegeben. Die\n    Antwort ist 200, und `approvedBy`/`approvedAt` zeigen ab dann auf\n    den zuletzt Freigebenden. Ein Aufrufer, der `not_found_or_not_pending`\n    als Schutz gegen Doppelfreigabe versteht, irrt.\n  · Der 404 mit `not_found_or_not_pending` bedeutet in Wahrheit: es gibt\n    keine Vorlage mit dieser Kennung in diesem Mandanten ODER sie steht\n    noch auf `private`, wurde also gar nicht eingereicht.\n\nVerlangt mindestens die Rolle `admin` (ueber `requireMinRole`, deshalb\ntraegt der 403 den ausfuehrlichen Rollen-Koerper)."}},"/api/v1/ai/templates/{id}/use":{"post":{"responses":{"200":{"description":"Die befuellten Prompts. `missing_variables` nennt die Platzhalter, die offen geblieben sind.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"template_id":{"type":"string"},"filled_user_prompt":{"type":"string"},"filled_system_prompt":{"type":["string","null"]},"missing_variables":{"type":"array","items":{"type":"string"}}},"required":["ok","template_id","filled_user_prompt","filled_system_prompt","missing_variables"],"additionalProperties":false},"example":{"ok":true,"template_id":"string","filled_user_prompt":"string","filled_system_prompt":"string","missing_variables":["string"]}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Fremde Vorlage, die nicht (oder noch nicht freigegeben) geteilt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Keine Vorlage mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiTemplatesByIdUse","tags":["ai","templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Vorlage mit Werten befuellen (fragt KEIN Modell, fuehrt nichts aus)","description":"Ersetzt die `{{platzhalter}}` in Nutzer- und System-Prompt der Vorlage\ndurch die uebergebenen Werte und gibt die fertigen Texte zurueck.\n\nTROTZ DES NAMENS WIRD NICHTS AUSGEFUEHRT. Es wird KEIN Sprachmodell\nbefragt, und der Aufruf kostet KEIN Modell-Kontingent — er ist reine\nTextersetzung. Wer den erzeugten Prompt tatsaechlich verwenden will,\nschickt ihn anschliessend selbst an den Chat; DER Aufruf kostet dann.\n\nGESCHRIEBEN WIRD TROTZDEM ETWAS: `use_count` der Vorlage wird um eins\nerhoeht — nebenlaeufig und ohne Absicherung. Scheitert das Hochzaehlen,\nbleibt die Antwort `ok: true`; der Zaehler ist also eine Naeherung, kein\nBeleg. Zurueckdrehen laesst er sich nicht.\n\nFEHLENDE WERTE SIND KEIN FEHLER. Ein Platzhalter ohne Wert (fehlend,\n`null` oder leerer Text) bleibt als `{{name}}` woertlich im Ergebnis\nstehen und wird in `missing_variables` aufgezaehlt. Die Antwort ist\ntrotzdem 200 — wer den Prompt ungeprueft weiterreicht, schickt die\nKlammern mit.\n\nDAS KENNZEICHEN `required` DER VORLAGE WIRD NICHT DURCHGESETZT. Eine als\npflichtig deklarierte Variable darf hier fehlen; sie erscheint dann nur\nin `missing_variables`. Ebenso wenig werden hinterlegte `default`-Werte\neingesetzt — es zaehlt allein, was im Rumpf steht.\n\nWerte werden nach Text gewandelt; Objekte und Listen als JSON. Es findet\nkeine Typpruefung gegen die Variablendefinition statt.\n\nDie Felder der Antwort sind snake_case — abweichend von den uebrigen\nOperationen dieses Routers.\n\nSichtbar (und damit benutzbar) ist eine Vorlage fuer Administratoren,\nfuer ihren Eigentuemer und fuer alle, wenn sie auf `tenant` oder\n`public` steht UND freigegeben ist. Eine eingereichte, aber noch nicht\nfreigegebene Vorlage ist fuer Fremde 403.\n\nDie Abfrage filtert nach Mandant; eine Vorlage aus einem anderen\nMandanten ist 404. Ein 404 heisst „fuer dich nicht sichtbar\", nicht zwingend „existiert nicht\": die Abfrage filtert bereits nach Mandant.\n\nUM DIE ABFRAGE STEHT KEIN `try`/`catch`: scheitert sie, kommt der\nzentrale 500 aus `app.onError` — nicht 503.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{},"default":{}}}},"example":{"variables":{}}}}}}},"/api/v1/ai/undo/eligible":{"get":{"responses":{"200":{"description":"Eligible rows, newest first, at most 20. `degraded: true` means the list could not be read at all — an empty `items` then says nothing about what is reversible.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":["string","null"]},"userId":{"type":["string","null"]},"action":{"type":"string"},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"revertPayload":{},"createdAt":{"type":"string"},"expiresAt":{"type":"string"},"remainingMs":{"type":"number"},"revertedAt":{"type":["string","null"]},"revertedBy":{"type":["string","null"]}},"required":["id","tenantId","userId","action","entityType","entityId","createdAt","expiresAt","remainingMs","revertedAt","revertedBy"],"additionalProperties":false}},"degraded":{"type":"boolean","const":true},"error":{"type":"string"}},"required":["items"]},"example":{"items":[{"id":"string","tenantId":"string","userId":"string","action":"string","entityType":"string","entityId":"string","createdAt":"string","expiresAt":"string","remainingMs":0,"revertedAt":"string","revertedBy":"string"}],"degraded":true,"error":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1AiUndoEligible","tags":["ai","undo"],"parameters":[],"summary":"Lists the reversible AI actions of the current user","description":"List the current user's last 20 reversible AI actions within the 5-minute undo window."}},"/api/v1/ai/undo/{activity_log_id}":{"post":{"responses":{"200":{"description":"Reverted — `status` is the short sentence the revert strategy reported","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"reverted_action":{"type":"string"},"reverted_at":{"type":"string"},"status":{"type":["string","null"]}},"required":["ok","reverted_action","reverted_at","status"],"additionalProperties":false},"example":{"ok":true,"reverted_action":"string","reverted_at":"string","status":"string"}}}},"400":{"description":"`missing_id`"},"401":{"description":"Unauthorized"},"403":{"description":"`forbidden` — the entry belongs to a different user"},"404":{"description":"Activity not found or not eligible"},"409":{"description":"Already reverted, or the entry is itself an undo"},"410":{"description":"Undo window expired (>5 min)"},"500":{"description":"Lookup or replay failed — nothing was marked as reverted"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiUndoByActivity_log_id","tags":["ai","undo"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"activity_log_id","required":true}],"summary":"Revert a previously-applied AI action by replaying its inverse payload","description":"The entry is looked up within the calling tenant only. An entry that carries a `user_id` can be reverted by that same user alone (403 otherwise); one without a user is not restricted that way. Four guards run before anything is replayed: already reverted (409), an `undo` entry itself (409), no stored inverse payload (404), and older than five minutes (410). The inverse payload is replayed FIRST — if that fails, nothing is marked and the answer is 500. On success the original row gets `reverted_at` and `reverted_by`, and a second `activity_log` row with action `undo` is written that points back at it; should that bookkeeping fail, it is only logged and the answer stays 200."}},"/api/v1/ai/undo-record":{"post":{"responses":{"200":{"description":"Recorded — returns the activity_log id to undo with","content":{"application/json":{"schema":{"type":"object","properties":{"activityLogId":{"type":"string","description":"Id of the inserted public.activity_log row — the undo token for POST /ai/undo/:id"}},"required":["activityLogId"]},"example":{"activityLogId":"string"}}}},"400":{"description":"Missing actionType / revertPayload"},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiUndo-record","tags":["ai","undo"],"parameters":[],"summary":"Persists a reversible AI action and returns its undo token","description":"Persist a reversible AI action (inverse payload) and return its activity_log id (undoToken)."}},"/api/v1/ai/conversations":{"get":{"responses":{"200":{"description":"Die Gespraeche ohne ihre Zuege. `degraded: true` heisst: die Liste ist leer, weil nicht gelesen werden konnte — nicht, weil es keine gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"summaryUntilTurn":{"type":"number"},"forkedFrom":{"type":["string","null"]},"forkedAtTurn":{"type":["number","null"]},"totalTokens":{"type":"number"},"totalCostEur":{"type":"number"},"messageCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","userId","title","summary","summaryUntilTurn","forkedFrom","forkedAtTurn","totalTokens","totalCostEur","messageCount","createdAt","updatedAt"],"additionalProperties":false}},"degraded":{"type":"boolean","const":true},"error":{"type":"string","const":"database_unavailable"}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","tenantId":"string","userId":"string","title":"string","summary":"string","summaryUntilTurn":0,"forkedFrom":"string","forkedAtTurn":0,"totalTokens":0,"totalCostEur":0,"messageCount":0,"createdAt":"string","updatedAt":"string"}],"degraded":true,"error":"database_unavailable"}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AiConversations","tags":["ai","conversations"],"parameters":[],"summary":"Die eigenen Gespraeche auflisten","description":"Liefert die Gespraeche des angemeldeten Nutzers, zuletzt geaendertes\nzuerst. Gefiltert wird nach Mandant UND Nutzer — das Gespraech eines\nKollegen taucht hier nicht auf, auch nicht fuer einen Administrator.\n\nDie Zuege fehlen: `messages` traegt diese Liste NICHT, nur\n`messageCount`. Den Verlauf liefert `GET /api/v1/ai/conversations/{id}`.\n\nHoechstens 100 Zeilen, ohne Blaetterung und ohne Filter — wer mehr\nGespraeche hat, sieht die aelteren hier nicht. Soft-geloeschte\n(`deleted_at`) bleiben aussen vor.\n\nACHTUNG: die Route antwortet AUCH bei einem Ausfall mit 200. Ohne\nDatenbank-Client kommt eine leere Liste mit `degraded: true`, nach einem\nLesefehler zusaetzlich `error`. Eine leere Liste allein beweist also\nnicht, dass es keine Gespraeche gibt."},"post":{"responses":{"201":{"description":"Das angelegte Gespraech, mit `messages`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"summaryUntilTurn":{"type":"number"},"forkedFrom":{"type":["string","null"]},"forkedAtTurn":{"type":["number","null"]},"totalTokens":{"type":"number"},"totalCostEur":{"type":"number"},"messageCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"},"tool":{"type":"string"},"ts":{"type":"string"}},"required":["role","content"]}}},"required":["id","tenantId","userId","title","summary","summaryUntilTurn","forkedFrom","forkedAtTurn","totalTokens","totalCostEur","messageCount","createdAt","updatedAt","messages"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","title":"string","summary":"string","summaryUntilTurn":0,"forkedFrom":"string","forkedAtTurn":0,"totalTokens":0,"totalCostEur":0,"messageCount":0,"createdAt":"string","updatedAt":"string","messages":[{"role":"user","content":"string","tool":"string","ts":"string"}]}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Das `INSERT` lief ohne Fehler, lieferte aber keine Zeile zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"insert_failed"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client (ohne `retryAfter`) ODER die Abfrage ist gescheitert (mit `retryAfter`).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"postApiV1AiConversations","tags":["ai","conversations"],"parameters":[],"summary":"Ein neues Gespraech anlegen","description":"Legt eine Zeile in `public.ai_conversations` an — Eigentuemer sind der\nMandant und der anfragende Nutzer aus dem Anfragekontext, nie Angaben\naus dem Rumpf.\n\nDer Aufruf kostet KEIN Modell-Kontingent: es wird kein Sprachmodell\nbefragt und nichts eingebettet, nur gespeichert. `initialMessages` wird\nunveraendert nach `messages_jsonb` geschrieben — die Zuege werden nicht\nan ein Modell geschickt.\n\nDie Wirkung ist dauerhaft. Umkehrbar nur als Soft-Delete:\n`DELETE /api/v1/ai/conversations/{id}` setzt `deleted_at`, die Inhalte\nbleiben gespeichert. Ein echtes Loeschen bietet dieser Router nicht.\n\nBis zu 20 Anfangszuege sind erlaubt, je bis 64.000 Zeichen. Ohne\n`initialMessages` entsteht ein leeres Gespraech.\n\n`title` ist frei und darf fehlen — es wird nicht erzeugt und bleibt dann\n`null`.\n\nHINWEIS ZUM SPEICHER: `public.ai_conversations` wird von zwei Stellen im\nHaus beansprucht — von diesen Routen (eine Zeile je Gespraech, Zuege in\n`messages_jsonb`) und vom Gespraechsgedaechtnis des Chats (eine Zeile je\nZug). Nur die erste Form wird hier angelegt; ein so erzeugtes Gespraech\nist im Chatverlauf nicht zu sehen.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer legt fuer sich selbst\nan.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","maxLength":200},"initialMessages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string","maxLength":64000},"tool":{"type":"string","maxLength":120},"ts":{"type":"string","format":"date-time"}},"required":["role","content"]},"maxItems":20,"default":[]}}},"example":{"title":"string","initialMessages":[{"role":"user","content":"string","tool":"string","ts":"2026-01-01T12:00:00.000Z"}]}}}}}},"/api/v1/ai/conversations/{id}":{"get":{"responses":{"200":{"description":"Das Gespraech mit seinen Zuegen.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"summaryUntilTurn":{"type":"number"},"forkedFrom":{"type":["string","null"]},"forkedAtTurn":{"type":["number","null"]},"totalTokens":{"type":"number"},"totalCostEur":{"type":"number"},"messageCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"},"tool":{"type":"string"},"ts":{"type":"string"}},"required":["role","content"]}}},"required":["id","tenantId","userId","title","summary","summaryUntilTurn","forkedFrom","forkedAtTurn","totalTokens","totalCostEur","messageCount","createdAt","updatedAt","messages"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","title":"string","summary":"string","summaryUntilTurn":0,"forkedFrom":"string","forkedAtTurn":0,"totalTokens":0,"totalCostEur":0,"messageCount":0,"createdAt":"string","updatedAt":"string","messages":[{"role":"user","content":"string","tool":"string","ts":"string"}]}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Kein Gespraech mit dieser Kennung fuer diesen Nutzer — oder bereits geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client (ohne `retryAfter`) ODER die Abfrage ist gescheitert (mit `retryAfter`).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"getApiV1AiConversationsById","tags":["ai","conversations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ein Gespraech mit allen Zuegen lesen","description":"Liefert ein Gespraech samt `messages`. Die Liste in\n`GET /api/v1/ai/conversations` traegt die Zuege NICHT — nur\n`messageCount`.\n\nDie Abfrage filtert nach Mandant UND Nutzer. Ein 404 heisst „fuer dich nicht sichtbar\", nicht zwingend „existiert nicht\": die Abfrage filtert bereits nach Mandant.\nDas Gespraech eines Kollegen ist hier also 404, nicht 403 — auch fuer\neinen Administrator.\n\n`messages: []` heisst nicht zwingend „leeres Gespraech\": ist der\ngespeicherte JSONB-Wert keine Liste, setzt der Handler ihn still auf\n`[]`, und `messageCount` wird 0.\n\nNach der Verdichtung enthaelt `messages` nicht mehr den ganzen Verlauf:\ndie aelteren Zuege stecken dann in `summary`, und `summaryUntilTurn`\nsagt, bis wohin. `messageCount` zaehlt nur, was noch woertlich da ist.\n\nHINWEIS ZUM SPEICHER: `public.ai_conversations` wird von zwei Stellen\nim Haus beansprucht — von diesen Routen (eine Zeile je Gespraech, Zuege\nin `messages_jsonb`) und vom Gespraechsgedaechtnis des Chats (eine\nZeile je Zug, Spalten `role`/`content`/`embedding`). Nur die erste Form\nwird angelegt. Was der Chat schreibt, taucht hier nicht auf."},"delete":{"responses":{"200":{"description":"Ausgeblendet.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Kein sichtbares Gespraech mit dieser Kennung — nicht vorhanden, fremd oder schon ausgeblendet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client (ohne `retryAfter`) ODER die Abfrage ist gescheitert (mit `retryAfter`).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"deleteApiV1AiConversationsById","tags":["ai","conversations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Gespraech ausblenden (Soft-Delete — die Inhalte bleiben gespeichert)","description":"Setzt `deleted_at` auf jetzt. Die Zeile bleibt mit allen Zuegen in\n`public.ai_conversations` stehen; sie wird nur nicht mehr\nausgeliefert.\n\nDAS IST KEINE LOESCHUNG IM SINNE DER DSGVO. Wer Gespraechsinhalte\nwirklich entfernen muss, kommt ueber diesen Endpunkt nicht ans Ziel.\n\nEs gibt keinen Weg zurueck: die Leseabfragen filtern auf\n`deleted_at IS NULL`, und diese Router bieten kein Wiederherstellen an.\n\nEin zweiter Aufruf antwortet mit 404 — die Bedingung `deleted_at IS\nNULL` trifft dann keine Zeile mehr. Ebenso das Gespraech eines anderen\nNutzers: Ein 404 heisst „fuer dich nicht sichtbar\", nicht zwingend „existiert nicht\": die Abfrage filtert bereits nach Mandant."}},"/api/v1/ai/conversations/{id}/fork":{"post":{"responses":{"201":{"description":"Das neue, abgezweigte Gespraech mit den uebernommenen Zuegen.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"summaryUntilTurn":{"type":"number"},"forkedFrom":{"type":["string","null"]},"forkedAtTurn":{"type":["number","null"]},"totalTokens":{"type":"number"},"totalCostEur":{"type":"number"},"messageCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"},"tool":{"type":"string"},"ts":{"type":"string"}},"required":["role","content"]}}},"required":["id","tenantId","userId","title","summary","summaryUntilTurn","forkedFrom","forkedAtTurn","totalTokens","totalCostEur","messageCount","createdAt","updatedAt","messages"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","title":"string","summary":"string","summaryUntilTurn":0,"forkedFrom":"string","forkedAtTurn":0,"totalTokens":0,"totalCostEur":0,"messageCount":0,"createdAt":"string","updatedAt":"string","messages":[{"role":"user","content":"string","tool":"string","ts":"string"}]}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Kein Gespraech mit dieser Kennung fuer diesen Nutzer — oder bereits ausgeblendet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Das `INSERT` lief ohne Fehler, lieferte aber keine Zeile zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"fork_failed"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client (ohne `retryAfter`) ODER die Abfrage ist gescheitert (mit `retryAfter`).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"postApiV1AiConversationsByIdFork","tags":["ai","conversations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ein Gespraech an einem Zug abzweigen (legt ein zweites Gespraech an)","description":"Legt ein NEUES Gespraech an, das die Zuege des Ursprungs bis\n`at_turn` (ausschliesslich) uebernimmt. Der Ursprung bleibt\nunveraendert — dies ist eine Kopie, keine Verschiebung.\n\nDer Aufruf kostet KEIN Modell-Kontingent: die Zuege werden kopiert, kein\nSprachmodell befragt. Die Wirkung ist dauerhaft; umkehrbar nur als\nSoft-Delete des neuen Gespraechs\n(`DELETE /api/v1/ai/conversations/{id}`).\n\nEIN ZU GROSSES `at_turn` IST KEIN FEHLER. Der Wert wird auf die\nvorhandene Zahl der Zuege gekappt (`Math.min`); wer 999 schickt,\nbekommt eine vollstaendige Kopie und keinen 400. Der WIRKLICH benutzte\nSchnitt steht in `forkedAtTurn` der Antwort.\n\nDIE VERDICHTUNG WIRD NICHT MITGENOMMEN. Kopiert wird allein\n`messages_jsonb`; `summary` und `summaryUntilTurn` des Ursprungs bleiben\nzurueck. Ist der Ursprung bereits verdichtet, enthaelt er die aelteren\nZuege nur noch als Zusammenfassung — die Abzweigung erbt dann genau\ndiesen gekuerzten Stand, nicht den vollen Verlauf.\n\nOhne `title` heisst die Abzweigung „<Ursprungstitel> (Fork)\", oder\n„Fork\", wenn der Ursprung keinen Titel hat.\n\nZaehler beginnen bei null: `totalTokens` und `totalCostEur` des\nUrsprungs werden NICHT uebernommen.\n\nDie Abfrage filtert nach Mandant UND Nutzer. Ein 404 heisst „fuer dich nicht sichtbar\", nicht zwingend „existiert nicht\": die Abfrage filtert bereits nach Mandant.\nDas Gespraech eines Kollegen ist hier also 404, nicht 403 — auch fuer\neinen Administrator.\n\nUM DIE ERSTE ABFRAGE STEHT KEIN `try`/`catch`: scheitert das Lesen des\nUrsprungs, kommt der zentrale 500 aus `app.onError`, nicht 503. Erst das\nSchreiben der Abzweigung ist abgesichert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"at_turn":{"type":"integer","minimum":0},"title":{"type":"string","maxLength":200}},"required":["at_turn"]},"example":{"at_turn":0,"title":"string"}}}}}},"/api/v1/ai/conversations/{id}/append":{"post":{"responses":{"200":{"description":"Das Gespraech nach dem Anhaengen. `didSummarise` sagt, ob dabei verdichtet wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"summaryUntilTurn":{"type":"number"},"forkedFrom":{"type":["string","null"]},"forkedAtTurn":{"type":["number","null"]},"totalTokens":{"type":"number"},"totalCostEur":{"type":"number"},"messageCount":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"},"tool":{"type":"string"},"ts":{"type":"string"}},"required":["role","content"]}},"didSummarise":{"type":"boolean"}},"required":["id","tenantId","userId","title","summary","summaryUntilTurn","forkedFrom","forkedAtTurn","totalTokens","totalCostEur","messageCount","createdAt","updatedAt","messages","didSummarise"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","title":"string","summary":"string","summaryUntilTurn":0,"forkedFrom":"string","forkedAtTurn":0,"totalTokens":0,"totalCostEur":0,"messageCount":0,"createdAt":"string","updatedAt":"string","messages":[{"role":"user","content":"string","tool":"string","ts":"string"}],"didSummarise":true}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Kein Gespraech mit dieser Kennung fuer diesen Nutzer — oder bereits ausgeblendet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Das `UPDATE` lief ohne Fehler, lieferte aber keine Zeile zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"update_failed"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client (ohne `retryAfter`) ODER die Abfrage ist gescheitert (mit `retryAfter`).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"postApiV1AiConversationsByIdAppend","tags":["ai","conversations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen Zug anhaengen (kann eine kostenpflichtige Verdichtung ausloesen)","description":"Haengt einen Gespraechszug an und schreibt die Zaehler fort. Die\ngesamte Zugliste wird dabei neu geschrieben, nicht ergaenzt.\n\nDIESER AUFRUF KANN EIN MODELL-KONTINGENT KOSTEN — als einziger in\ndiesem Router. Ueberschreitet das Gespraech die Verdichtungsschwelle,\nfasst ein Sprachmodell die aelteren Zuege zu einer Zusammenfassung\nzusammen. Dieser Aufruf laeuft MIT Mandantenkennung, wird also erfasst\n(`public.ai_cost_events`) und faellt unter Guardrails und Budget. Ob es\npassiert ist, steht im Feld `didSummarise` der Antwort; die meisten\nAufrufe loesen nichts aus und kosten nichts.\n\nDIE VERDICHTUNG IST NICHT UMKEHRBAR. Sie ERSETZT die aelteren Zuege\ndurch eine einzelne System-Notiz — der Wortlaut ist danach weg, in\ndiesem Gespraech wie in jeder spaeteren Abzweigung. Wer den vollen\nVerlauf braucht, sichert ihn vorher ueber\n`GET /api/v1/ai/conversations/{id}`.\n\nIST KEIN SPRACHMODELL EINGERICHTET oder scheitert der Aufruf, wird\ntrotzdem verdichtet — dann mit einer mechanisch gekuerzten Stichpunkt-\nliste (die ersten 200 Zeichen je Zug) statt einer echten\nZusammenfassung. Auch das ersetzt die Zuege endgueltig, und\n`didSummarise` ist in beiden Faellen `true`. Die Antwort unterscheidet\ndie zwei Herkuenfte nicht.\n\n`tokensDelta` und `costEurDelta` werden ADDIERT, nicht gesetzt, und\nnicht geprueft: die Zahlen kommen vom Aufrufer und werden geglaubt.\nDiese Zaehler sind eine Eigenbuchhaltung des Gespraechs — sie haben\nnichts mit `public.ai_cost_events` oder dem Monatsbudget zu tun.\n\nDie Abfrage filtert nach Mandant UND Nutzer. Ein 404 heisst „fuer dich nicht sichtbar\", nicht zwingend „existiert nicht\": die Abfrage filtert bereits nach Mandant.\n\nUM DIE ERSTE ABFRAGE UND UM DIE VERDICHTUNG STEHT KEIN `try`/`catch`:\nscheitert das Lesen des Gespraechs, kommt der zentrale 500 aus\n`app.onError`, nicht 503. Erst das Schreiben ist abgesichert — und wenn\nes scheitert, ist die Verdichtung bereits gelaufen und bezahlt, ohne\ndass sie gespeichert wurde.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string","maxLength":64000},"tool":{"type":"string","maxLength":120},"ts":{"type":"string","format":"date-time"}},"required":["role","content"]},"tokensDelta":{"type":"integer","minimum":0,"default":0},"costEurDelta":{"type":"number","minimum":0,"default":0}},"required":["message"]},"example":{"message":{"role":"user","content":"string","tool":"string","ts":"2026-01-01T12:00:00.000Z"},"tokensDelta":0,"costEurDelta":0}}}}}},"/api/v1/ai/rag/collections":{"get":{"responses":{"200":{"description":"List of collections — snake_case rows, no `tenant_id`","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","description":"Vorbelegt mit \"active\""},"document_count":{"type":"integer"},"chunk_count":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","description","status","document_count","chunk_count","created_at","updated_at"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","name":"string","description":"string","status":"string","document_count":0,"chunk_count":0,"created_at":"string","updated_at":"string"}]}}}},"401":{"description":"No tenant in the request context"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiRagCollections","tags":["ai","rag-wizard"],"parameters":[],"summary":"List RAG collections for the current tenant","description":"Reads public.rag_collections for the calling tenant, most recently updated first, capped at 500 rows — there is no paging and no filter, so a tenant beyond that cap silently loses the tail. The counters `document_count` and `chunk_count` are stored columns maintained by the indexing worker, not counted at read time. Missing tables are created on first call, so an empty list is the normal answer for a fresh tenant."},"post":{"responses":{"201":{"description":"Created — the row itself, no envelope","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","description":"Vorbelegt mit \"active\""},"document_count":{"type":"integer"},"chunk_count":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"tenant_id":{"type":"string"}},"required":["id","name","description","status","document_count","chunk_count","created_at","updated_at","tenant_id"],"additionalProperties":false},"example":{"id":"string","name":"string","description":"string","status":"string","document_count":0,"chunk_count":0,"created_at":"string","updated_at":"string","tenant_id":"string"}}}},"400":{"description":"Body rejected by the validator"},"401":{"description":"No tenant in the request context"},"403":{"description":"Admin role required"},"500":{"description":"Insert failed — includes a duplicate name"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiRagCollections","tags":["ai","rag-wizard"],"parameters":[],"summary":"Create a new RAG collection","description":"Inserts one row into public.rag_collections and answers with it verbatim (`RETURNING *`, so including `tenant_id`). The name is unique per tenant: a second collection with the same name violates the constraint and surfaces as 500 `db_insert_failed`, not as a 409. Nothing is indexed here — the collection starts empty, with both counters at 0. Requires the admin role.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":1000}},"required":["name"]},"example":{"name":"string","description":"string"}}}}}},"/api/v1/ai/rag/collections/{id}":{"delete":{"responses":{"200":{"description":"Geloescht. `deleted` traegt die Kennung der Sammlung.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Die Rolle des Aufrufers reicht nicht aus. Der Koerper kommt aus `requireMinRole`, nicht aus dem Handler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Keine Sammlung mit dieser Kennung in diesem Mandanten — oder schon geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Eine der drei Loeschungen ist gescheitert. Der Zustand kann teilweise geraeumt sein.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"db_delete_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1AiRagCollectionsById","tags":["ai","rag-wizard"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"RAG-Sammlung mit allen Dokumenten und Abschnitten endgueltig loeschen","description":"Loescht in drei Schritten: erst die Abschnitte (`rag_chunks`), dann die\nDokumente (`rag_documents`), dann die Sammlung selbst\n(`rag_collections`). Alles endgueltig, alles auf den Mandanten\neingeschraenkt.\n\nDAS IST KEIN DATENBANK-CASCADE, SONDERN DREI EINZELNE ANWEISUNGEN OHNE\nTRANSAKTION. Scheitert eine der spaeteren, bleibt der Rest weg: der\nhaeufigste Ausgang ist eine leere Sammlung, deren Dokumente und\nAbschnitte schon verschwunden sind. Der Aufrufer bekommt dann 500 und\nsollte den Aufruf wiederholen — er ist gefahrlos wiederholbar.\n\nDie Reihenfolge hat noch eine Folge: der 404 kommt aus dem LETZTEN\nSchritt. Bei einer unbekannten Kennung sind die ersten beiden\nLoeschungen bereits gelaufen — sie treffen dann nichts, weil sie\nebenfalls nach Sammlung und Mandant filtern.\n\nEin zweiter Aufruf antwortet mit 404. Die hochgeladenen Dateien im\nObjektspeicher werden NICHT geloescht.\n\nVerlangt mindestens die Rolle `admin`."}},"/api/v1/ai/rag/upload":{"post":{"responses":{"201":{"description":"Document created — the row itself, no envelope","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"collection_id":{"type":"string"},"tenant_id":{"type":"string"},"file_name":{"type":"string"},"file_url":{"type":["string","null"],"description":"null, wenn kein Ablagedienst eingerichtet ist — die Datei liegt dann nirgends"},"mime_type":{"type":["string","null"]},"size_bytes":{"type":["string","null"],"description":"BIGINT — kommt als Zeichenkette, nicht als Zahl"},"uploaded_at":{"type":"string"},"last_indexed_at":{"type":["string","null"],"description":"null, bis ein Indizierungslauf durch ist"}},"required":["id","collection_id","tenant_id","file_name","file_url","mime_type","size_bytes","uploaded_at","last_indexed_at"],"additionalProperties":false},"example":{"id":"string","collection_id":"string","tenant_id":"string","file_name":"string","file_url":"string","mime_type":"string","size_bytes":"string","uploaded_at":"string","last_indexed_at":"string"}}}},"400":{"description":"Not multipart, or `collection_id` / `file` missing"},"401":{"description":"No tenant in the request context"},"403":{"description":"Admin role required"},"404":{"description":"Collection unknown or owned by another tenant"},"413":{"description":"File larger than 50 MB"},"500":{"description":"Insert failed"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiRagUpload","tags":["ai","rag-wizard"],"parameters":[],"summary":"Upload a file into a RAG collection","description":"Multipart with the fields `file` and `collection_id`. The collection must belong to the calling tenant (404 otherwise) and the file must stay under 50 MB (413). Stored is only the metadata row in public.rag_documents — is no storage service wired up, `file_url` stays null and the answer is still 201, so a successful upload does not prove the bytes were kept anywhere. Nothing is parsed, chunked or embedded here; that starts with POST /wizard/jobs."}},"/api/v1/ai/rag/wizard/jobs":{"post":{"responses":{"201":{"description":"Job queued — the row itself, no envelope","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"collection_id":{"type":"string"},"status":{"type":"string","description":"Beim Anlegen immer \"queued\""},"percent":{"type":"integer"},"file_count":{"type":"integer"},"processed_count":{"type":"integer"},"file_ids_jsonb":{"type":"array","items":{"type":"string"},"description":"Die uebergebenen Dateikennungen"},"error_message":{"type":["string","null"]},"started_at":{"type":["string","null"]},"completed_at":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","tenant_id","collection_id","status","percent","file_count","processed_count","file_ids_jsonb","error_message","started_at","completed_at","created_at"],"additionalProperties":false},"example":{"id":"string","tenant_id":"string","collection_id":"string","status":"string","percent":0,"file_count":0,"processed_count":0,"file_ids_jsonb":["string"],"error_message":"string","started_at":"string","completed_at":"string","created_at":"string"}}}},"400":{"description":"Body rejected by the validator"},"401":{"description":"No tenant in the request context"},"403":{"description":"Admin role required"},"500":{"description":"Insert failed"},"503":{"description":"Database unavailable"}},"operationId":"postApiV1AiRagWizardJobs","tags":["ai","rag-wizard"],"parameters":[],"summary":"Kick off an indexing job for selected files","description":"Writes one row into public.rag_jobs with status `queued` and returns it — the work itself is done later by the worker, so a 201 means \"accepted\", not \"indexed\". Between 1 and 100 file ids per call; `file_count` is simply their number. Neither the collection nor the file ids are checked against the database here, so a job can be queued for ids that do not exist. Progress is read back through GET /wizard/jobs/{id}. Requires the admin role.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"collection_id":{"type":"string","minLength":1},"file_ids":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":100}},"required":["collection_id","file_ids"]},"example":{"collection_id":"string","file_ids":["string"]}}}}}},"/api/v1/ai/rag/wizard/jobs/{id}":{"get":{"responses":{"200":{"description":"Der Auftragsstand. Drei zugesagte Zahlen, der Rest kommt roh aus der Tabelle.","content":{"application/json":{"schema":{"type":"object","properties":{"percent":{"type":"number"},"processed_count":{"type":"number"},"file_count":{"type":"number"}},"required":["percent","processed_count","file_count"],"additionalProperties":{}},"example":{"percent":0,"processed_count":0,"file_count":0}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Kein Auftrag mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AiRagWizardJobsById","tags":["ai","rag-wizard"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Stand eines Indizierungsauftrags (Zeile aus SELECT *, nur drei Felder zugesagt)","description":"Liefert den Stand eines mit `POST /api/v1/ai/rag/wizard/jobs`\nangelegten Auftrags. Der Hintergrund-Arbeiter holt `queued`-Auftraege\nalle 30 Sekunden ab und fuehrt sie ueber `parsing` nach `done` oder\n`failed`.\n\nDIE ANTWORT IST DIE ROHE DATENBANKZEILE. Der Handler liest sie mit\n`SELECT *` und streut sie unveraendert in den Koerper — in snake_case,\nmit allem, was die Tabelle gerade hat. Zugesagt sind hier nur die drei\nZahlen `percent`, `processed_count` und `file_count`, weil der Handler\nsie nach dem Streuen ausdruecklich setzt. Alle weiteren Felder sind\nnicht Teil des Vertrags: was heute mitkommt, kann eine spaetere\nAenderung an der Tabelle umbenennen oder entfernen, ohne dass sich hier\netwas aendert.\n\nDie drei Zahlen sind bewusst auf 0 gezwungen, wenn die Spalte NULL\nist — sonst zeigte die Oberflaeche `NaN%`. `percent: 0` heisst also\n„noch nichts\" ODER „Wert fehlt\", nicht zwingend „gerade begonnen\".\n\nUm die Abfrage steht KEIN `try`/`catch`: scheitert sie, kommt der\nzentrale 500 aus `app.onError`."}},"/api/v1/ai/rag/query":{"post":{"responses":{"200":{"description":"Top-K results","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"chunk":{"type":"string","description":"Der Abschnittstext, ungekuerzt"},"score":{"type":"number","description":"Kosinus-Aehnlichkeit, ungerundet"},"document_id":{"type":"string"},"document_name":{"type":"string"},"page_number":{"type":["integer","null"]},"stale":{"type":"boolean","description":"Quelle aelter als 365 Tage"}},"required":["chunk","score","document_id","document_name","page_number","stale"],"additionalProperties":false},"description":"Hoechstens `top_k` Treffer, bester zuerst"},"embedding":{"type":"object","properties":{"provider":{"type":"string"},"model":{"type":"string"},"dim":{"type":"integer","description":"Breite des Anfragevektors"}},"required":["provider","model","dim"],"additionalProperties":false},"searched":{"type":"integer","description":"Verglichene Abschnitte — NICHT die Zahl der Treffer"},"skipped":{"type":"integer"},"skipped_reasons":{"type":"object","properties":{"other_model":{"type":"integer","description":"Anderes Modell, also anderer Vektorraum"},"other_dimension":{"type":"integer","description":"Gleiches Modell, abweichende Vektorbreite"},"no_provenance":{"type":"integer","description":"Vor der Herkunftsspalte indiziert"},"unreadable":{"type":"integer","description":"Vektor nicht lesbar"}},"required":["other_model","other_dimension","no_provenance","unreadable"],"additionalProperties":false},"note":{"type":["string","null"],"description":"Klartexthinweis zu den uebersprungenen Abschnitten; null, wenn keiner uebersprungen wurde"}},"required":["results","embedding","searched","skipped","skipped_reasons","note"],"additionalProperties":false},"example":{"results":[{"chunk":"string","score":0,"document_id":"string","document_name":"string","page_number":0,"stale":true}],"embedding":{"provider":"string","model":"string","dim":0},"searched":0,"skipped":0,"skipped_reasons":{"other_model":0,"other_dimension":0,"no_provenance":0,"unreadable":0},"note":"string"}}}},"400":{"description":"Body rejected by the validator"},"401":{"description":"No tenant in the request context"},"503":{"description":"Database unavailable, or the query could not be embedded"}},"operationId":"postApiV1AiRagQuery","tags":["ai","rag-wizard"],"parameters":[],"summary":"Retrieve top-K chunks by cosine similarity for a query","description":"Embeds the query and compares it against the stored chunks of ONE collection, in memory (up to 5000 chunks, no vector index). Only chunks from the SAME embedding model and vector width take part; every other chunk is skipped and counted in `skipped_reasons`, so `searched` can be far smaller than the collection. If the embedding call fails, the endpoint answers 503 rather than falling back to a placeholder vector — a hash vector would return a fully populated, plausible-looking hit list unrelated to the question. Read-only: nothing is written.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"collection_id":{"type":"string","minLength":1},"query":{"type":"string","minLength":1,"maxLength":2000},"top_k":{"type":"integer","minimum":1,"maximum":50,"default":5}},"required":["collection_id","query"]},"example":{"collection_id":"string","query":"string","top_k":1}}}}}},"/api/v1/ai/rag/cite":{"post":{"responses":{"200":{"description":"Citations. Same read path as /query, but the snippet is cut at 240 characters and the score is rounded to two decimals. `stale: true` marks a source older than 365 days.","content":{"application/json":{"schema":{"type":"object","properties":{"citations":{"type":"array","items":{"type":"object","properties":{"doc_name":{"type":"string"},"document_id":{"type":"string"},"page":{"type":["integer","null"]},"snippet":{"type":"string","description":"Hoechstens 240 Zeichen, danach abgeschnitten"},"confidence_score":{"type":"number","description":"Kosinus-Aehnlichkeit, auf zwei Stellen gerundet"},"stale":{"type":"boolean","description":"Quelle aelter als 365 Tage"}},"required":["doc_name","document_id","page","snippet","confidence_score","stale"],"additionalProperties":false},"description":"Hoechstens `top_k` Belege, bester zuerst"},"embedding":{"type":"object","properties":{"provider":{"type":"string"},"model":{"type":"string"},"dim":{"type":"integer","description":"Breite des Anfragevektors"}},"required":["provider","model","dim"],"additionalProperties":false},"searched":{"type":"integer","description":"Verglichene Abschnitte — NICHT die Zahl der Belege"},"skipped":{"type":"integer"},"skipped_reasons":{"type":"object","properties":{"other_model":{"type":"integer","description":"Anderes Modell, also anderer Vektorraum"},"other_dimension":{"type":"integer","description":"Gleiches Modell, abweichende Vektorbreite"},"no_provenance":{"type":"integer","description":"Vor der Herkunftsspalte indiziert"},"unreadable":{"type":"integer","description":"Vektor nicht lesbar"}},"required":["other_model","other_dimension","no_provenance","unreadable"],"additionalProperties":false},"note":{"type":["string","null"]}},"required":["citations","embedding","searched","skipped","skipped_reasons","note"],"additionalProperties":false},"example":{"citations":[{"doc_name":"string","document_id":"string","page":0,"snippet":"string","confidence_score":0,"stale":true}],"embedding":{"provider":"string","model":"string","dim":0},"searched":0,"skipped":0,"skipped_reasons":{"other_model":0,"other_dimension":0,"no_provenance":0,"unreadable":0},"note":"string"}}}},"400":{"description":"Body rejected by the validator"},"401":{"description":"No tenant in the request context"},"503":{"description":"Database unavailable, or the query could not be embedded"}},"operationId":"postApiV1AiRagCite","tags":["ai","rag-wizard"],"parameters":[],"description":"Citations + stale-flags for an AI response. KEIN Aufrufer in apps/ (Stand 12.08.2026) — bis 12.08.2026 stand hier \"used by ai-chat\", was nicht stimmt: der KI-Chat zitiert ueber routes/ai-citations.ts. Wer diesen Endpunkt verdrahtet, aendert bitte auch diesen Satz.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","minLength":1,"maxLength":2000},"response_text":{"type":"string","maxLength":20000},"collection_id":{"type":"string"},"top_k":{"type":"integer","minimum":1,"maximum":20,"default":3}},"required":["query"]},"example":{"query":"string","response_text":"string","collection_id":"string","top_k":1}}}},"summary":"Citations + stale-flags for an AI response","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/citations/{citationId}":{"get":{"responses":{"200":{"description":"Citation detail","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The citation id as given, \"entityType:entityId\""},"entityType":{"type":"string","description":"customer | invoice | document | immo | property"},"entityId":{"type":"string"},"title":{"type":"string","description":"Name, invoice number or file name — falls back to a generic German word"},"url":{"type":"string","description":"Dashboard path to the record; set for every type this endpoint resolves"},"fields":{"type":"object","additionalProperties":{},"description":"The selected columns verbatim, snake_case — the set differs per entity type"}},"required":["id","entityType","entityId","title","fields"]},"example":{"id":"string","entityType":"string","entityId":"string","title":"string","url":"string","fields":{}}}}},"400":{"description":"Malformed citation ID or unusable tenant slug"},"401":{"description":"Unauthorized"},"404":{"description":"Entity not found / unsupported type"},"500":{"description":"Lookup failed"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1AiCitationsByCitationId","tags":["ai","citations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"citationId","required":true}],"summary":"Resolve a citation ID to its full underlying record","description":"Splits the id at the first colon into entity type and entity id and loads the record behind it, so the citation modal can show more than the indexed snippet. Only `customer`, `invoice`, `document`, `immo` and `property` are projected — every other type answers 404 rather than guessing at a table, which makes an unsupported type indistinguishable from a deleted record. Customers, invoices and properties come from the caller's tenant schema, documents from the shared `public.rag_documents` with an explicit tenant filter. Read-only: nothing is written and no model is called."}},"/api/v1/ai/agent/plans":{"get":{"responses":{"200":{"description":"Die sichtbaren Plaene. `degraded: true` heisst: die Liste konnte nicht gelesen werden und sagt nichts ueber den Bestand.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"prompt":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"status":{"type":"string","enum":["planning","awaiting_confirm","executing","paused","completed","failed","rolled_back","cancelled"]},"currentStep":{"type":"number"},"totalSteps":{"type":"number"},"costEstimateEur":{"type":"number"},"costActualEur":{"type":"number"},"tokensUsed":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cacheRead":{"type":"number"}},"required":["input","output","cacheRead"],"additionalProperties":false},"rollbackStack":{"type":"array","items":{"type":"object","properties":{"stepIndex":{"type":"number"},"revertPayload":{}},"required":["stepIndex"],"additionalProperties":false},"default":[]},"errorMessage":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"completedAt":{"type":"string"},"authorRole":{"type":"string"},"authorPermissions":{"type":"array","items":{"type":"string"}},"authorTenantPlan":{"type":"string"},"readOnly":{"type":"boolean"}},"required":["id","tenantId","userId","prompt","steps","status","currentStep","totalSteps","costEstimateEur","costActualEur","tokensUsed","rollbackStack","createdAt","updatedAt"],"additionalProperties":false}},"degraded":{"type":"boolean","const":true}},"required":["items"],"additionalProperties":false},"example":{"items":[{"id":"string","tenantId":"string","userId":"string","prompt":"string","steps":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"status":"planning","currentStep":0,"totalSteps":0,"costEstimateEur":0,"costActualEur":0,"tokensUsed":{"input":0,"output":0,"cacheRead":0},"rollbackStack":[{"stepIndex":0}],"errorMessage":"string","createdAt":"string","updatedAt":"string","completedAt":"string","authorRole":"string","authorPermissions":["string"],"authorTenantPlan":"string","readOnly":true}],"degraded":true}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1AiAgentPlans","tags":["ai","agent-plans"],"parameters":[{"in":"query","name":"status","schema":{"type":"string","maxLength":120}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0}}],"description":"Listet Agenten-Plaene des Mandanten. WER WAS SIEHT haengt an der Rolle: ein Mandanten-Administrator sieht ALLE Plaene des Mandanten, jeder andere nur die eigenen — dafuer gibt es keinen Parameter, das entscheidet der Server. `status` grenzt auf einen oder mehrere Zustaende ein, `limit` (1..500) und `offset` blaettern; eine Gesamtzahl kommt NICHT mit. Jeder Plan traegt seine Schritte samt Kostenschaetzung vollstaendig mit. Ist der Speicher nicht erreichbar, antwortet der Aufruf trotzdem 200 mit leerer Liste und `degraded: true` — diese Kennzeichnung ist der einzige Unterschied zu „es gibt keine Plaene\".","summary":"Listet Agenten-Plaene des Mandanten","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Der angelegte Plan im Zustand `awaiting_confirm`, dazu die Beanstandungen des Planers.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"prompt":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"status":{"type":"string","enum":["planning","awaiting_confirm","executing","paused","completed","failed","rolled_back","cancelled"]},"currentStep":{"type":"number"},"totalSteps":{"type":"number"},"costEstimateEur":{"type":"number"},"costActualEur":{"type":"number"},"tokensUsed":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cacheRead":{"type":"number"}},"required":["input","output","cacheRead"],"additionalProperties":false},"rollbackStack":{"type":"array","items":{"type":"object","properties":{"stepIndex":{"type":"number"},"revertPayload":{}},"required":["stepIndex"],"additionalProperties":false},"default":[]},"errorMessage":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"completedAt":{"type":"string"},"authorRole":{"type":"string"},"authorPermissions":{"type":"array","items":{"type":"string"}},"authorTenantPlan":{"type":"string"},"readOnly":{"type":"boolean"},"warnings":{"type":"array","items":{"type":"string"}}},"required":["id","tenantId","userId","prompt","steps","status","currentStep","totalSteps","costEstimateEur","costActualEur","tokensUsed","rollbackStack","createdAt","updatedAt","warnings"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","prompt":"string","steps":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"status":"planning","currentStep":0,"totalSteps":0,"costEstimateEur":0,"costActualEur":0,"tokensUsed":{"input":0,"output":0,"cacheRead":0},"rollbackStack":[{"stepIndex":0}],"errorMessage":"string","createdAt":"string","updatedAt":"string","completedAt":"string","authorRole":"string","authorPermissions":["string"],"authorTenantPlan":"string","readOnly":true,"warnings":["string"]}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Der Modellaufruf, die Pruefung des Vorschlags oder das Anlegen ist gescheitert — auch, wenn das Modell gar keine Schritte lieferte. `message` traegt den rohen Fehlertext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"plan_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiAgentPlans","tags":["ai","agent-plans"],"parameters":[],"summary":"Plan aus einem Auftrag erzeugen (echter Modellaufruf, nicht erfasst)","description":"Laesst ein Sprachmodell aus `prompt` eine Folge von Schritten\nvorschlagen, prueft sie gegen den Werkzeugkatalog und legt das Ergebnis\nals Plan in `public.ai_agent_plans` an — im Zustand `awaiting_confirm`.\n\nDER PLANUNGSAUFRUF IST EIN ECHTER MODELLAUFRUF und kostet beim Anbieter\nGeld. Er wird aber NICHT erfasst: der verdrahtete Planer ruft `callLLM`\nohne Mandantenkennung auf, und Kostenerfassung wie Guardrails greifen\nnur mit ihr. Folgen: keine Zeile in `public.ai_cost_events`, kein\nAnstieg des Monatszaehlers, keine Maskierung des Auftragstextes. Der\nVerbrauch der PLANUNG ist damit in\n`GET /api/v1/tenant/ai/costs` unsichtbar.\n\nAUSGEFUEHRT WIRD NOCH NICHTS. Der Plan ist ein Vorschlag; erst\n`POST /api/v1/ai/agent/plans/{id}/confirm` gibt ihn frei, und erst der\nAusfuehrungs-Arbeiter arbeitet die Schritte gegen echte Daten ab. Bis\ndahin ist der einzige bleibende Effekt die Planzeile — stilllegbar ueber\n`POST /api/v1/ai/agent/plans/{id}/cancel`.\n\nOHNE VERDRAHTETEN PLANER KOMMT TROTZDEM 201. Ist kein Reasoner\ninstalliert, erzeugt ein Rueckfall einen Plan aus EINEM\n`reflection`-Schritt, dessen `rationale` woertlich „No host reasoner is\nwired\" nebst dem gekuerzten Auftragstext enthaelt. Das ist kein Plan\nund kein Fehler — nur daran zu erkennen.\n\n`readOnly: true` laesst den Planer nur lesende Werkzeuge sehen, sodass\ner schreibende Schritte gar nicht erst vorschlagen kann; der Arbeiter\nverweigert sie zusaetzlich. Standard ist `false`.\n\nDIE RECHTE DES ERSTELLERS WERDEN EINGEFROREN. Rolle und Tarif des\nAnfragenden werden auf dem Plan festgehalten und begrenzen spaeter, was\nder Agent tun darf — ein Plan kann nie mehr als sein Ersteller. Wer die\nRolle des Erstellers spaeter herabstuft, aendert damit NICHT, was ein\nbereits angelegter Plan darf.\n\n`warnings` gibt es NUR in dieser Antwort und wird nicht gespeichert: es\nnennt, was der Planer am Vorschlag des Modells beanstandet hat — etwa\nSchritte mit unbekanntem Werkzeug, die zu `reflection` herabgestuft\nwurden. Ein spaeteres `GET /api/v1/ai/agent/plans/{id}` zeigt sie nicht\nmehr. Wer sie verwirft, verliert den einzigen Hinweis darauf, dass der\nPlan nicht das enthaelt, was das Modell vorschlug.\n\n`costEstimateEur` ist eine Schaetzung fuer die spaetere AUSFUEHRUNG,\nnicht der Preis dieses Aufrufs. `model` im Rumpf steuert allein das\nPreismodell dieser Schaetzung — welches Modell wirklich plant, entscheidet\n`callLLM`.\n\n`tokensUsed` traegt den Verbrauch der PLANUNG, nicht der Ausfuehrung.\n\nDer Eintrag im Aktivitaetsprotokoll laeuft nebenlaeufig (`void`):\nscheitert er, bleibt der Plan bestehen und die Antwort 201.\n\nKeine Rollenpruefung auf dieser Operation: jeder angemeldete Benutzer des\nMandanten darf planen lassen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":8000},"model":{"type":"string","maxLength":120},"readOnly":{"type":"boolean"}},"required":["prompt"]},"example":{"prompt":"string","model":"string","readOnly":true}}}}}},"/api/v1/ai/agent/plans/{id}":{"get":{"responses":{"200":{"description":"Der Plan mit allen Schritten.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"prompt":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"status":{"type":"string","enum":["planning","awaiting_confirm","executing","paused","completed","failed","rolled_back","cancelled"]},"currentStep":{"type":"number"},"totalSteps":{"type":"number"},"costEstimateEur":{"type":"number"},"costActualEur":{"type":"number"},"tokensUsed":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cacheRead":{"type":"number"}},"required":["input","output","cacheRead"],"additionalProperties":false},"rollbackStack":{"type":"array","items":{"type":"object","properties":{"stepIndex":{"type":"number"},"revertPayload":{}},"required":["stepIndex"],"additionalProperties":false},"default":[]},"errorMessage":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"completedAt":{"type":"string"},"authorRole":{"type":"string"},"authorPermissions":{"type":"array","items":{"type":"string"}},"authorTenantPlan":{"type":"string"},"readOnly":{"type":"boolean"}},"required":["id","tenantId","userId","prompt","steps","status","currentStep","totalSteps","costEstimateEur","costActualEur","tokensUsed","rollbackStack","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","prompt":"string","steps":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"status":"planning","currentStep":0,"totalSteps":0,"costEstimateEur":0,"costActualEur":0,"tokensUsed":{"input":0,"output":0,"cacheRead":0},"rollbackStack":[{"stepIndex":0}],"errorMessage":"string","createdAt":"string","updatedAt":"string","completedAt":"string","authorRole":"string","authorPermissions":["string"],"authorTenantPlan":"string","readOnly":true}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Fremder Plan im selben Mandanten, und der Aufrufer ist kein Administrator.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Kein Plan mit dieser Kennung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Die Abfrage ist gescheitert. `message` traegt den rohen Treibertext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"db_error"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AiAgentPlansById","tags":["ai","agent-plans"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen Agenten-Plan lesen (verschluckt /plans/all)","description":"Liefert einen Plan samt aller Schritte aus `public.ai_agent_plans`.\n\nSichtbar ist er fuer seinen Ersteller; `admin`, `manager` und\n`tenant_admin` sehen jeden Plan ihres Mandanten. Ein Plan aus einem\nanderen Mandanten ist 404. Ein 404 heisst „fuer dich nicht sichtbar\", nicht zwingend „existiert nicht\": die Abfrage filtert bereits nach Mandant.\n\nDIESER PFAD IST BREITER, ALS ER AUSSIEHT. `{id}` steht in der\nAnmeldereihenfolge vor der Telemetrie-Route `GET /ai/agent/plans/all`,\nund Honos Matcher nimmt die zuerst angemeldete Regel. `/plans/all`\nlandet deshalb HIER, mit `id = \"all\"` — und antwortet 404, weil es\nkeinen Plan mit dieser Kennung gibt. Die mandantenweite Liste ist\nueber diesen Weg nicht erreichbar, obwohl sie dokumentiert ist.\n\n`rollbackStack` fuellt der Ausfuehrungs-Arbeiter. Er wird von KEINEM\nEndpunkt dieser API abgearbeitet — siehe\n`POST /api/v1/ai/agent/plans/{id}/cancel`."}},"/api/v1/ai/agent/plans/{id}/confirm":{"post":{"responses":{"200":{"description":"Der Plan im neuen Zustand `executing` — oder `null`, wenn die Zeile verschwunden ist.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"prompt":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"status":{"type":"string","enum":["planning","awaiting_confirm","executing","paused","completed","failed","rolled_back","cancelled"]},"currentStep":{"type":"number"},"totalSteps":{"type":"number"},"costEstimateEur":{"type":"number"},"costActualEur":{"type":"number"},"tokensUsed":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cacheRead":{"type":"number"}},"required":["input","output","cacheRead"],"additionalProperties":false},"rollbackStack":{"type":"array","items":{"type":"object","properties":{"stepIndex":{"type":"number"},"revertPayload":{}},"required":["stepIndex"],"additionalProperties":false},"default":[]},"errorMessage":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"completedAt":{"type":"string"},"authorRole":{"type":"string"},"authorPermissions":{"type":"array","items":{"type":"string"}},"authorTenantPlan":{"type":"string"},"readOnly":{"type":"boolean"}},"required":["id","tenantId","userId","prompt","steps","status","currentStep","totalSteps","costEstimateEur","costActualEur","tokensUsed","rollbackStack","createdAt","updatedAt"],"additionalProperties":false},{"type":"null"}]},"example":{"id":"string","tenantId":"string","userId":"string","prompt":"string","steps":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"status":"planning","currentStep":0,"totalSteps":0,"costEstimateEur":0,"costActualEur":0,"tokensUsed":{"input":0,"output":0,"cacheRead":0},"rollbackStack":[{"stepIndex":0}],"errorMessage":"string","createdAt":"string","updatedAt":"string","completedAt":"string","authorRole":"string","authorPermissions":["string"],"authorTenantPlan":"string","readOnly":true}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Fremder Plan, und der Aufrufer ist kein Administrator.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Kein Plan mit dieser Kennung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Der Plan steht nicht auf `awaiting_confirm`. `message` nennt den tatsaechlichen Zustand.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_status"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"Das Schreiben ist gescheitert. `message` traegt den rohen Treibertext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"confirm_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiAgentPlansByIdConfirm","tags":["ai","agent-plans"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Plan freigeben (setzt nur den Zustand — hier laeuft noch nichts)","description":"Setzt den Plan von `awaiting_confirm` auf `executing` und markiert die\nNICHT zerstoerenden Schritte als vom Menschen freigegeben.\n\nIN DIESER ANFRAGE WIRD KEIN SCHRITT AUSGEFUEHRT. Der Aufruf kehrt\nsofort zurueck; gearbeitet wird vom Hintergrund-Arbeiter, der\n`executing`-Plaene abholt. Wer den Fortschritt braucht, liest\n`GET /api/v1/ai/agent/plans/{id}` oder haengt sich an den\nEreignisstrom.\n\nZERSTOERENDE SCHRITTE BLEIBEN ABSICHTLICH UNBESTAETIGT. Diese Freigabe\ngilt nur fuer die uebrigen. Jeder Schritt mit `destructive: true` haelt\nden Plan spaeter erneut an und wird einzeln freigegeben. Eine\nZustimmung hier ist also KEINE Zustimmung zu allem, was im Plan steht.\n\nNur aus `awaiting_confirm` heraus moeglich; jeder andere Zustand\nantwortet mit 409 — auch ein zweiter Aufruf derselben Freigabe.\n\nDer Koerper kann `null` sein: `store.update` liefert `null`, wenn die\nZeile zwischen Lesen und Schreiben verschwunden ist, und der Handler\nreicht das mit Status 200 durch."}},"/api/v1/ai/agent/plans/{id}/cancel":{"post":{"responses":{"200":{"description":"Der Plan im Endzustand `cancelled` oder `rolled_back` — oder `null`, wenn die Zeile verschwunden ist.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"prompt":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"status":{"type":"string","enum":["planning","awaiting_confirm","executing","paused","completed","failed","rolled_back","cancelled"]},"currentStep":{"type":"number"},"totalSteps":{"type":"number"},"costEstimateEur":{"type":"number"},"costActualEur":{"type":"number"},"tokensUsed":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cacheRead":{"type":"number"}},"required":["input","output","cacheRead"],"additionalProperties":false},"rollbackStack":{"type":"array","items":{"type":"object","properties":{"stepIndex":{"type":"number"},"revertPayload":{}},"required":["stepIndex"],"additionalProperties":false},"default":[]},"errorMessage":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"completedAt":{"type":"string"},"authorRole":{"type":"string"},"authorPermissions":{"type":"array","items":{"type":"string"}},"authorTenantPlan":{"type":"string"},"readOnly":{"type":"boolean"}},"required":["id","tenantId","userId","prompt","steps","status","currentStep","totalSteps","costEstimateEur","costActualEur","tokensUsed","rollbackStack","createdAt","updatedAt"],"additionalProperties":false},{"type":"null"}]},"example":{"id":"string","tenantId":"string","userId":"string","prompt":"string","steps":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"status":"planning","currentStep":0,"totalSteps":0,"costEstimateEur":0,"costActualEur":0,"tokensUsed":{"input":0,"output":0,"cacheRead":0},"rollbackStack":[{"stepIndex":0}],"errorMessage":"string","createdAt":"string","updatedAt":"string","completedAt":"string","authorRole":"string","authorPermissions":["string"],"authorTenantPlan":"string","readOnly":true}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Fremder Plan, und der Aufrufer ist kein Administrator.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Kein Plan mit dieser Kennung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Der Plan steht bereits in einem Endzustand. `message` nennt ihn.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_status"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"Das Schreiben ist gescheitert. `message` traegt den rohen Treibertext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"cancel_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiAgentPlansByIdCancel","tags":["ai","agent-plans"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Plan abbrechen (setzt nur den Zustand — es wird NICHTS rueckabgewickelt)","description":"Beendet den Plan. Der Endzustand haengt daran, ob bereits\nRueckabwicklungs-Eintraege aufgelaufen sind:\n\n  · `rollbackStack` leer      -> Zustand `cancelled`\n  · `rollbackStack` gefuellt  -> Zustand `rolled_back`\n\nDER ZWEITE FALL IST EINE ZUSAGE, DIE NIEMAND EINLOEST. `rolled_back`\nliest sich wie „die erledigten Schritte wurden rueckgaengig gemacht\".\nDas passiert nicht: dieser Aufruf setzt Zustand, Fehlertext und\nAbschlusszeit — mehr nicht. Die Umkehrfunktionen aus dem\nRueckabwicklungs-Stapel werden in dieser API von keiner Stelle\nausgefuehrt, und der Hintergrund-Arbeiter holt ausschliesslich Plaene\nim Zustand `executing`; `rolled_back` ist fuer ihn ein Endzustand.\n\nWer nach einem Abbruch mit gefuelltem Stapel weiterarbeitet, muss\ndavon ausgehen, dass die bereits ausgefuehrten Schritte WIRKSAM\nGEBLIEBEN sind — angelegte Datensaetze, verschickte Belege,\nveraenderte Bestaende. `rollbackStack` in\n`GET /api/v1/ai/agent/plans/{id}` zeigt, was offen geblieben ist.\n\n`errorMessage` traegt danach „Cancelled by <Nutzerkennung> at <Zeit>\" —\nein Abbruch, kein Fehler, obwohl das Feld so heisst.\n\nAus einem Endzustand heraus (`completed`, `rolled_back`, `cancelled`)\nkommt 409. `executing` und `paused` lassen sich abbrechen — ein Schritt,\nder gerade laeuft, wird dadurch NICHT gestoppt."}},"/api/v1/ai/agent/plans/{planId}/stream":{"get":{"responses":{"200":{"description":"OK — SSE stream (text/event-stream), kein JSON-Dokument","content":{"text/event-stream":{"schema":{"type":"string"}}}},"400":{"description":"planId fehlt"},"401":{"description":"Unauthorized"},"404":{"description":"Plan not found — oder er gehoert einem anderen Mandanten"},"503":{"description":"Mandantenzugehoerigkeit nicht pruefbar — der Strom wird verweigert"}},"operationId":"getApiV1AiAgentPlansByPlanIdStream","tags":["ai-agent"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"planId","required":true}],"summary":"Server-Sent-Events stream for AI agent-plan execution","description":"Emits step lifecycle events plus heartbeats. Der Rumpf ist KEIN JSON-Objekt, sondern ein laufender Ereignisstrom: je Ereignis eine Zeile `data: <json>`, gefolgt von einer Leerzeile. Die Ereignisse tragen `type` — plan_started, step_started, step_completed, step_failed, step_awaiting_human, cost_update, done, heartbeat — dazu `planId` und `timestamp`. Alle 30 Sekunden geht ein Herzschlag raus, damit die Verbindung nicht am Rand der Zustellkette abgeraeumt wird; nach 30 Minuten oder mit einem `done`-Ereignis schlieszt der Strom von selbst. REIN LESEND: der Aufruf startet, pausiert und bricht nichts ab — er hoert nur zu, und wer sich spaeter verbindet, sieht KEINE zurueckliegenden Ereignisse. Der Plan muss dem eigenen Mandanten gehoeren; laesst sich das nicht pruefen, wird der Strom mit 503 verweigert statt ungeprueft geoeffnet."}},"/api/v1/ai/agent/plans/{planId}/confirm-step":{"post":{"responses":{"200":{"description":"Step rejected (plan stays paused) or approved (plan re-enqueued)","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"status":{"type":"string","const":"paused"},"message":{"type":"string","description":"Plain-text hint that the plan can still be resumed"}},"required":["ok","status","message"]},{"type":"object","properties":{"ok":{"type":"boolean","const":true},"status":{"type":"string","const":"executing"},"jobId":{"type":"string","description":"Job id of the re-enqueued execution"},"mode":{"type":"string","enum":["queued","inline"],"description":"queued = handed to BullMQ, inline = executed synchronously in this process"},"resumedFromStep":{"type":"integer","minimum":0,"description":"Index of the step execution resumes from"}},"required":["ok","status","jobId","mode","resumedFromStep"]}]},"example":{"ok":true,"status":"paused","message":"string"}}}},"400":{"description":"Bad request — planId missing or no pending step"},"401":{"description":"Unauthorized"},"404":{"description":"Plan not found (also when it belongs to another tenant)"},"409":{"description":"Plan not in resumable state","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"plan_not_resumable"},"current_status":{"type":"string","description":"The status the plan is actually in"}},"required":["error","current_status"]}}}},"503":{"description":"Plan store unavailable"}},"operationId":"postApiV1AiAgentPlansByPlanIdConfirm-step","tags":["ai-agent"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"planId","required":true}],"description":"User confirms or rejects a paused agent-plan step. On approve, the step gate is cleared (optionally with a replaced input), the plan flips to `executing` and is re-enqueued from that step; on reject nothing is written and the plan stays `paused`. Without an explicit `stepIndex` the first step in `awaiting_human` is used. Plans of another tenant answer 404, not 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"approved":{"type":"boolean"},"modifiedInput":{},"stepIndex":{"type":"integer","minimum":0}},"required":["approved"]},"example":{"approved":true,"stepIndex":0}}}},"summary":"User confirms or rejects a paused agent-plan step","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/agent/plans/{planId}/override-step":{"post":{"responses":{"200":{"description":"OK — override applied, worker re-enqueued.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"status":{"type":"string","const":"executing"},"jobId":{"type":"string"},"mode":{"type":"string","enum":["queued","inline"]},"resumedFromStep":{"type":"number"},"action":{"type":"string","enum":["skip","set_result","retry"]}},"required":["ok","status","jobId","mode","resumedFromStep","action"],"additionalProperties":false},"example":{"ok":true,"status":"executing","jobId":"string","mode":"queued","resumedFromStep":0,"action":"skip"}}}},"400":{"description":"Bad request — invalid body or step state."},"401":{"description":"Unauthenticated."},"403":{"description":"Forbidden — not admin and not plan owner."},"404":{"description":"Plan or step not found."},"409":{"description":"Plan is not in an overridable state (must be paused/failed)."},"503":{"description":"Plan store unavailable."}},"operationId":"postApiV1AiAgentPlansByPlanIdOverride-step","tags":["ai-agent"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"planId","required":true}],"summary":"Manually overrides a stuck plan step: skip, set result or retry","description":"Tenant-admin or plan-owner manually overrides a stuck step (skip / set_result / retry). Every override is written to the tenant audit-log with the provided reason.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"stepIndex":{"type":"integer","minimum":0},"action":{"type":"string","enum":["skip","set_result","retry"]},"setResult":{},"modifiedInput":{},"reason":{"type":"string","minLength":1,"maxLength":1024}},"required":["stepIndex","action","reason"]},"example":{"stepIndex":0,"action":"skip","reason":"string"}}}}}},"/api/v1/ai/agent/auto-approve":{"get":{"responses":{"200":{"description":"The whitelist. A tenant without a row — and a read that failed — both answer 200 with the empty default (nothing whitelisted, `updatedAt` null), so the agent falls back to pausing on every confirmation step rather than erroring.","content":{"application/json":{"schema":{"type":"object","properties":{"whitelistToolNames":{"type":"array","items":{"type":"string"},"description":"Tool names that skip the confirmation step; empty means every step still pauses"},"whitelistPatterns":{"type":"array","items":{"type":"string"},"description":"Patterns matched against tool names, alongside the exact names above"},"allowNonDestructiveDefault":{"type":"boolean","description":"Whether non-destructive steps run without confirmation even when not whitelisted"},"updatedAt":{"type":["string","null"],"description":"When the whitelist was last written; null while the tenant has no row"},"updatedByUserId":{"type":["string","null"],"description":"Who last wrote it; null while the tenant has no row"}},"required":["whitelistToolNames","whitelistPatterns","allowNonDestructiveDefault","updatedAt","updatedByUserId"]},"example":{"whitelistToolNames":["string"],"whitelistPatterns":["string"],"allowNonDestructiveDefault":true,"updatedAt":"string","updatedByUserId":"string"}}}},"401":{"description":"No tenant in the auth context","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthenticated"}},"required":["error"]}}}}},"operationId":"getApiV1AiAgentAuto-approve","tags":["ai-agent"],"parameters":[],"description":"Fetch the auto-approve whitelist for the calling tenant. Returns an empty default if none has been configured.","summary":"Fetch the auto-approve whitelist for the calling tenant","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"The stored whitelist — or the submitted one echoed back if the write failed","content":{"application/json":{"schema":{"type":"object","properties":{"whitelistToolNames":{"type":"array","items":{"type":"string"},"description":"Tool names that skip the confirmation step; empty means every step still pauses"},"whitelistPatterns":{"type":"array","items":{"type":"string"},"description":"Patterns matched against tool names, alongside the exact names above"},"allowNonDestructiveDefault":{"type":"boolean","description":"Whether non-destructive steps run without confirmation even when not whitelisted"},"updatedAt":{"type":["string","null"],"description":"When the whitelist was last written; null while the tenant has no row"},"updatedByUserId":{"type":["string","null"],"description":"Who last wrote it; null while the tenant has no row"}},"required":["whitelistToolNames","whitelistPatterns","allowNonDestructiveDefault","updatedAt","updatedByUserId"]},"example":{"whitelistToolNames":["string"],"whitelistPatterns":["string"],"allowNonDestructiveDefault":true,"updatedAt":"string","updatedByUserId":"string"}}}},"400":{"description":"Bad request"},"401":{"description":"No tenant or user in the auth context","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthenticated"}},"required":["error"]}}}},"403":{"description":"Forbidden — admin required"}},"operationId":"putApiV1AiAgentAuto-approve","tags":["ai-agent"],"parameters":[],"description":"Update the auto-approve whitelist for the calling tenant. Admin only. This REPLACES the stored lists — all three fields are required, and omitting an entry removes it; there is no partial patch. At most 500 tool names and 200 patterns, each up to 120 characters. The whitelist is stored per tenant, and `updatedAt` / `updatedByUserId` are stamped from the caller. Whether the named tools actually exist is NOT checked. When the write itself fails the route still answers 200 and echoes the submitted values back — so a 200 is not proof that the whitelist was persisted.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"whitelistToolNames":{"type":"array","items":{"type":"string","minLength":1,"maxLength":120},"maxItems":500},"whitelistPatterns":{"type":"array","items":{"type":"string","minLength":1,"maxLength":120},"maxItems":200},"allowNonDestructiveDefault":{"type":"boolean"}},"required":["whitelistToolNames","whitelistPatterns","allowNonDestructiveDefault"]},"example":{"whitelistToolNames":["string"],"whitelistPatterns":["string"],"allowNonDestructiveDefault":true}}}},"summary":"Update the auto-approve whitelist for the calling tenant","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/agent/templates":{"post":{"responses":{"201":{"description":"Die angelegte Vorlage — vollstaendig, ohne Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"createdByUserId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"stepsTemplate":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"variablesSchema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["string","number","date","boolean"]},"default":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"description":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","type"],"additionalProperties":false}},"isShared":{"type":"boolean"},"sourcePlanId":{"type":"string"},"usageCount":{"type":"number"},"lastUsedAt":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","createdByUserId","name","stepsTemplate","variablesSchema","isShared","usageCount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","createdByUserId":"string","name":"string","description":"string","category":"string","stepsTemplate":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"variablesSchema":[{"key":"string","label":"string","type":"string","default":"string","description":"string","required":true}],"isShared":true,"sourcePlanId":"string","usageCount":0,"lastUsedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Der Quellplan gehoert einem anderen Mandanten, oder einem anderen Nutzer und der Aufrufer ist kein Administrator.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Zu dieser Kennung gibt es ueberhaupt keinen Plan. Der Plan eines fremden Mandanten wird gefunden und mit 403 abgewiesen — 404 heisst hier also wirklich „existiert nicht\".","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"source_plan_not_found"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Die Vorlage konnte nicht geschrieben werden.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"create_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiAgentTemplates","tags":["ai","agent-templates"],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sourcePlanId":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000},"category":{"type":"string","maxLength":120},"variablesSchema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","pattern":"^[a-zA-Z_][a-zA-Z0-9_]*$","minLength":1,"maxLength":64},"label":{"type":"string","minLength":1,"maxLength":120},"type":{"type":"string","enum":["string","number","date","boolean"]},"default":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"description":{"type":"string","maxLength":500},"required":{"type":"boolean"}},"required":["key","label","type"]},"maxItems":50},"isShared":{"type":"boolean"}},"required":["sourcePlanId","name"]},"example":{"sourcePlanId":"00000000-0000-4000-8000-000000000000","name":"string","description":"string","category":"string","variablesSchema":[],"isShared":true}}}},"summary":"Create a reusable plan template from an existing plan","description":"Kopiert die Schritte eines bestehenden Plans in eine wiederverwendbare Vorlage in\n`public.ai_agent_templates`. Der Quellplan bleibt unveraendert; die Vorlage merkt sich\nlediglich seine Kennung in `sourcePlanId`.\n\nIN `stepsTemplate` BLEIBEN DIE PLATZHALTER STEHEN. Ersetzt werden sie erst beim\nErzeugen eines Plans aus der Vorlage — hier wird nichts aufgeloest und nichts\nausgefuehrt, es laeuft also auch kein Modellaufruf.\n\nDer Quellplan muss demselben Mandanten gehoeren UND entweder dem aufrufenden Nutzer\noder, bei `admin`/`manager`/`tenant_admin`, irgendjemandem im Mandanten. Beide\nVerletzungen enden bei 403; ein Plan eines fremden Mandanten ist damit von einem\nfremden Plan im eigenen Mandanten nicht zu unterscheiden.\n\n`variablesSchema` wird uebernommen wie gesendet und NICHT gegen die Platzhalter der\nSchritte geprueft: eine Variable ohne Platzhalter und ein Platzhalter ohne Variable\nfallen erst beim Erzeugen eines Plans auf. Ohne Angabe ist die Liste leer,\n`isShared` ohne Angabe `false`.\n\nEin Erfolg wird als Aktivitaet `ai_agent_template` / `template_created` vermerkt."},"get":{"responses":{"200":{"description":"Die sichtbaren Vorlagen — ODER eine leere Liste mit `degraded: true`, wenn nicht gelesen werden konnte.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"createdByUserId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"stepsTemplate":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"variablesSchema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["string","number","date","boolean"]},"default":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"description":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","type"],"additionalProperties":false}},"isShared":{"type":"boolean"},"sourcePlanId":{"type":"string"},"usageCount":{"type":"number"},"lastUsedAt":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","createdByUserId","name","stepsTemplate","variablesSchema","isShared","usageCount","createdAt","updatedAt"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},{"type":"object","properties":{"items":{"type":"array","items":{},"maxItems":0},"degraded":{"type":"boolean","const":true},"error":{"type":"string"}},"required":["items","degraded"],"additionalProperties":false}]},"example":{"items":[{"id":"string","tenantId":"string","createdByUserId":"string","name":"string","description":"string","category":"string","stepsTemplate":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"variablesSchema":[{"key":"string","label":"string","type":"string","default":"string","description":"string","required":true}],"isShared":true,"sourcePlanId":"string","usageCount":0,"lastUsedAt":"string","createdAt":"string","updatedAt":"string"}]}}}},"400":{"description":"Die Abfrageparameter verletzen das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AiAgentTemplates","tags":["ai","agent-templates"],"parameters":[{"in":"query","name":"category","schema":{"type":"string","maxLength":120}},{"in":"query","name":"shared","schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string","maxLength":200}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0}}],"summary":"Agenten-Plan-Vorlagen auflisten (antwortet NIE mit 503)","description":"Liefert die Plan-Vorlagen des Mandanten aus\n`public.ai_agent_templates`. Reine Leseoperation: es wird nichts\ngeschrieben und KEIN Modell-Kontingent verbraucht.\n\nSichtbar sind die eigenen Vorlagen und die geteilten (`isShared`);\n`admin`, `manager` und `tenant_admin` sehen alle des Mandanten. Die\nFilterung nach Sichtbarkeit passiert NACH dem Lesen aus der Datenbank —\n`limit` und `offset` wirken also auf die UNGEFILTERTE Menge. Eine Seite\nkann dadurch weniger Eintraege enthalten, als `limit` vermuten laesst,\nund beim Blaettern koennen Eintraege ganz ausfallen.\n\nDIESE OPERATION ANTWORTET NIE MIT 503 — auch dann nicht, wenn gar keine\nDatenbank erreichbar ist. Beide Fehlerwege enden bei 200:\n\n  · kein Datenbank-Client → `{ \"items\": [], \"degraded\": true }`\n  · Abfrage gescheitert   → dasselbe, zusaetzlich `error` mit dem ROHEN\n    Text des Datenbanktreibers\n\nEine leere Liste ohne `degraded` heisst „keine Vorlagen\"; mit `degraded`\nheisst sie „nicht nachgesehen\". Wer nur `items` auswertet, kann die\nbeiden Faelle nicht unterscheiden. Die Nachbarrouten derselben Datei\nantworten im selben Fall mit 503.\n\n`shared` ist eine Zeichenkette und wird als `=== \"true\"` gelesen: jeder\nandere Wert — auch `1` oder `TRUE` — bedeutet `false`, nicht „kein\nFilter\".\n\nIN `stepsTemplate` STEHEN NOCH DIE PLATZHALTER; ersetzt werden sie erst\nbeim Erzeugen eines Plans."}},"/api/v1/ai/agent/templates/{id}":{"get":{"responses":{"200":{"description":"Die Vorlage mit Schritten und Variablen.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"createdByUserId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"stepsTemplate":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"variablesSchema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["string","number","date","boolean"]},"default":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"description":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","type"],"additionalProperties":false}},"isShared":{"type":"boolean"},"sourcePlanId":{"type":"string"},"usageCount":{"type":"number"},"lastUsedAt":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","createdByUserId","name","stepsTemplate","variablesSchema","isShared","usageCount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","createdByUserId":"string","name":"string","description":"string","category":"string","stepsTemplate":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"variablesSchema":[{"key":"string","label":"string","type":"string","default":"string","description":"string","required":true}],"isShared":true,"sourcePlanId":"string","usageCount":0,"lastUsedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Fremder Mandant, oder fremde und nicht geteilte Vorlage.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Keine Vorlage mit dieser Kennung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Die Abfrage ist gescheitert. `message` traegt den rohen Treibertext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"db_error"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1AiAgentTemplatesById","tags":["ai","agent-templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Agenten-Plan-Vorlage lesen","description":"Liefert eine Vorlage samt `stepsTemplate` und `variablesSchema` aus\n`public.ai_agent_templates`. Die Liste unter\n`GET /api/v1/ai/agent/templates` traegt dieselben Felder, aber\ngefiltert und begrenzt.\n\nSichtbar ist sie fuer ihren Ersteller, fuer `admin`/`manager`/\n`tenant_admin` — und fuer jeden im Mandanten, wenn `isShared` gesetzt\nist. Fremde Mandanten sind 403, nicht 404. Ein 404 heisst „fuer dich nicht sichtbar\", nicht zwingend „existiert nicht\": die Abfrage filtert bereits nach Mandant.\n\nIN `stepsTemplate` STEHEN NOCH DIE PLATZHALTER. Die `{{name}}`-Marken\nwerden erst beim Erzeugen eines Plans ersetzt\n(`POST /api/v1/ai/agent/templates/{id}/instantiate`). Wer die Schritte\nanzeigt, zeigt Rohtext.\n\nDie Vorlage traegt KEINE Freigabestufe: `isShared: true` heisst sofort\nsichtbar. Anders als bei den KI-Vorlagen (`/api/v1/ai/templates`) muss\nniemand zustimmen."},"patch":{"responses":{"200":{"description":"Die Vorlage im gespeicherten Zustand nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"createdByUserId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"stepsTemplate":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"variablesSchema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["string","number","date","boolean"]},"default":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"description":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","type"],"additionalProperties":false}},"isShared":{"type":"boolean"},"sourcePlanId":{"type":"string"},"usageCount":{"type":"number"},"lastUsedAt":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","createdByUserId","name","stepsTemplate","variablesSchema","isShared","usageCount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","createdByUserId":"string","name":"string","description":"string","category":"string","stepsTemplate":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"variablesSchema":[{"key":"string","label":"string","type":"string","default":"string","description":"string","required":true}],"isShared":true,"sourcePlanId":"string","usageCount":0,"lastUsedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Fremder Mandant, oder weder Ersteller noch Administrator.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Keine Vorlage mit dieser Kennung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Lesen oder Schreiben ist gescheitert. `message` traegt den rohen Treibertext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"patch_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"patchApiV1AiAgentTemplatesById","tags":["ai","agent-templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Agenten-Plan-Vorlage aendern (die Schritte selbst sind nicht aenderbar)","description":"Aendert Kopfdaten einer Plan-Vorlage: Name, Beschreibung, Kategorie,\nVariablendefinition und die Freigabe im Mandanten. Nur die\nmitgeschickten Felder werden geschrieben.\n\nDer Aufruf kostet KEIN Modell-Kontingent: es wird kein Sprachmodell\nbefragt, nur eine Zeile geaendert. Die Wirkung ist dauerhaft und nur\ndadurch umkehrbar, dass man die alten Werte erneut schreibt.\n\n`stepsTemplate` LAESST SICH HIER NICHT AENDERN. Die Schritte stammen aus\ndem Ursprungsplan und stehen fest; das Pruefschema kennt das Feld gar\nnicht. Wer andere Schritte braucht, legt aus einem anderen Plan eine\nneue Vorlage an.\n\n`isShared: true` WIRKT SOFORT. Es gibt keine Freigabestufe und keine\nZustimmung — die Vorlage ist ab diesem Aufruf fuer jeden im Mandanten\nsichtbar. `false` nimmt sie ebenso sofort wieder zurueck. (Anders als\nbei den KI-Vorlagen unter `/api/v1/ai/templates`, wo ein Administrator\nzustimmen muss.)\n\n`variablesSchema` wird als Ganzes ersetzt, nicht verschmolzen. Ein\nPlatzhalter, dessen Variable dabei wegfaellt, bleibt in den Schritten\nstehen und wird beim Erzeugen eines Plans nicht mehr ersetzt.\n\n`description` und `category` nehmen ausdruecklich `null` an — damit\nlaesst sich ein Wert loeschen, waehrend Weglassen ihn stehen laesst.\n\nAendern darf der Ersteller oder `admin`/`manager`/`tenant_admin`. Eine\nVorlage aus einem anderen Mandanten ist 403, nicht 404 — die Pruefung\nliest sie zuerst und vergleicht dann den Mandanten.\n\nDie Antwort ist der gespeicherte Stand aus dem Speicher, nicht der Rumpf.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}]},"category":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"variablesSchema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","pattern":"^[a-zA-Z_][a-zA-Z0-9_]*$","minLength":1,"maxLength":64},"label":{"type":"string","minLength":1,"maxLength":120},"type":{"type":"string","enum":["string","number","date","boolean"]},"default":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"description":{"type":"string","maxLength":500},"required":{"type":"boolean"}},"required":["key","label","type"]},"maxItems":50},"isShared":{"type":"boolean"}}},"example":{"name":"string","description":"string","category":"string","variablesSchema":[],"isShared":true}}}}},"delete":{"responses":{"200":{"description":"Geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Weder Ersteller noch Administrator.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Keine Vorlage mit dieser Kennung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Der Speicher meldete die Loeschung als nicht ausgefuehrt, oder die Abfrage ist gescheitert. `message` fehlt im ersten Fall.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"delete_failed"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"delete_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}]}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1AiAgentTemplatesById","tags":["ai","agent-templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Agenten-Plan-Vorlage endgueltig loeschen","description":"Entfernt die Zeile endgueltig aus `public.ai_agent_templates` — echtes\n`DELETE`, kein `deleted_at`. Ein zweiter Aufruf antwortet mit 404.\n\nLoeschen darf der Ersteller oder `admin`/`manager`/`tenant_admin`.\n\nBEREITS ERZEUGTE PLAENE BLEIBEN. Ein Plan aus\n`POST /{id}/instantiate` haelt eine Kopie der Schritte; er laeuft\nweiter und bleibt lesbar. Geloescht wird nur die Vorlage.\n\nEine geteilte Vorlage (`isShared: true`) verschwindet damit fuer alle\nim Mandanten. `usageCount` wird nicht geprueft und nicht gewarnt.\n\nDie Loeschung wird im Aktivitaetsprotokoll vermerkt — allerdings\nnebenlaeufig (`void`): scheitert das Protokollieren, bleibt die\nLoeschung bestehen und die Antwort `{ ok: true }`."}},"/api/v1/ai/agent/templates/{id}/instantiate":{"post":{"responses":{"201":{"description":"Der erzeugte Plan im Zustand `awaiting_confirm`. Ausgefuehrt wurde noch nichts.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"prompt":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"],"additionalProperties":false}},"status":{"type":"string"},"currentStep":{"type":"number"},"totalSteps":{"type":"number"},"costEstimateEur":{"type":"number"},"costActualEur":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","userId","prompt","steps","status","currentStep","totalSteps","costEstimateEur","costActualEur","createdAt","updatedAt"]},"example":{"id":"string","tenantId":"string","userId":"string","prompt":"string","steps":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"status":"string","currentStep":0,"totalSteps":0,"costEstimateEur":0,"costActualEur":0,"createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Der Rumpf verletzt das Pruefschema — der ROHE Auswurf des Validators.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant oder kein Nutzer im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Fremder Mandant, oder fremde und nicht geteilte Vorlage.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Keine Vorlage mit dieser Kennung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Lesen, Einsetzen oder Anlegen ist gescheitert. `message` traegt den rohen Treibertext. Ob dabei bereits eine Planzeile entstanden ist, sagt die Antwort NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"instantiate_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client verfuegbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1AiAgentTemplatesByIdInstantiate","tags":["ai","agent-templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aus einer Vorlage einen Plan erzeugen (nur planen, nicht ausfuehren)","description":"Setzt die Werte in die `{{platzhalter}}` der Vorlage ein und legt daraus\neinen NEUEN Plan in `public.ai_agent_plans` an. Der Plan startet im\nZustand `awaiting_confirm`.\n\nES LAEUFT NOCH NICHTS. Der Aufruf kostet KEIN Modell-Kontingent: es wird\nweder geplant noch ausgefuehrt, die Schritte stehen ja schon in der\nVorlage. Erst `POST /api/v1/ai/agent/plans/{id}/confirm` gibt den Plan\nfrei, und erst der Ausfuehrungs-Arbeiter verbraucht dann wirklich\nKontingent — und fuehrt die Schritte gegen echte Daten aus.\n\nGeschrieben wird trotzdem dauerhaft: eine Planzeile, der Zaehler\n`usageCount` der Vorlage und ein Eintrag im Aktivitaetsprotokoll. Der\nPlan laesst sich ueber `POST /api/v1/ai/agent/plans/{id}/cancel`\nstilllegen; der Zaehler laesst sich nicht zurueckdrehen.\n\n`costEstimateEur` ist eine SCHAETZUNG gegen `claude-sonnet-4-6` und\nsagt nichts darueber, welches Modell zur Ausfuehrungszeit wirklich\nlaeuft. Bis zum 30.08.2026 wurde gegen `claude-sonnet-4-7` gerechnet —\neine Kennung, die keine der beiden Preistabellen kennt; beide fielen\nauf `claude-opus-4-7` zurueck (15/75 statt 3/15 USD je Mio. Token) und\ndie Zahl lag rund fuenfmal zu hoch.\n\nPFLICHTVARIABLEN WERDEN NICHT ERZWUNGEN. Eine im `variablesSchema` als\n`required` markierte Variable darf fehlen; der Aufruf antwortet trotzdem\nmit 201. Auch hinterlegte `default`-Werte werden NICHT eingesetzt. Ein\nPlatzhalter ohne Wert bleibt als `{{name}}` woertlich in `toolInput`\nstehen — und geht so in den Plan ein, den der Arbeiter spaeter\nausfuehren soll. Die Antwort weist nicht darauf hin; wer sichergehen\nwill, prueft die Schritte vor dem Freigeben.\n\nEin bekannter Platzhalter mit dem Wert `null` wird durch die LEERE\nZeichenkette ersetzt, nicht offengelassen.\n\nZustand, Ergebnisse und Zeitstempel der Vorlagenschritte werden\nzurueckgesetzt: der neue Plan beginnt sauber bei `pending`.\n\nERZEUGEN DARF JEDER, DER DIE VORLAGE SEHEN DARF — also auch bei einer\nbloss geteilten (`isShared`) fremden Vorlage. Das ist eine schwaechere\nSchranke als beim Aendern und Loeschen, die den Ersteller oder einen\nAdministrator verlangen.\n\nDer Eintrag im Aktivitaetsprotokoll laeuft nebenlaeufig (`void`):\nscheitert er, bleibt der Plan bestehen und die Antwort 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"default":{}}}},"example":{"variables":{"beispiel":"string"}}}}}}},"/api/v1/ai/agent/telemetry":{"get":{"responses":{"200":{"description":"Telemetry aggregate — bei `degraded: true` sind alle Zahlen Nullen","content":{"application/json":{"schema":{"type":"object","properties":{"totalPlans":{"type":"number"},"completedCount":{"type":"number"},"failedCount":{"type":"number"},"cancelledCount":{"type":"number"},"rolledBackCount":{"type":"number"},"totalCostEur":{"type":"number"},"avgCostEur":{"type":"number"},"totalTokensInput":{"type":"number"},"totalTokensOutput":{"type":"number"},"topUsers":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string"},"planCount":{"type":"number"},"totalCostEur":{"type":"number"}},"required":["userId","planCount","totalCostEur"]}},"topTools":{"type":"array","items":{"type":"object","properties":{"toolName":{"type":"string"},"usageCount":{"type":"number"}},"required":["toolName","usageCount"]}},"dailyTrend":{"type":"array","items":{"type":"object","properties":{"day":{"type":"string"},"planCount":{"type":"number"},"costEur":{"type":"number"}},"required":["day","planCount","costEur"]}},"avgDurationSeconds":{"type":"number"},"degraded":{"type":"boolean","const":true},"error":{"type":"string"}},"required":["totalPlans","completedCount","failedCount","cancelledCount","rolledBackCount","totalCostEur","avgCostEur","totalTokensInput","totalTokensOutput","topUsers","topTools","dailyTrend","avgDurationSeconds"]},"example":{"totalPlans":0,"completedCount":0,"failedCount":0,"cancelledCount":0,"rolledBackCount":0,"totalCostEur":0,"avgCostEur":0,"totalTokensInput":0,"totalTokensOutput":0,"topUsers":[{"userId":"string","planCount":0,"totalCostEur":0}],"topTools":[{"toolName":"string","usageCount":0}],"dailyTrend":[{"day":"string","planCount":0,"costEur":0}],"avgDurationSeconds":0,"degraded":true,"error":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — insufficient role"}},"operationId":"getApiV1AiAgentTelemetry","tags":["ai","agent-plans","telemetry"],"parameters":[{"in":"query","name":"from","schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}]}},{"in":"query","name":"to","schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}]}}],"summary":"Aggregate agent-plan telemetry for the current tenant (admin-only)","description":"Fasst die Agenten-Plaene des EIGENEN Mandanten zusammen: Anzahl je Ausgang, Kosten in Euro, Token-Verbrauch, die aktivsten Nutzer und Werkzeuge, ein Tagesverlauf (letzte 30 Tage, aeltester Tag zuerst) und die mittlere Laufzeit abgeschlossener Plaene. `from` und `to` grenzen den Zeitraum ein — ein reines Datum (JJJJ-MM-TT) gilt ab Tagesbeginn UTC; ohne Angabe zaehlt alles. Rein lesend, nur fuer Administratoren. Der Endpunkt scheitert nie: ohne Datenbank oder bei einem Fehler kommen NULLEN mit `degraded: true` — eine 0 heiszt dann „nicht gemessen\", nicht „nichts passiert\"."}},"/api/v1/ai/agent/plans/all":{"get":{"responses":{"200":{"description":"List of plans — bei `degraded: true` wurde nicht gelesen","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":"string"},"prompt":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"kind":{"type":"string","enum":["tool_call","sub_agent","reflection","human_confirm"]},"toolName":{"type":"string"},"toolInput":{},"rationale":{"type":"string"},"estimatedTokensIn":{"type":"number"},"estimatedTokensOut":{"type":"number"},"estimatedCostEur":{"type":"number"},"requiresConfirmation":{"type":"boolean"},"destructive":{"type":"boolean"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"]},"result":{},"revertPayload":{},"userConfirmed":{"type":"boolean"},"startedAt":{"type":"string"},"completedAt":{"type":"string"}},"required":["index","kind","rationale","estimatedTokensIn","estimatedTokensOut","estimatedCostEur","requiresConfirmation","destructive","status"]}},"status":{"type":"string","enum":["planning","awaiting_confirm","executing","paused","completed","failed","rolled_back","cancelled"]},"currentStep":{"type":"integer"},"totalSteps":{"type":"integer"},"costEstimateEur":{"type":"number"},"costActualEur":{"type":"number"},"tokensUsed":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cacheRead":{"type":"number"}},"required":["input","output","cacheRead"]},"rollbackStack":{"type":"array","items":{"type":"object","properties":{"stepIndex":{"type":"integer"},"revertPayload":{}},"required":["stepIndex"]}},"errorMessage":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"completedAt":{"type":"string"},"authorRole":{"type":"string"},"authorPermissions":{"type":"array","items":{"type":"string"}},"authorTenantPlan":{"type":"string"},"readOnly":{"type":"boolean"}},"required":["id","tenantId","userId","prompt","steps","status","currentStep","totalSteps","costEstimateEur","costActualEur","tokensUsed","rollbackStack","createdAt","updatedAt"]}},"total":{"type":"integer"},"degraded":{"type":"boolean","const":true},"error":{"type":"string"}},"required":["items","total"]},"example":{"items":[{"id":"string","tenantId":"string","userId":"string","prompt":"string","steps":[{"index":0,"kind":"tool_call","toolName":"string","rationale":"string","estimatedTokensIn":0,"estimatedTokensOut":0,"estimatedCostEur":0,"requiresConfirmation":true,"destructive":true,"status":"pending","userConfirmed":true,"startedAt":"string","completedAt":"string"}],"status":"planning","currentStep":0,"totalSteps":0,"costEstimateEur":0,"costActualEur":0,"tokensUsed":{"input":0,"output":0,"cacheRead":0},"rollbackStack":[{"stepIndex":0}],"errorMessage":"string","createdAt":"string","updatedAt":"string","completedAt":"string","authorRole":"string","authorPermissions":["string"],"authorTenantPlan":"string","readOnly":true}],"total":0,"degraded":true,"error":"string"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — insufficient role"}},"operationId":"getApiV1AiAgentPlansAll","tags":["ai","agent-plans"],"parameters":[{"in":"query","name":"status","schema":{"type":"string","maxLength":200}},{"in":"query","name":"userId","schema":{"type":"string","maxLength":120}},{"in":"query","name":"from","schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}]}},{"in":"query","name":"to","schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}]}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0}}],"summary":"List ALL agent plans for the current tenant (admin-only)","description":"Anders als die nutzereigene Planliste zeigt diese die Plaene ALLER Nutzer des Mandanten — deshalb nur fuer Administratoren. Filter: `status` (mehrere durch Komma getrennt; unbekannte Werte werden verworfen, und wenn KEINER uebrig bleibt, wirkt der Filter gar nicht), `userId` sowie `from`/`to` auf das Anlagedatum. Blaettern ueber `limit` (1…500, Vorgabe 50) und `offset`. ACHTUNG bei der Blaetterung: der Zeitraum wird erst NACH dem Blaettern angewandt, `items` kann darum kuerzer sein als `limit`, obwohl es weitere Treffer gibt — `total` zaehlt dagegen ueber alle Filter. Jeder Eintrag enthaelt die vollstaendigen Schritte samt Prompt und Zwischenergebnissen. Rein lesend; ohne Datenbank oder bei einem Fehler kommt eine leere Liste mit `degraded: true`."}},"/api/v1/ai/workflow-builder/plan":{"post":{"responses":{"200":{"description":"Der Entwurf und seine Herkunft.","content":{"application/json":{"schema":{"type":"object","properties":{"draft":{"type":"object","properties":{"name":{"type":"string","minLength":1},"trigger":{"type":"string","enum":["manual","event","schedule"]},"entityType":{"type":"string"},"eventName":{"type":"string"},"cron":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"params":{"type":"object","additionalProperties":{}}},"required":["action","params"]},"minItems":1},"rollbackStrategy":{"type":"string","enum":["none","undo_last","snapshot"],"default":"snapshot"}},"required":["name","trigger","steps","rollbackStrategy"]},"source":{"type":"string","description":"`heuristic` = aus festen Schluesselwoertern gebaut · `llm` = von einem eingehaengten Modell. Ist keines eingehaengt oder scheitert es, steht hier `heuristic`."}},"required":["draft","source"]},"example":{"draft":{"name":"string","trigger":"manual","entityType":"string","eventName":"string","cron":"string","steps":[{"action":"string","params":{}}],"rollbackStrategy":"none"},"source":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1AiWorkflow-builderPlan","tags":["ai","workflow-builder"],"parameters":[],"description":"Uebersetzt eine Beschreibung in natuerlicher Sprache in einen Workflow-Entwurf: Ausloeser (`manual`, `event` oder `schedule`), Schritte und Ruecknahmestrategie. Es wird NICHTS gespeichert — dafuer ist `/commit` da. Ist kein Sprachmodell eingehaengt, entsteht der Entwurf aus festen Schluesselwoertern (etwa „taeglich\" → Zeitplan `0 9 * * *`, „Rechnung bezahlt\" → Ereignis `invoice.paid`, „Mail\" → ein E-Mail-Schritt); ein eingehaengtes Modell wird gefragt und sein Ergebnis noch einmal geprueft, bevor es herauskommt. Scheitert es, greift wieder die Heuristik — `source` sagt, was zutraf. Erkennt die Heuristik nichts, entsteht ein Entwurf mit einem einzelnen Protokollschritt, nie ein leerer. Der Entwurf ist ein GERUEST: Empfaenger und Texte stehen als Platzhalter darin und wollen nachbearbeitet werden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","minLength":3,"maxLength":4000}},"required":["prompt"]},"example":{"prompt":"string"}}}},"summary":"Uebersetzt eine Beschreibung in natuerlicher Sprache in einen Workflow-Entwurf","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/workflow-builder/commit":{"post":{"responses":{"201":{"description":"Angelegt. `workflow` ist die Antwort des Workflow-Speichers, unveraendert durchgereicht.","content":{"application/json":{"schema":{"type":"object","properties":{"workflow":{"description":"Die Antwort des Workflow-Speichers, unveraendert durchgereicht. Ihre Form bestimmt `POST /api/v1/workflows`, nicht dieser Aufruf — deshalb hier keine Feldzusage."},"action_id":{"type":["string","null"],"description":"Die Id aus dieser Antwort; null, wenn sie keine trug"}},"required":["action_id"]},"example":{"action_id":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"},"503":{"description":"Workflow-Engine nicht verfuegbar"}},"operationId":"postApiV1AiWorkflow-builderCommit","tags":["ai","workflow-builder"],"parameters":[],"description":"Speichert einen Entwurf als echten Workflow. Der Rumpf ist die Form aus `/plan`, zusaetzlich das optionale `active` (Vorgabe: aktiv — der Workflow laeuft also sofort). Die Schritte werden dabei UEBERSETZT, nicht durchgereicht: `event` wird zu einer Datensatzaenderung mit Operation `create`, ein Schritt ohne bekannte Entsprechung landet als KI-Schritt mit dem urspruenglichen Text als Anweisung. Wer eine wortgetreue Uebernahme braucht, legt den Workflow direkt ueber `POST /api/v1/workflows` an. Genau dorthin ruft dieser Aufruf intern weiter; faellt dieser Weg aus, kommt 503 und es wurde nichts gespeichert. Ab Rolle `admin` — anders als `/plan`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"trigger":{"type":"string","enum":["manual","event","schedule"]},"entityType":{"type":"string"},"eventName":{"type":"string"},"cron":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"params":{"type":"object","additionalProperties":{}}},"required":["action","params"]},"minItems":1},"rollbackStrategy":{"type":"string","enum":["none","undo_last","snapshot"],"default":"snapshot"},"active":{"type":"boolean"}},"required":["name","trigger","steps"]},"example":{"name":"string","trigger":"manual","entityType":"string","eventName":"string","cron":"string","steps":[{"action":"string","params":{}}],"rollbackStrategy":"none","active":true}}}},"summary":"Speichert einen Entwurf als echten Workflow","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/voice-transcribe":{"post":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"501":{"description":"Nicht verfuegbar — die EINZIGE Antwort dieser Route. Ohne Schluessel `not_configured`, mit Schluessel `not_implemented`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","const":"not_configured"},"message":{"type":"string"},"hint":{"type":"string"}},"required":["ok","error","message","hint"]},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","const":"not_implemented"},"message":{"type":"string"}},"required":["ok","error","message"]}]}}}}},"operationId":"postApiV1AiVoice-transcribe","tags":["ai","voice"],"parameters":[],"description":"Backend Whisper bridge. Returns 501 when transcription backend is not configured. Browser-side Web-Speech-API is the preferred path; this exists for high-accuracy fallback.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"audio_base64":{"type":"string","minLength":8,"maxLength":20000000},"mime":{"type":"string","minLength":3,"maxLength":80,"default":"audio/webm"},"language":{"type":"string","minLength":2,"maxLength":8}},"required":["audio_base64"]},"example":{"audio_base64":"stringxx","mime":"string","language":"string"}}}},"summary":"Backend Whisper bridge","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/ai/vision":{"post":{"responses":{"200":{"description":"Classification result","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"text":{"type":"string"},"model":{"type":"string"},"usage":{"type":"object","properties":{"input_tokens":{"type":"number"},"output_tokens":{"type":"number"}},"required":["input_tokens","output_tokens"]}},"required":["ok","text","model","usage"]},"example":{"ok":true,"text":"string","model":"string","usage":{"input_tokens":0,"output_tokens":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"501":{"description":"Kein AI-Provider konfiguriert","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","enum":["not_configured","vision_call_failed"]},"message":{"type":"string"}},"required":["ok","error","message"]}}}},"502":{"description":"Der Aufruf beim Anbieter ist gescheitert (Drosselung, Zeitgrenze, Region). Die Meldung ist bewusst allgemein — der rohe Anbieterfehler bleibt im Log.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","enum":["not_configured","vision_call_failed"]},"message":{"type":"string"}},"required":["ok","error","message"]}}}}},"operationId":"postApiV1AiVision","tags":["ai","vision"],"parameters":[],"summary":"Klassifiziert ein Bild ueber die zentrale LLM-Schicht","description":"Multimodale Bild-Klassifikation über die zentrale, provider-agnostische lib/llm.ts (EU-Region bei NEMIX_AI_PROVIDER=bedrock). Returns 501 when no AI provider is configured. ANTHROPIC_VISION_KEY ist seit der AI-1-Migration KEIN eigener Hebel mehr.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"image_base64":{"type":"string","minLength":64,"maxLength":20000000},"mime":{"type":"string","enum":["image/png","image/jpeg","image/webp","image/gif"],"default":"image/png"},"prompt":{"type":"string","minLength":1,"maxLength":4000,"default":"Beschreibe das Bild knapp und nenne erkannte Beträge/Daten/Namen."}},"required":["image_base64"]},"example":{"image_base64":"stringxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","mime":"image/png","prompt":"string"}}}}}},"/api/v1/custom-agents":{"get":{"responses":{"200":{"description":"Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"trigger_type":{"type":"string","enum":["manual","schedule","webhook","on-event"]},"trigger_config":{"type":"object","additionalProperties":{}},"allowed_tools":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["active","paused","archived"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","description","trigger_type","trigger_config","allowed_tools","status","created_at","updated_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","name":"string","description":"string","trigger_type":"manual","trigger_config":{},"allowed_tools":["string"],"status":"active","created_at":"string","updated_at":"string"}]}}}},"401":{"description":"Unauthorized"},"500":{"description":"Abfrage fehlgeschlagen"},"503":{"description":"Keine Datenbankverbindung"}},"operationId":"getApiV1Custom-agents","tags":["ai","custom-agents"],"parameters":[],"summary":"Listet alle Custom-Agents des Mandanten (nicht archiviert)","description":"Gibt alle aktiven und pausierten Agenten in EINER Antwort zurueck — ohne Blaetterung und ohne Filter —, zuletzt geaenderte zuerst. Archivierte fehlen; sie sind nur ueber GET /{id} mit bekannter Kennung erreichbar. Anders als dort liefert die Liste `system_prompt` NICHT mit. Lesen braucht keine besondere Rolle."},"post":{"responses":{"201":{"description":"Erstellt. Die vollstaendige Zeile, einschliesslich `system_prompt`.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Rolle unterhalb von `admin` — `code: \"INSUFFICIENT_ROLE\"`."},"500":{"description":"Anlegen fehlgeschlagen — `error: \"create_failed\"`."},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1Custom-agents","tags":["ai","custom-agents"],"parameters":[],"description":"Erstellt einen neuen Custom-Agent.\n\nNUR ADMINISTRATOREN (seit 17.08.2026). Der Grund steht im Rumpf: `allowed_tools` bestimmt, welche Werkzeuge der Agent ausfuehren darf. Einen Agenten anzulegen heisst also, Ausfuehrungsrechte zu vergeben — vorher durfte das jeder angemeldete Benutzer des Mandanten, auch einer ohne jedes Schreibrecht auf die betroffenen Daten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":2000},"trigger_type":{"type":"string","enum":["manual","schedule","webhook","on-event"]},"trigger_config":{"type":"object","additionalProperties":{},"default":{}},"system_prompt":{"type":"string","minLength":1,"maxLength":20000},"allowed_tools":{"type":"array","items":{"type":"string"},"default":[]}},"required":["name","trigger_type","system_prompt"]},"example":{"name":"string","description":"string","trigger_type":"manual","trigger_config":{},"system_prompt":"string","allowed_tools":["string"]}}}},"summary":"Erstellt einen neuen Custom-Agent","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/custom-agents/{id}":{"get":{"responses":{"200":{"description":"Der Agent, auch wenn er archiviert ist.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}},"example":{"id":"7b3c9d1e-2f4a-4b5c-8d6e-9f0a1b2c3d4e","tenant_id":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","name":"Mahnwesen-Assistent","description":"Prüft jeden Morgen überfällige Rechnungen und bereitet Zahlungserinnerungen vor.","trigger_type":"schedule","trigger_config":{"cron":"0 7 * * 1-5","timezone":"Europe/Berlin"},"system_prompt":"Du bist der Mahnwesen-Assistent der Musterbau GmbH. Sprich Kunden höflich und bestimmt an.","allowed_tools":["search_invoices","email_writer"],"status":"active","created_by":"e2d4c6a8-0b1c-4d2e-8f3a-4b5c6d7e8f9a","created_at":"2026-04-08T06:30:00.000Z","updated_at":"2026-07-19T12:00:15.000Z"}}}},"401":{"description":"Kein Mandantenkontext."},"404":{"description":"Kein Agent mit dieser Kennung IM EIGENEN MANDANTEN — `error: \"Not found\"`. Deckt „gibt es nicht\" und „gehoert einem anderen Mandanten\" gemeinsam ab.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error: \"get_failed\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"getApiV1Custom-agentsById","tags":["Eigene Agenten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen eigenen KI-Agenten lesen","description":"Die vollstaendige Zeile des Agenten, inklusive `system_prompt` und `allowed_tools`.\n\nIM GEGENSATZ ZUR LISTE FILTERT DIESE ROUTE NICHT NACH STATUS: ein archivierter Agent wird hier weiterhin geliefert. Das ist beabsichtigt und der Weg zurueck — mit der Kennung aus dieser Antwort laesst sich ueber `PUT /{id}` der `status` wieder auf `active` setzen. Die Liste zeigt archivierte Agenten nicht, gibt die Kennung also nicht mehr her; wer sie verliert, kommt ueber diese API nicht mehr an den Agenten.\n\nDie Abfrage ist ein `SELECT *` ohne Serialisierer — der Vertrag sagt keine Feldnamen zu. Heute sind es unter anderem `id`, `tenant_id`, `name`, `description`, `trigger_type`, `trigger_config`, `system_prompt`, `allowed_tools`, `status` und die Zeitstempel.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten darf das."},"put":{"responses":{"200":{"description":"Geaendert. Die vollstaendige Zeile nach dem Schreiben.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validierungsfehler ODER kein einziges aenderbares Feld im Rumpf — `error: \"Empty update\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"401":{"description":"Kein Mandantenkontext."},"403":{"description":"Rolle unterhalb von `admin` — `code: \"INSUFFICIENT_ROLE\"`. Der Agent bleibt unveraendert.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"404":{"description":"Kein Agent mit dieser Kennung IM EIGENEN MANDANTEN — `error: \"Not found\"`. Deckt „gibt es nicht\" und „gehoert einem anderen Mandanten\" gemeinsam ab.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Schreiben fehlgeschlagen — `error: \"update_failed\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"putApiV1Custom-agentsById","tags":["Eigene Agenten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen eigenen Agenten aendern","description":"Aendert die uebergebenen Felder eines Agenten. TROTZ `PUT` IST DAS EIN TEIL-UPDATE: nur mitgeschickte Felder werden geschrieben, weggelassene bleiben stehen. Es wird nichts auf Vorgabewerte zurueckgesetzt.\n\nDAS IST DER WEG ZURUECK AUS DEM ARCHIV. `status` ist hier — anders als beim Anlegen — Teil des Rumpfs: `{\"status\": \"active\"}` holt einen ueber `DELETE /{id}` archivierten Agenten wieder zurueck. Erlaubt sind `active`, `paused` und `archived`.\n\nAENDERBAR SIND AUCH `system_prompt` UND `allowed_tools`, also genau die beiden Felder, die bestimmen, was der Agent tun darf. Deshalb dieselbe Schranke wie beim Anlegen: NUR ADMINISTRATOREN (seit 17.08.2026). Waere das Anlegen gesichert und das Umschreiben offen, liesse sich die Werkzeugliste eines bestehenden Agenten nachtraeglich erweitern.\n\nEin Rumpf ohne ein einziges aenderbares Feld ist ein Fehler, kein Leerlauf: die Route antwortet 400 (`Empty update`) und schreibt nichts. `updated_at` wird bei jeder erfolgreichen Aenderung neu gesetzt.\n\nDie Antwort ist die vollstaendige Zeile aus einem `RETURNING *` ohne Serialisierer — der Vertrag sagt wie bei `GET /{id}` keine Feldnamen zu.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":2000},"trigger_type":{"type":"string","enum":["manual","schedule","webhook","on-event"]},"trigger_config":{"type":"object","additionalProperties":{},"default":{}},"system_prompt":{"type":"string","minLength":1,"maxLength":20000},"allowed_tools":{"type":"array","items":{"type":"string"},"default":[]},"status":{"type":"string","enum":["active","paused","archived"]}}},"example":{"name":"string","description":"string","trigger_type":"manual","trigger_config":{},"system_prompt":"string","allowed_tools":["string"],"status":"active"}}}}},"delete":{"responses":{"200":{"description":"Archiviert. `archived` traegt die Kennung zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"archived":{"type":"string"}},"required":["ok","archived"]},"example":{"ok":true,"archived":"string"}}}},"401":{"description":"Kein Mandantenkontext."},"403":{"description":"Rolle unterhalb von `admin` — `code: \"INSUFFICIENT_ROLE\"`. Der Agent laeuft weiter.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"404":{"description":"Kein Agent mit dieser Kennung IM EIGENEN MANDANTEN — `error: \"Not found\"`. Deckt „gibt es nicht\" und „gehoert einem anderen Mandanten\" gemeinsam ab.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Schreiben fehlgeschlagen — `error: \"archive_failed\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"deleteApiV1Custom-agentsById","tags":["Eigene Agenten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen eigenen Agenten archivieren","description":"ES WIRD NICHTS GELOESCHT: die Route setzt `status = \"archived\"`. Die Zeile bleibt mitsamt Prompt, Werkzeugliste und Laufhistorie stehen.\n\nDER WEG ZURUECK EXISTIERT — anders als bei manch anderem Zustandsfeld im Haus. `PUT /{id}` mit `{\"status\": \"active\"}` holt den Agenten zurueck. Nur muss man die Kennung dann noch haben: die Liste zeigt archivierte Agenten nicht mehr, `GET /{id}` dagegen schon.\n\nEin bereits archivierter Agent laesst sich erneut archivieren und meldet wieder 200 — es wird nicht auf den Vorzustand geprueft.\n\nNUR ADMINISTRATOREN (seit 17.08.2026). Der Weg zurueck fuehrt ueber `PUT /{id}`, und der traegt dieselbe Schranke — waere das Archivieren offen und das Zurueckholen nicht, koennte ein beliebiger Benutzer jeden Agenten des Mandanten stilllegen, ohne ihn wieder anschalten zu koennen."}},"/api/v1/custom-agents/{id}/run":{"post":{"responses":{"200":{"description":"Lauf beendet. Der Rumpf ist das Ergebnis des Runners (Ausgabetext und Zwischenschritte). ACHTUNG: `status` kann auch `failed` sein — ein gescheiterter Lauf kommt als 200, nicht als 5xx.","content":{"application/json":{"schema":{"type":"object","properties":{"runId":{"type":"string"},"status":{"type":"string","enum":["done","failed"]},"outputText":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"tool":{"type":"string"},"input":{},"output":{},"duration_ms":{"type":"number"}},"required":["tool","duration_ms"]}},"errorMessage":{"type":"string"}},"required":["runId","status","outputText","steps"]},"example":{"runId":"string","status":"done","outputText":"string","steps":[{"tool":"string","duration_ms":0}],"errorMessage":"string"}}}},"400":{"description":"Validierungsfehler — `inputText` fehlt, ist leer oder zu lang.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"401":{"description":"Kein Mandantenkontext."},"403":{"description":"Rolle unterhalb von `manager` — `code: \"INSUFFICIENT_ROLE\"`. Es wird nichts gestartet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"404":{"description":"Kein Agent mit dieser Kennung IM EIGENEN MANDANTEN — `error: \"Not found\"`. Deckt „gibt es nicht\" und „gehoert einem anderen Mandanten\" gemeinsam ab.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"409":{"description":"Der Agent ist pausiert oder archiviert — `error: \"agent_not_active\"`. Es wurde nichts ausgefuehrt und nichts berechnet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"agent_not_active"},"status":{"type":"string"},"message_de":{"type":"string"}},"required":["error","message_de"]}}}},"500":{"description":"Der Lauf ist gescheitert — `error: \"run_failed\"`. Auch der Fall, in dem der Runner selbst einen nicht aktiven Agenten abweist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1Custom-agentsByIdRun","tags":["Eigene Agenten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen eigenen Agenten ausfuehren","description":"Startet den Agenten mit `inputText` als Eingabe und ANTWORTET ERST, WENN ER FERTIG IST. Der Lauf ist synchron: es gibt keine Lauf-Kennung zum Nachfragen und keinen Abbruch. Je nach Auftrag kann das dauern.\n\nNUR EIN AKTIVER AGENT LAEUFT. Steht `status` auf `paused` oder `archived`, bricht die Route mit 409 ab, BEVOR das Modell angefragt wird — das ist der Not-Aus pro Agent. Das Feld `message_de` der Antwort nennt den Grund im Klartext.\n\nAusfuehren braucht `manager`, nicht `admin`: einen VORHANDENEN Agenten zu starten ist eine Bedienhandlung, kein Vergeben von Rechten. Die Werkzeugliste steht zu diesem Zeitpunkt fest; wer sie aendern will, braucht `admin` (`POST /` und `PUT /{id}`).\n\nDIESE ROUTE HAT KEIN 503 — als einzige der Datei. Liefert `getQueryClient()` nichts, wird die Statuspruefung samt ihres 404 und 409 UEBERSPRUNGEN und der Lauf trotzdem gestartet; die fail-closed Absicherung ist dann allein die Pruefung im Runner, deren Fehler als 500 erscheint. Dasselbe gilt, wenn die Statusabfrage selbst scheitert.\n\nDer Lauf wird in `public.custom_agent_runs` mitgeschrieben und ist anschliessend ueber `GET /{id}/runs` sichtbar. Das Modell kommt aus dem zentralen Gateway, nicht aus der Agenten-Konfiguration.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"inputText":{"type":"string","minLength":1,"maxLength":20000}},"required":["inputText"]},"example":{"inputText":"string"}}}}}},"/api/v1/custom-agents/{id}/runs":{"get":{"responses":{"200":{"description":"Bis zu 20 Laeufe. Leer heisst „noch nie gelaufen ODER Agent gibt es nicht\".","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"agent_id":{"type":"string"},"trigger_source":{"type":["string","null"]},"input_text":{"type":["string","null"]},"output_text":{"type":["string","null"]},"steps":{},"status":{"type":"string"},"error_message":{"type":["string","null"]},"started_at":{"type":"string"},"finished_at":{"type":["string","null"]},"total_tokens":{"type":["number","null"]},"cost_usd":{"type":["number","null"]}},"required":["id","agent_id","trigger_source","input_text","output_text","status","error_message","started_at","finished_at","total_tokens","cost_usd"]}}},"required":["data"]},"example":{"data":[{"id":"string","agent_id":"string","trigger_source":"string","input_text":"string","output_text":"string","status":"string","error_message":"string","started_at":"string","finished_at":"string","total_tokens":0,"cost_usd":0}]}}}},"401":{"description":"Kein Mandantenkontext."},"500":{"description":"Abfrage fehlgeschlagen — `error: \"runs_failed\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"getApiV1Custom-agentsByIdRuns","tags":["Eigene Agenten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Die letzten Laeufe eines Agenten","description":"Die letzten **20** Laeufe, neueste zuerst. Die Zahl ist fest verdrahtet und nicht einstellbar; es gibt keine Blaetterung und keine Gesamtzahl. Wer mehr Historie braucht, bekommt sie ueber diese API nicht.\n\nDIESE ROUTE PRUEFT NICHT, OB DER AGENT EXISTIERT. Eine erfundene Kennung ergibt keinen 404, sondern 200 mit leerer Liste — nicht zu unterscheiden von einem Agenten, der noch nie gelaufen ist. Der Mandantenfilter greift trotzdem: fremde Laeufe erscheinen nie.\n\n`output_text` und `steps` enthalten die vollstaendige Antwort des Modells samt Zwischenschritten — je nach Auftrag also Geschaeftsdaten im Klartext. `cost_usd` ist in US-DOLLAR, anders als die Euro-Betraege der Kostenuebersicht unter `/api/admin/ai-monitoring`.\n\nFeldnamen sind hier zusagbar, weil die Abfrage eine ausgeschriebene Spaltenliste hat — kein `SELECT *`.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten darf das."}},"/api/v1/2fa/status":{"get":{"responses":{"200":{"description":"Zustand des zweiten Faktors","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Nur nach bestaetigtem Code true — eine begonnene Einrichtung zaehlt nicht."},"einrichtungOffen":{"type":"boolean","description":"Ein Geheimnis liegt vor, der erste Code wurde aber noch nicht bestaetigt."},"verbleibendeWiederherstellungscodes":{"type":"integer","description":"Noch unverbrauchte Wiederherstellungscodes; 0 wenn nichts gespeichert ist."}},"required":["enabled","einrichtungOffen","verbleibendeWiederherstellungscodes"]},"example":{"enabled":true,"einrichtungOffen":true,"verbleibendeWiederherstellungscodes":0}}}},"401":{"description":"Anmeldung noetig"},"503":{"description":"2FA-Speicher nicht verdrahtet"}},"operationId":"getApiV12faStatus","tags":["2fa"],"parameters":[],"description":"Zustand des zweiten Faktors fuer den angemeldeten Nutzer. `enabled` ist nur nach bestaetigtem Code true; `einrichtungOffen` meldet eine begonnene, noch nicht bestaetigte Einrichtung. Ohne Anmeldung 401 — das heisst „unbekannt\", nicht „aus\".","summary":"Zustand des zweiten Faktors fuer den angemeldeten Nutzer","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/2fa/enroll":{"post":{"responses":{"201":{"description":"Enrollment stored as pending — secret and the ten one-time recovery codes are handed out once, in this body.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"otpauthUrl":{"type":"string","description":"otpauth:// URL for the QR code — carries issuer, account label and secret."},"secret":{"type":"string","description":"The same TOTP secret, base32-encoded, for manual entry when no QR code can be scanned."},"recoveryCodes":{"type":"array","items":{"type":"string"},"description":"The ten recovery codes in plaintext, formatted xxxx-xxxx-xxxx. Returned only here — the server keeps nothing but their hashes."}},"required":["ok","otpauthUrl","secret","recoveryCodes"]},"example":{"ok":true,"otpauthUrl":"string","secret":"string","recoveryCodes":["string"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Already enabled"}},"operationId":"postApiV12faEnroll","tags":["2fa"],"parameters":[],"description":"Generate a fresh TOTP secret + 10 recovery codes (pending). Returns otpauth URL for QR rendering.","summary":"Generate a fresh TOTP secret + 10 recovery codes (pending)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/2fa/verify":{"post":{"responses":{"200":{"description":"2FA enabled — the code matched and the account state was flipped","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"enabled":{"type":"boolean","const":true,"description":"Always true — the account is 2FA-enabled from here on."}},"required":["ok","enabled"]},"example":{"ok":true,"enabled":true}}}},"401":{"description":"Invalid TOTP"},"404":{"description":"No pending enrollment"}},"operationId":"postApiV12faVerify","tags":["2fa"],"parameters":[],"summary":"Confirm the pending 2FA enrollment with the first TOTP code","description":"Takes `code` from the JSON body (6-10 characters; anything else is rejected with 422) and checks it against the secret that /2fa/enroll stored, tolerating a drift of one 30 s step in either direction. On a match the same record is written back with `enabled: true` — secret and recovery codes are left as they are, so the codes issued at enrollment stay valid. Without a pending enrollment the answer is 404, on a wrong code 401; neither writes anything."}},"/api/v1/2fa/disable":{"post":{"responses":{"200":{"description":"2FA disabled — secret cleared for this user","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"enabled":{"type":"boolean","const":false,"description":"Always false — the account is 2FA-free from here on."}},"required":["ok","enabled"]},"example":{"ok":true,"enabled":false}}}},"401":{"description":"Invalid code"},"409":{"description":"Not enabled"}},"operationId":"postApiV12faDisable","tags":["2fa"],"parameters":[],"summary":"Turn 2FA off using a current TOTP or a recovery code","description":"Takes `code` from the JSON body (6-10 characters; anything else is rejected with 422). The value is first tried as a current TOTP (one 30 s step of drift allowed) and, only if that fails, as one of the recovery codes — which is then burnt. An enrollment that is not actually enabled answers 409, a code that matches neither 401; both leave the stored state untouched. On success the secret is cleared and `enabled` set to false. A disable via recovery code additionally drops the whole recovery-code list; a disable via TOTP keeps the stored codes."}},"/api/v1/2fa/recovery":{"post":{"responses":{"200":{"description":"Recovery accepted — the code is now marked as used and cannot be reused","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"remaining":{"type":"integer","description":"Recovery codes still unused — the one just burnt is already deducted."}},"required":["ok","remaining"]},"example":{"ok":true,"remaining":0}}}},"401":{"description":"Invalid code"},"409":{"description":"Not enabled"}},"operationId":"postApiV12faRecovery","tags":["2fa"],"parameters":[],"description":"Burn a recovery code for step-up auth. Returns remaining count.","summary":"Burn a recovery code for step-up auth","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/api-keys":{"get":{"responses":{"200":{"description":"Liste der API-Keys ohne Schluesselwert","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Schluessels"},"name":{"type":"string","description":"Vergebener Name"},"prefix":{"type":"string","description":"Die ersten zehn Zeichen des Schluessels (Spalte `key_prefix`) als Wiedererkennung"},"scopes":{"type":"array","items":{"type":"string"},"description":"Berechtigungen aus der Spalte `permissions`; leere Liste wenn keine gesetzt sind"},"lastUsedAt":{"type":["string","null"],"description":"Letzte Verwendung; null solange der Schluessel nie benutzt wurde"},"expiresAt":{"type":["string","null"],"description":"Ablaufzeitpunkt; null wenn der Schluessel nicht ablaeuft"},"createdAt":{"type":["string","null"],"description":"Anlagezeitpunkt"}},"required":["id","name","prefix","scopes","lastUsedAt","expiresAt","createdAt"]},"description":"Alle Schluessel des Mandanten, neueste zuerst — OHNE den Schluesselwert"}},"required":["keys"]},"example":{"keys":[{"id":"string","name":"string","prefix":"string","scopes":["string"],"lastUsedAt":"string","expiresAt":"string","createdAt":"string"}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Admin-Rolle erforderlich"},"503":{"description":"Datenbank nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"getApiV1Api-keys","tags":["api-keys"],"parameters":[],"description":"Liest alle Schluessel des Mandanten aus `public.api_keys`, neueste zuerst. Zurueck kommen nur Kennung, Name, die ersten zehn Zeichen, Berechtigungen, letzte Verwendung, Ablauf und Anlagezeitpunkt — der Schluesselwert selbst NIE, gespeichert ist nur sein SHA-256-Abdruck. Abgelaufene Schluessel werden NICHT ausgefiltert; das steht in `expiresAt` und ist vom Aufrufer zu pruefen. Es wird nicht geblaettert. Erfordert die Rolle `admin` — auch das blosse Lesen.","summary":"Liest alle Schluessel des Mandanten aus `public.api_keys`, neueste zuerst","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"API-Key erstellt — Voll-Wert nur in dieser Antwort","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Schluessels"},"name":{"type":"string","description":"Vergebener Name"},"key":{"type":"string","description":"Der vollstaendige Schluessel — NUR in dieser einen Antwort. Gespeichert wird nur sein SHA-256-Abdruck"},"prefix":{"type":"string","description":"Die ersten zehn Zeichen; erscheinen spaeter in der Liste"},"scopes":{"type":"array","items":{"type":"string"},"description":"Die gesetzten Berechtigungen"},"expiresAt":{"type":["string","null"],"description":"Ablaufzeitpunkt; null wenn keiner uebergeben wurde"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"warning":{"type":"string","description":"Hinweis, dass der Wert nicht erneut abrufbar ist"}},"required":["id","name","key","prefix","scopes","expiresAt","createdAt","warning"]},"example":{"id":"string","name":"string","key":"string","prefix":"string","scopes":["string"],"expiresAt":"string","createdAt":"string","warning":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Admin-Rolle erforderlich"},"503":{"description":"Datenbank nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"postApiV1Api-keys","tags":["api-keys"],"parameters":[],"description":"Erstellt einen neuen API-Key (nur Admin). Der Voll-Wert wird NUR EINMALIG im Response zurückgegeben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"scopes":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"minItems":1,"maxItems":32,"default":["read"]},"expires_at":{"type":"string","format":"date-time"}},"required":["name"]},"example":{"name":"string","scopes":["string"],"expires_at":"2026-01-01T12:00:00.000Z"}}}},"summary":"Erstellt einen neuen API-Key (nur Admin)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/api-keys/{id}":{"delete":{"responses":{"200":{"description":"API-Key gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string","description":"Kennung des geloeschten Schluessels"}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Admin-Rolle erforderlich"},"404":{"description":"Kein API-Key mit dieser Kennung im eigenen Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfügbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"deleteApiV1Api-keysById","tags":["api-keys"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Loescht die Zeile in `public.api_keys` ENDGUELTIG — kein Soft-Delete, kein `deleted_at`, kein Wiederherstellen. Der Schluessel ist ab sofort ungueltig. Geloescht wird nur innerhalb des eigenen Mandanten; ein fremder oder unbekannter Schluessel ergibt 404, nicht 403. Erfordert die Rolle `admin`.","summary":"Loescht die Zeile in `public.api_keys` ENDGUELTIG","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/entwickler-paket":{"get":{"responses":{"200":{"description":"ZIP archive (application/zip), sent as an attachment.","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Role below manager"},"503":{"description":"Database unavailable — retry after the given number of seconds"}},"operationId":"getApiV1Entwickler-paket","tags":["developer"],"parameters":[],"summary":"Download the personalised developer starter kit","description":"Returns a ZIP archive pre-filled for the calling tenant: a quick-start README, a CLAUDE.md project manual for the customer's own AI, an .env template, this tenant's custom-field definitions as JSON and Markdown, a mandanten-profil.json describing how this business actually works (own rules, workflows, number patterns, templates, approval thresholds — each block marked with where it came from, and `nicht_verfuegbar` where it could not be read), and a ready-made MCP configuration. Carries NO API key and no business data — definitions and patterns only, never values and never counters. It does carry a one-shot redemption code, valid for 30 minutes, which the customer's AI trades for a key at POST /api/v1/entwickler-paket/einloesen; the code's scopes are capped against the rights of the caller downloading it. `X-Package-Code` reports whether a code could be issued. Requires role manager or above."}},"/api/v1/mcp/tokens":{"post":{"responses":{"201":{"description":"Token erstellt — Plaintext nur in dieser Antwort","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":["string","null"]},"token":{"type":"string","description":"Klartext im Format mcp_<64 Hex> — nur in dieser einen Antwort"},"scopes":{"type":"string","description":"Gespeicherte Umfaenge, durch Leerzeichen getrennt"},"expiresAt":{"type":"string","description":"ISO 8601"},"createdAt":{"type":"string"},"warning":{"type":"string"}},"required":["id","label","token","scopes","expiresAt","createdAt","warning"]},"example":{"id":"string","label":"string","token":"string","scopes":"string","expiresAt":"string","createdAt":"string","warning":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"500":{"description":"Einfuegen lieferte keine Zeile"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"postApiV1McpTokens","tags":["mcp","mcp-tokens"],"parameters":[],"description":"Erstellt einen neuen MCP-Bearer-Token. Der Plaintext-Wert wird NUR EINMAL im Response zurueckgegeben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":120},"expires_in_days":{"type":"integer","exclusiveMinimum":0,"maximum":365,"default":90},"scopes":{"type":"array","items":{"type":"string"}}}},"example":{"label":"string","expires_in_days":1,"scopes":["string"]}}}},"summary":"Erstellt einen neuen MCP-Bearer-Token","x-nemix-summary-source":"description:first-sentence"},"get":{"responses":{"200":{"description":"Liste eigener Tokens","content":{"application/json":{"schema":{"type":"object","properties":{"tokens":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":["string","null"]},"scopes":{"type":"string","description":"Spaltenwert roh — im Altbestand auch \"mcp:read+write\""},"umfaenge":{"type":"array","items":{"type":"string","enum":["mcp:read","mcp:write","mcp:build","mcp:admin"]},"description":"Derselbe Wert ausgewertet, wie der MCP-Server ihn liest"},"lastUsedAt":{"type":["string","null"]},"expiresAt":{"type":["string","null"]},"revokedAt":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"status":{"type":"string","enum":["active","revoked","expired"]}},"required":["id","label","scopes","umfaenge","lastUsedAt","expiresAt","revokedAt","createdAt","status"]}}},"required":["tokens"]},"example":{"tokens":[{"id":"string","label":"string","scopes":"string","umfaenge":["mcp:read"],"lastUsedAt":"string","expiresAt":"string","revokedAt":"string","createdAt":"string","status":"active"}]}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getApiV1McpTokens","tags":["mcp","mcp-tokens"],"parameters":[],"summary":"Listet die eigenen MCP-Tokens ohne Klartext","description":"Listet die eigenen MCP-Tokens (ohne Plaintext, nur Metadaten + last_used_at + expires_at)."}},"/api/v1/mcp/tokens/{id}":{"delete":{"responses":{"200":{"description":"Token widerrufen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Token nicht gefunden oder bereits widerrufen"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"deleteApiV1McpTokensById","tags":["mcp","mcp-tokens"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Widerruft (revoked_at = NOW) einen MCP-Token. Token bleibt in der DB als Audit-Spur.","summary":"Widerruft (revoked_at = NOW) einen MCP-Token","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/mcp/verbindungen/anfrage/{id}":{"get":{"responses":{"200":{"description":"Offene Freigabe","content":{"application/json":{"schema":{"type":"object","properties":{"anfrage":{"type":"string"},"client":{"type":"object","properties":{"name":{"type":"string"},"weiterleitungsHost":{"type":"string"},"clientUri":{"type":["string","null"]}},"required":["name","weiterleitungsHost","clientUri"]},"umfaenge":{"type":"array","items":{"type":"string","enum":["mcp:read","mcp:write","mcp:build","mcp:admin"]}},"mandanten":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"nummer":{"type":["string","null"]},"heimat":{"type":"boolean"}},"required":["id","name","nummer","heimat"]}},"vorauswahl":{"type":["string","null"]},"laeuftAb":{"type":"string"}},"required":["anfrage","client","umfaenge","mandanten","vorauswahl","laeuftAb"]},"example":{"anfrage":"string","client":{"name":"string","weiterleitungsHost":"string","clientUri":"string"},"umfaenge":["mcp:read"],"mandanten":[{"id":"string","name":"string","nummer":"string","heimat":true}],"vorauswahl":"string","laeuftAb":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Unbekannt oder fremd"},"410":{"description":"Abgelaufen oder schon entschieden"}},"operationId":"getApiV1McpVerbindungenAnfrageById","tags":["mcp"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine offene MCP-Freigabe lesen","description":"Liefert, was die Zustimmungsseite zeigt: welche Anwendung, wohin der Code geht (Host der Weiterleitung), welche Umfaenge sie anfragt und welche Mandanten waehlbar sind — Heimat-Mandant und ausdrueckliche Zuteilungen, nicht die Super-Admin-Pauschale. Nur fuer den Nutzer, der die Freigabe begonnen hat (sonst 404); abgelaufen oder entschieden: 410. Gueltig 10 Minuten."},"post":{"responses":{"200":{"description":"Wohin es weitergeht","content":{"application/json":{"schema":{"type":"object","properties":{"weiter":{"type":"string"}},"required":["weiter"]},"example":{"weiter":"string"}}}},"400":{"description":"Rumpf ungueltig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Fremder Ursprung oder Mandant nicht waehlbar"},"404":{"description":"Unbekannt oder fremd"},"410":{"description":"Abgelaufen oder schon entschieden"}},"operationId":"postApiV1McpVerbindungenAnfrageById","tags":["mcp"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine MCP-Freigabe entscheiden","description":"Erlauben: der gewaehlte Mandant muss einer der waehlbaren sein (sonst 403), die Umfaenge koennen gegenueber der Anfrage nur VERENGT werden, Lesen bleibt immer dabei. Die Antwort nennt die Adresse, zu der die Seite den Browser schickt — mit Code, `state` und `iss`. Ablehnen: dieselbe Adresse mit `error=access_denied`. Jede Freigabe laesst sich genau einmal entscheiden (sonst 410). Nur aus der eigenen Web-Adresse (Ursprung geprueft, sonst 403).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entscheidung":{"type":"string","enum":["erlauben","ablehnen"]},"mandant":{"type":"string","minLength":1,"maxLength":64},"umfaenge":{"type":"array","items":{"type":"string","enum":["mcp:read","mcp:write","mcp:build","mcp:admin"]},"maxItems":4}},"required":["entscheidung"]},"example":{"entscheidung":"erlauben","mandant":"string","umfaenge":["mcp:read"]}}}}}},"/api/v1/mcp/verbindungen":{"get":{"responses":{"200":{"description":"Verbindungen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"familie":{"type":"string"},"client":{"type":"string"},"mandant":{"type":["string","null"]},"umfaenge":{"type":"array","items":{"type":"string","enum":["mcp:read","mcp:write","mcp:build","mcp:admin"]}},"verbundenSeit":{"type":"string"},"zuletztBenutzt":{"type":["string","null"]},"laeuftAb":{"type":["string","null"]}},"required":["familie","client","mandant","umfaenge","verbundenSeit","zuletztBenutzt","laeuftAb"]}}},"required":["data"]},"example":{"data":[{"familie":"string","client":"string","mandant":"string","umfaenge":["mcp:read"],"verbundenSeit":"string","zuletztBenutzt":"string","laeuftAb":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1McpVerbindungen","tags":["mcp"],"parameters":[],"summary":"Eigene MCP-Verbindungen auflisten","description":"Alle per OAuth verbundenen Anwendungen des angemeldeten Nutzers, die noch gelten — mit Mandant, Umfaengen, seit wann, zuletzt benutzt und wann die Verbindung ohne Nutzung ablaeuft. Die kopierten `mcp_`-Tokens stehen weiter unter /api/v1/mcp/tokens."}},"/api/v1/mcp/verbindungen/{familie}":{"delete":{"responses":{"200":{"description":"Getrennt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Unbekannt, fremd oder schon getrennt"}},"operationId":"deleteApiV1McpVerbindungenByFamilie","tags":["mcp"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"familie","required":true}],"summary":"Eine MCP-Verbindung trennen","description":"Widerruft alle Tokens dieser Verbindung sofort. Die Anwendung muss sich danach neu anmelden und neu freigegeben werden. Nur eigene Verbindungen (sonst 404)."}},"/api/v1/flags/eval":{"get":{"responses":{"200":{"description":"Flag decisions, keyed by flag name — one entry per requested name.","content":{"application/json":{"schema":{"type":"object","properties":{"decisions":{"type":"object","additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"boolean"},"reason":{"type":"string","enum":["disabled","rollout-in","rollout-out","targeted","default-on","unknown"]}},"required":["name","value","reason"],"additionalProperties":false}}},"required":["decisions"],"additionalProperties":false},"example":{"decisions":{"beispiel":{"name":"string","value":true,"reason":"disabled"}}}}}},"400":{"description":"Missing tenantId"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1FlagsEval","tags":["flags"],"parameters":[],"description":"Evaluate one or more feature flags for the current tenant context. `names` is a comma-separated list; an empty or missing list short-circuits to `{ decisions: {} }` WITHOUT requiring a tenant. The context comes from the auth middleware (`tenantId`, `userId`); `plan`, `industry` and `region` are read from the query string and feed the targeting rules. Rollout is bucketed per tenant, not per request, so repeated calls for the same tenant return the same decision. Each decision carries a `reason` — a `false` with reason `unknown` means the flag does not exist.","summary":"Evaluate one or more feature flags for the current tenant context","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/flags/stream":{"get":{"responses":{"200":{"description":"An endless `text/event-stream`, NOT a JSON document. Every event is one `data:` line holding the JSON below. The first event is always `flag.snapshot` with the decisions for every flag in the store; after that, `flag.set` and `flag.delete` arrive as flags change. The schema describes ONE event payload.","content":{"text/event-stream":{"schema":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"flag.snapshot"},"decisions":{"type":"object","additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"boolean"},"reason":{"type":"string","enum":["disabled","rollout-in","rollout-out","targeted","default-on","unknown"]}},"required":["name","value","reason"],"additionalProperties":false}}},"required":["type","decisions"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"flag.set"},"flag":{"type":"object","properties":{"name":{"type":"string"},"decision":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"boolean"},"reason":{"type":"string","enum":["disabled","rollout-in","rollout-out","targeted","default-on","unknown"]}},"required":["name","value","reason"],"additionalProperties":false}},"required":["name","decision"],"additionalProperties":false}},"required":["type","flag"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"flag.delete"},"name":{"type":"string"}},"required":["type","name"],"additionalProperties":false}]}}}},"400":{"description":"Missing tenantId"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1FlagsStream","tags":["flags"],"parameters":[],"summary":"Streams live flag updates as server-sent events","description":"SSE stream of live flag updates — initial snapshot followed by incremental events."}},"/api/v1/support/messages":{"get":{"responses":{"200":{"description":"Nachrichten des eigenen Fadens plus Serverzeit","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","description":"Fortlaufende Kennung der Nachricht"},"conversation_id":{"type":"string","description":"Gespraechsfaden; ohne Vorgabe `conv-<Anwenderkennung>`"},"user_id":{"type":"string","description":"Anwender, dem der Faden gehoert; `anon` ohne Anwenderkontext"},"role":{"type":"string","enum":["user","agent","system","bot"],"description":"Wer geschrieben hat. Ueber diese Route angelegte Nachrichten haben immer `user`"},"body":{"type":"string","description":"Nachrichtentext"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Dateiname"},"url":{"type":"string","description":"Adresse der Datei"},"size":{"type":"number","description":"Groesse in Byte, sofern uebermittelt"}},"required":["name","url"]},"description":"Anhaenge; leere Liste wenn keine. Der Inhalt wird UNGEPRUEFT uebernommen"},"meta":{"type":"object","additionalProperties":{},"description":"Zusatzangaben; enthaelt mindestens `tenant_id`"},"zendesk_ticket_id":{"type":["string","null"],"description":"Ticketnummer des erzeugten Tickets (Feldname historisch); null oder fehlend, wenn keins entstand"},"read_at":{"type":["string","null"],"description":"Zeitpunkt des Lesens; bei eigenen Nachrichten (role=user) sofort gesetzt"},"created_at":{"type":"string","description":"Anlagezeitpunkt"}},"required":["id","conversation_id","user_id","role","body","attachments","meta","created_at"]},"description":"Die Nachrichten des eigenen Fadens, aelteste zuerst"},"server_time":{"type":"integer","description":"Serverzeit in Millisekunden — als `since` des naechsten Abrufs zu verwenden"}},"required":["messages","server_time"]},"example":{"messages":[{"id":0,"conversation_id":"string","user_id":"string","role":"user","body":"string","attachments":[{"name":"string","url":"string","size":0}],"meta":{},"zendesk_ticket_id":"string","read_at":"string","created_at":"string"}],"server_time":0}}}},"400":{"description":"Ungueltige since-Parameter"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1SupportMessages","tags":["support"],"parameters":[],"summary":"Nachrichten des eigenen Support-Fadens lesen","description":"Liefert die Nachrichten des EIGENEN Fadens (Mandant und Anwender aus dem Auth-Kontext), aelteste zuerst. `since` ist ein Unix-Zeitstempel in MILLISEKUNDEN und liefert nur Neueres; ohne ihn kommt der ganze Verlauf. Es wird nicht geblaettert. `server_time` gehoert in das `since` des naechsten Aufrufs — die Route wartet selbst NICHT auf neue Nachrichten, das Nachfassen macht der Aufrufer."},"post":{"responses":{"201":{"description":"Nachricht erstellt — mit Ticketnummer, oder null wenn kein Ticket entstand","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"object","properties":{"id":{"type":"integer","description":"Fortlaufende Kennung der Nachricht"},"conversation_id":{"type":"string","description":"Gespraechsfaden; ohne Vorgabe `conv-<Anwenderkennung>`"},"user_id":{"type":"string","description":"Anwender, dem der Faden gehoert; `anon` ohne Anwenderkontext"},"role":{"type":"string","enum":["user","agent","system","bot"],"description":"Wer geschrieben hat. Ueber diese Route angelegte Nachrichten haben immer `user`"},"body":{"type":"string","description":"Nachrichtentext"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Dateiname"},"url":{"type":"string","description":"Adresse der Datei"},"size":{"type":"number","description":"Groesse in Byte, sofern uebermittelt"}},"required":["name","url"]},"description":"Anhaenge; leere Liste wenn keine. Der Inhalt wird UNGEPRUEFT uebernommen"},"meta":{"type":"object","additionalProperties":{},"description":"Zusatzangaben; enthaelt mindestens `tenant_id`"},"zendesk_ticket_id":{"type":["string","null"],"description":"Ticketnummer des erzeugten Tickets (Feldname historisch); null oder fehlend, wenn keins entstand"},"read_at":{"type":["string","null"],"description":"Zeitpunkt des Lesens; bei eigenen Nachrichten (role=user) sofort gesetzt"},"created_at":{"type":"string","description":"Anlagezeitpunkt"}},"required":["id","conversation_id","user_id","role","body","attachments","meta","created_at"],"description":"Die angelegte Nachricht"},"ticket_number":{"type":["string","null"],"description":"Nummer des erzeugten Tickets (TKT-JJJJ-NNNN); null, wenn die Ticketanlage scheiterte — die Nachricht ist trotzdem angelegt"}},"required":["message","ticket_number"]},"example":{"message":{"id":0,"conversation_id":"string","user_id":"string","role":"user","body":"string","attachments":[{"name":"string","url":"string","size":0}],"meta":{},"zendesk_ticket_id":"string","read_at":"string","created_at":"string"},"ticket_number":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"422":{"description":"Body oder Attachments erforderlich / zu lang"}},"operationId":"postApiV1SupportMessages","tags":["support"],"parameters":[],"description":"Haengt eine Nachricht mit `role: \"user\"` an den eigenen Faden und legt dazu ein Ticket im Ticketsystem des Mandanten an (`TKT-JJJJ-NNNN`, Quelle `customer_portal`, Kategorie `general`; der Titel ist der auf 120 Zeichen gekuerzte Text). Die Ticketanlage ist nachrangig: scheitert sie, kommt trotzdem 201 und `ticket_number` ist null. Entweder `body` oder `attachments` muss gefuellt sein, `body` hoechstens 4000 Zeichen — sonst 422. Anhaenge werden UNGEPRUEFT uebernommen, es findet kein Upload statt. `conversation_id` ist freiwillig; ohne sie schreibt die Nachricht in den Standardfaden des Anwenders.","summary":"Haengt eine Nachricht mit `role","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/support/messages/{id}/read":{"post":{"responses":{"200":{"description":"Immer `{ ok: true }` — auch wenn keine Nachricht getroffen wurde","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"400":{"description":"Ungueltige ID"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1SupportMessagesByIdRead","tags":["support"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt `read_at` auf jetzt. Markiert wird nur innerhalb des eigenen Fadens (Mandant und Anwender aus dem Auth-Kontext). ACHTUNG: eine unbekannte oder fremde Kennung ergibt KEIN 404 — die Antwort ist auch dann `{ ok: true }`, obwohl nichts geaendert wurde. Nur eine nicht-numerische Kennung ergibt 400.","summary":"Setzt `read_at` auf jetzt","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/service-visits":{"get":{"responses":{"200":{"description":"Die gefundenen Besuche","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Besuchs (UUID)"},"customer_id":{"type":"string","format":"uuid","description":"Kunde, bei dem der Besuch stattfindet"},"employee_id":{"type":"string","format":"uuid","description":"Mitarbeiter, der faehrt"},"started_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Beginn der Anfahrt, Form `YYYY-MM-DDTHH:MM:SS.mmm±HH` — Zonenversatz nur mit Stunden, kein RFC 3339"},"ended_at":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Ende des Besuchs, gleiche Form; `null`, solange der Besuch laeuft"},"route_geo":{"type":["object","null"],"properties":{"type":{"type":"string","const":"LineString","description":"GeoJSON-Typ — immer `LineString`"},"coordinates":{"type":"array","items":{"type":"array","maxItems":2,"minItems":2,"prefixItems":[{"type":"number"},{"type":"number"}]},"description":"Stuetzpunkte als [Laengengrad, Breitengrad] in WGS 84 (EPSG:4326) — GeoJSON-Reihenfolge, nicht lat/lon"}},"required":["type","coordinates"],"additionalProperties":false,"description":"Die gefahrene Strecke; `null`, solange keine oder eine Strecke mit weniger als zwei Punkten vorliegt"},"route_length_m":{"type":["number","null"],"minimum":0,"description":"Streckenlaenge in Metern, serverseitig aus `route_geo` gerechnet; `null` ohne Strecke"},"notes":{"type":["string","null"],"maxLength":2000,"description":"Freitext zum Besuch; `null`, wenn keiner erfasst ist"},"created_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Anlagezeitpunkt, gleiche Form wie `started_at`"},"updated_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Letzte Aenderung, gleiche Form wie `started_at`"}},"required":["id","customer_id","employee_id","started_at","ended_at","route_geo","route_length_m","notes","created_at","updated_at"],"additionalProperties":false},"description":"Die Besuche, neueste zuerst"},"count":{"type":"integer","minimum":0,"description":"Laenge von `items` — NICHT die Gesamtzahl. `limit`/`offset` werden hier nicht zurueckgemeldet"}},"required":["items","count"],"additionalProperties":false},"example":{"items":[],"count":0}}}},"400":{"description":"Query-Parameter abgelehnt (rohes Zod-Ergebnis). Ein fehlender Mandantenkontext antwortet unter demselben Code mit Klartext `tenant context missing`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"503":{"description":"Kein Datenbank-Client vorhanden — Klartext, kein JSON"}},"operationId":"getApiV1Service-visits","tags":["service-visits"],"parameters":[{"in":"query","name":"customer_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"employee_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":500}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0}}],"summary":"Service-Visits auflisten","description":"Listet Servicebesuche, wahlweise auf einen Kunden oder einen Mitarbeiter eingeschraenkt. Fehlt die Tabelle im Mandantenschema, wird sie beim Lesen angelegt; scheitert das, antwortet die Route mit einer LEEREN Liste und 200 statt mit einem Fehler — ein leeres Ergebnis heisst hier also nicht zwingend „keine Besuche\"."},"post":{"responses":{"201":{"description":"Der angelegte Besuch","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Besuchs (UUID)"},"customer_id":{"type":"string","format":"uuid","description":"Kunde, bei dem der Besuch stattfindet"},"employee_id":{"type":"string","format":"uuid","description":"Mitarbeiter, der faehrt"},"started_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Beginn der Anfahrt, Form `YYYY-MM-DDTHH:MM:SS.mmm±HH` — Zonenversatz nur mit Stunden, kein RFC 3339"},"ended_at":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Ende des Besuchs, gleiche Form; `null`, solange der Besuch laeuft"},"route_geo":{"type":["object","null"],"properties":{"type":{"type":"string","const":"LineString","description":"GeoJSON-Typ — immer `LineString`"},"coordinates":{"type":"array","items":{"type":"array","maxItems":2,"minItems":2,"prefixItems":[{"type":"number"},{"type":"number"}]},"description":"Stuetzpunkte als [Laengengrad, Breitengrad] in WGS 84 (EPSG:4326) — GeoJSON-Reihenfolge, nicht lat/lon"}},"required":["type","coordinates"],"additionalProperties":false,"description":"Die gefahrene Strecke; `null`, solange keine oder eine Strecke mit weniger als zwei Punkten vorliegt"},"route_length_m":{"type":["number","null"],"minimum":0,"description":"Streckenlaenge in Metern, serverseitig aus `route_geo` gerechnet; `null` ohne Strecke"},"notes":{"type":["string","null"],"maxLength":2000,"description":"Freitext zum Besuch; `null`, wenn keiner erfasst ist"},"created_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Anlagezeitpunkt, gleiche Form wie `started_at`"},"updated_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Letzte Aenderung, gleiche Form wie `started_at`"}},"required":["id","customer_id","employee_id","started_at","ended_at","route_geo","route_length_m","notes","created_at","updated_at"],"additionalProperties":false}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis). Ein fehlender Mandantenkontext antwortet unter demselben Code mit Klartext `tenant context missing`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"503":{"description":"Kein Datenbank-Client vorhanden — Klartext, kein JSON"}},"operationId":"postApiV1Service-visits","tags":["service-visits"],"parameters":[],"summary":"Service-Visit starten","description":"Legt einen Servicebesuch an („Anfahrt starten\") und gibt ihn OHNE Umschlag zurueck. Ohne `started_at` setzt die Datenbank den aktuellen Zeitpunkt. `route_geo` und `route_length_m` sind beim Start immer `null` — die Strecke kommt spaeter per PATCH dazu.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid"},"employee_id":{"type":"string","format":"uuid"},"started_at":{"type":"string","format":"date-time"},"notes":{"type":"string","maxLength":2000}},"required":["customer_id","employee_id"]},"example":{"customer_id":"00000000-0000-4000-8000-000000000000","employee_id":"00000000-0000-4000-8000-000000000000","started_at":"2026-01-01T12:00:00.000Z","notes":"string"}}}}}},"/api/v1/service-visits/{id}":{"get":{"responses":{"200":{"description":"Der Besuch samt Route","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Besuchs (UUID)"},"customer_id":{"type":"string","format":"uuid","description":"Kunde, bei dem der Besuch stattfindet"},"employee_id":{"type":"string","format":"uuid","description":"Mitarbeiter, der faehrt"},"started_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Beginn der Anfahrt, Form `YYYY-MM-DDTHH:MM:SS.mmm±HH` — Zonenversatz nur mit Stunden, kein RFC 3339"},"ended_at":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Ende des Besuchs, gleiche Form; `null`, solange der Besuch laeuft"},"route_geo":{"type":["object","null"],"properties":{"type":{"type":"string","const":"LineString","description":"GeoJSON-Typ — immer `LineString`"},"coordinates":{"type":"array","items":{"type":"array","maxItems":2,"minItems":2,"prefixItems":[{"type":"number"},{"type":"number"}]},"description":"Stuetzpunkte als [Laengengrad, Breitengrad] in WGS 84 (EPSG:4326) — GeoJSON-Reihenfolge, nicht lat/lon"}},"required":["type","coordinates"],"additionalProperties":false,"description":"Die gefahrene Strecke; `null`, solange keine oder eine Strecke mit weniger als zwei Punkten vorliegt"},"route_length_m":{"type":["number","null"],"minimum":0,"description":"Streckenlaenge in Metern, serverseitig aus `route_geo` gerechnet; `null` ohne Strecke"},"notes":{"type":["string","null"],"maxLength":2000,"description":"Freitext zum Besuch; `null`, wenn keiner erfasst ist"},"created_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Anlagezeitpunkt, gleiche Form wie `started_at`"},"updated_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Letzte Aenderung, gleiche Form wie `started_at`"}},"required":["id","customer_id","employee_id","started_at","ended_at","route_geo","route_length_m","notes","created_at","updated_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","customer_id":"00000000-0000-4000-8000-000000000001","employee_id":"00000000-0000-4000-8000-000000000002","started_at":"2026-01-01T08:00:00.000+01","ended_at":"2026-01-01T10:30:00.000+01","route_geo":{"type":"LineString","coordinates":[[13.405,52.52],[13.42,52.53]]},"route_length_m":1520.4,"notes":"Wartung Heizungsanlage","created_at":"2026-01-01T08:00:00.000+01","updated_at":"2026-01-01T10:30:00.000+01"}}}},"400":{"description":"Kein Mandantenkontext — Klartext `tenant context missing`, kein JSON"},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Besuch mit dieser Kennung — Klartext `service_visit not found`, kein JSON"},"503":{"description":"Kein Datenbank-Client vorhanden — Klartext, kein JSON"}},"operationId":"getApiV1Service-visitsById","tags":["service-visits"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Service-Visit abrufen","description":"Liefert einen Servicebesuch samt GeoJSON-Route OHNE Umschlag — die Felder stehen direkt im Wurzelobjekt. Die Strecke kommt als `LineString` in WGS 84 zurueck, nicht als Rohgeometrie."},"patch":{"responses":{"200":{"description":"Der Besuch nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Besuchs (UUID)"},"customer_id":{"type":"string","format":"uuid","description":"Kunde, bei dem der Besuch stattfindet"},"employee_id":{"type":"string","format":"uuid","description":"Mitarbeiter, der faehrt"},"started_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Beginn der Anfahrt, Form `YYYY-MM-DDTHH:MM:SS.mmm±HH` — Zonenversatz nur mit Stunden, kein RFC 3339"},"ended_at":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Ende des Besuchs, gleiche Form; `null`, solange der Besuch laeuft"},"route_geo":{"type":["object","null"],"properties":{"type":{"type":"string","const":"LineString","description":"GeoJSON-Typ — immer `LineString`"},"coordinates":{"type":"array","items":{"type":"array","maxItems":2,"minItems":2,"prefixItems":[{"type":"number"},{"type":"number"}]},"description":"Stuetzpunkte als [Laengengrad, Breitengrad] in WGS 84 (EPSG:4326) — GeoJSON-Reihenfolge, nicht lat/lon"}},"required":["type","coordinates"],"additionalProperties":false,"description":"Die gefahrene Strecke; `null`, solange keine oder eine Strecke mit weniger als zwei Punkten vorliegt"},"route_length_m":{"type":["number","null"],"minimum":0,"description":"Streckenlaenge in Metern, serverseitig aus `route_geo` gerechnet; `null` ohne Strecke"},"notes":{"type":["string","null"],"maxLength":2000,"description":"Freitext zum Besuch; `null`, wenn keiner erfasst ist"},"created_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Anlagezeitpunkt, gleiche Form wie `started_at`"},"updated_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}(:\\d{2})?$","description":"Letzte Aenderung, gleiche Form wie `started_at`"}},"required":["id","customer_id","employee_id","started_at","ended_at","route_geo","route_length_m","notes","created_at","updated_at"],"additionalProperties":false}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis). Ein fehlender Mandantenkontext antwortet unter demselben Code mit Klartext `tenant context missing`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Besuch mit dieser Kennung — Klartext `service_visit not found`, kein JSON"},"503":{"description":"Kein Datenbank-Client vorhanden — Klartext, kein JSON"}},"operationId":"patchApiV1Service-visitsById","tags":["service-visits"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Service-Visit aktualisieren","description":"Aendert Strecke, Endzeitpunkt oder Notiz und gibt den Besuch OHNE Umschlag zurueck. Wird `route_geo` mitgeschickt, rechnet der Server `route_length_m` daraus neu. Eine Strecke mit WENIGER ALS ZWEI Punkten wird dabei als `null` gespeichert, nicht abgelehnt — Postgres nimmt keinen LineString aus einem Punkt an; `route_length_m` wird dann ebenfalls `null`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ended_at":{"type":"string","format":"date-time"},"route_geo":{"type":["object","null"],"properties":{"type":{"type":"string","const":"LineString"},"coordinates":{"type":"array","items":{"type":"array","maxItems":2,"minItems":2,"prefixItems":[{"type":"number"},{"type":"number"}]}}},"required":["type","coordinates"]},"notes":{"type":"string","maxLength":2000}}},"example":{"ended_at":"2026-01-01T12:00:00.000Z","route_geo":{"type":"LineString","coordinates":[]},"notes":"string"}}}}},"delete":{"responses":{"204":{"description":"Der Besuch ist entfernt. Kein Rumpf — die Antwort ist leer"},"400":{"description":"Kein Mandantenkontext — Klartext `tenant context missing`, kein JSON"},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `admin`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `admin`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Besuch mit dieser Kennung — Klartext `service_visit not found`, kein JSON"},"503":{"description":"Kein Datenbank-Client vorhanden — Klartext, kein JSON"}},"operationId":"deleteApiV1Service-visitsById","tags":["service-visits"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Service-Visit löschen","description":"Entfernt den Servicebesuch samt seiner GeoJSON-Route endgueltig — es gibt hier KEIN `deleted_at` und damit keinen Weg zurueck. Nur fuer die Rolle `admin`."}},"/api/v1/system/status":{"get":{"responses":{"200":{"description":"Momentaufnahme; einzelne Felder koennen \"down\" melden","content":{"application/json":{"schema":{"type":"object","properties":{"ai":{"type":"string","enum":["healthy","degraded","down"],"description":"Zustand des KI-Zugangs, gelesen aus dem Sicherungsschalter — Anthropic wird nicht neu angefragt"},"db":{"type":"string","enum":["healthy","down"],"description":"Ergebnis eines SELECT 1 gegen die Datenbank"},"redis":{"type":"string","enum":["healthy","down"],"description":"Ergebnis eines PING; ohne gesetztes REDIS_URL gilt der Zwischenspeicher als healthy"},"retry_after":{"type":"integer","minimum":0,"description":"Nur wenn ai=down: empfohlene Wartezeit in Sekunden bis zum naechsten KI-Versuch"},"status_banner":{"type":["string","null"],"description":"Text fuer das Hinweisband der Oberflaeche; null solange der KI-Zugang nicht down ist"}},"required":["ai","db","redis","status_banner"]},"example":{"ai":"healthy","db":"healthy","redis":"healthy","retry_after":0,"status_banner":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1SystemStatus","tags":["system"],"parameters":[],"summary":"Momentaufnahme von Datenbank, Zwischenspeicher und KI-Zustand","description":"Pingt Datenbank und Zwischenspeicher parallel mit je 500 ms Zeitgrenze und liest den KI-Zustand aus dem Sicherungsschalter, ohne Anthropic erneut anzufragen. Antwortet auch dann mit 200, wenn einzelne Teile down sind — der Zustand steht im Rumpf, nicht im Status. Ohne gesetztes REDIS_URL zaehlt der Zwischenspeicher als healthy."}},"/api/v1/settings/tax-rate":{"get":{"responses":{"200":{"description":"Effektiver Steuersatz. `source` sagt, ob er vom Mandanten gepflegt ist (tenant) oder eine Vorgabe greift (env, fallback).","content":{"application/json":{"schema":{"type":"object","properties":{"rate":{"type":"number"},"percent":{"type":"number"},"source":{"type":"string","enum":["tenant","env","fallback"]}},"required":["rate","percent","source"],"additionalProperties":false},"example":{"rate":0,"percent":0,"source":"tenant"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1SettingsTax-rate","tags":["settings"],"parameters":[],"summary":"Get default VAT rate","description":"Liefert den effektiven Default-Steuersatz für den Mandanten. Die Route rät nie stillschweigend: source sagt, ob der Satz gepflegt ist (tenant) oder eine Vorgabe greift (env, fallback = 0,19)."}},"/api/v1/settings/approval-thresholds":{"get":{"responses":{"200":{"description":"Effective approval limits. `source` distinguishes a maintained limit from a product default and from a database where the columns do not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"singleEyeEur":{"type":"number"},"twoEyesEur":{"type":"number"},"fourEyesEur":{"type":"number"},"source":{"type":"string","enum":["tenant","mixed","default","not-provisioned"]},"maintained":{"type":"object","properties":{"singleEye":{"type":"boolean"},"twoEyes":{"type":"boolean"},"fourEyes":{"type":"boolean"}},"required":["singleEye","twoEyes","fourEyes"],"additionalProperties":false}},"required":["singleEyeEur","twoEyesEur","fourEyesEur","source","maintained"],"additionalProperties":false},"example":{"singleEyeEur":0,"twoEyesEur":0,"fourEyesEur":0,"source":"tenant","maintained":{"singleEye":true,"twoEyes":true,"fourEyes":true}}}}},"401":{"description":"Unauthorized"},"404":{"description":"Tenant row not found"},"500":{"description":"Query failed — the message carries the real cause"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1SettingsApproval-thresholds","tags":["settings"],"parameters":[],"summary":"Get approval thresholds","description":"The tenant's approval limits: below singleEyeEur a document needs no approval, at or above twoEyesEur two approvers are required, at or above fourEyesEur four. The three numbers are always the values the approval workflow would really apply; `source` says whether the tenant chose them (tenant/mixed) or they are product defaults (default), and `not-provisioned` means the columns are absent from this database, so nothing was ever configurable. `maintained` reports the same per limit."}},"/api/v1/settings/company":{"get":{"responses":{"200":{"description":"Firmenprofil. Nicht gepflegte Textfelder kommen als Leerstring, nicht als null (Ausnahme: logoUrl). `vatId` ist der Alt-Alias von `taxId`. Fehlt die Organisationszeile, sind legalForm und country VORGABEN (GmbH / Deutschland), keine gepflegten Werte.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"legalForm":{"type":"string"},"taxId":{"type":"string"},"vatId":{"type":"string"},"registerNo":{"type":"string"},"street":{"type":"string"},"zip":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"website":{"type":"string"},"logoUrl":{"type":["string","null"]}},"required":["name","legalForm","taxId","vatId","registerNo","street","zip","city","country","email","phone","website","logoUrl"],"additionalProperties":false},"example":{"name":"string","legalForm":"string","taxId":"string","vatId":"string","registerNo":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","website":"string","logoUrl":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1SettingsCompany","tags":["settings"],"parameters":[],"summary":"Get company profile","description":"Firmenprofil des Mandanten. Fehlt die Organisationszeile, sind legalForm (GmbH) und country (Deutschland) VORGABEN — nichts an der Antwort unterscheidet sie von gepflegten Werten. Nicht gefüllte Textfelder kommen als Leerstring, nur logoUrl kann null sein. vatId ist der Alt-Alias von taxId und liest dieselbe Spalte."},"patch":{"responses":{"200":{"description":"Das geaenderte Firmenprofil — dieselben Felder wie GET, aber OHNE `logoUrl`","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"legalForm":{"type":"string"},"taxId":{"type":"string"},"vatId":{"type":"string"},"registerNo":{"type":"string"},"street":{"type":"string"},"zip":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"website":{"type":"string"}},"required":["name","legalForm","taxId","vatId","registerNo","street","zip","city","country","email","phone","website"],"additionalProperties":false},"example":{"name":"string","legalForm":"string","taxId":"string","vatId":"string","registerNo":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","website":"string"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"404":{"description":"Not Found"}},"operationId":"patchApiV1SettingsCompany","tags":["settings"],"parameters":[],"summary":"Update company profile","description":"Firmenprofil aktualisieren; nur gesendete Felder werden geschrieben. Erfordert Rolle admin oder höher. Zwei Eigenheiten: legalForm und country liegen in der Tabelle organizations — schlägt dieses Schreiben fehl (Tabelle fehlt), wird es stillschweigend übergangen und die Antwort ist trotzdem 200 mit der Vorgabe. Die Antwort enthält im Gegensatz zu GET /company KEIN logoUrl.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"legalForm":{"type":"string","maxLength":32},"taxId":{"type":"string","maxLength":64},"vatId":{"type":"string","maxLength":64},"registerNo":{"type":"string","maxLength":64},"street":{"type":"string","maxLength":255},"zip":{"type":"string","maxLength":20},"city":{"type":"string","maxLength":255},"country":{"type":"string","maxLength":100},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"string","const":""}]},"phone":{"type":"string","maxLength":64},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]}}},"example":{"name":"string","legalForm":"string","taxId":"string","vatId":"string","registerNo":"string","street":"string","zip":"string","city":"string","country":"string","email":"beispiel@example.com","phone":"string","website":"https://example.com"}}}}}},"/api/v1/settings/users":{"get":{"responses":{"200":{"description":"Alle Nutzer des Mandanten, aelteste zuerst — auch die entfernten","content":{"application/json":{"schema":{"type":"object","properties":{"users":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"name":{"type":"string","description":"Leerstring, wenn kein Name hinterlegt ist — nie null"},"role":{"type":"string"},"status":{"type":"string","const":"active","description":"Fester Wert, keine gemessene Eigenschaft"},"invitedAt":{"type":"string","description":"In Wahrheit `created_at` der Nutzerzeile"},"lastActiveAt":{"type":"string","description":"In Wahrheit `updated_at` der Nutzerzeile"}},"required":["id","email","name","role","status","invitedAt","lastActiveAt"],"additionalProperties":false}}},"required":["users"],"additionalProperties":false},"example":{"users":[{"id":"string","email":"string","name":"string","role":"string","status":"active","invitedAt":"string","lastActiveAt":"string"}]}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1SettingsUsers","tags":["settings"],"parameters":[],"summary":"List tenant users","description":"Alle Nutzer des Mandanten mit E-Mail und Rolle; eine Rollenprüfung findet nicht statt, eine Anmeldung genügt. Drei Felder sind nicht, was ihr Name verspricht: status ist fest \"active\", invitedAt ist created_at und lastActiveAt ist updated_at der Nutzerzeile. Entfernte Nutzer verschwinden hier NICHT — die Abfrage filtert deleted_at nicht."}},"/api/v1/settings/users/invite":{"post":{"responses":{"201":{"description":"Quittung ohne Wirkung — der Sitzplatz war frei. Es wurde weder ein Nutzer angelegt noch eine E-Mail versendet.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"invited":{"type":"boolean","const":true,"description":"Fester Wert — es wurde nichts versendet"},"currentCount":{"type":"integer","description":"Belegte Sitzplaetze VOR dieser Einladung"},"maxUsers":{"type":"integer","description":"Sitzplatz-Obergrenze des gebuchten Plans"},"overage":{"type":"boolean","description":"True, wenn der Platz ueber den Plan hinaus ginge"},"message":{"type":"string"}},"required":["ok","invited","currentCount","maxUsers","overage","message"],"additionalProperties":false},"example":{"ok":true,"invited":true,"currentCount":0,"maxUsers":0,"overage":true,"message":"string"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"402":{"description":"Seat-Limit erreicht — Upgrade nötig"}},"operationId":"postApiV1SettingsUsersInvite","tags":["settings"],"parameters":[],"summary":"Invite user — checks the seat cap, sends NO invitation","description":"Prüft das Sitzplatz-Limit des gebuchten Plans. Mehr passiert nicht: es wird KEIN Nutzer angelegt und KEINE E-Mail versendet — die Anbindung an Better Auth fehlt noch. Die 201-Antwort ist eine Quittung ohne Wirkung (\"Einladung wird implementiert\"). Ist das Limit erreicht, kommt 402 mit currentCount, maxUsers und upgradeUrl. Der Stripe-Haken für Zusatzlizenzen wird im Betrieb nirgends registriert, es wird also auch nichts nachberechnet. Erfordert Rolle admin.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["admin","member","viewer"]}},"required":["email","role"]},"example":{"email":"beispiel@example.com","role":"admin"}}}}}},"/api/v1/settings/users/{id}":{"patch":{"responses":{"200":{"description":"Quittung — sie sagt NICHT, dass eine Zeile geaendert wurde","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"404":{"description":"Not Found"}},"operationId":"patchApiV1SettingsUsersById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update user role","description":"Rolle eines Nutzers im Mandanten setzen. Erfordert Rolle admin oder höher. Die Plattform-Rollen super_admin, system und api sind hier NICHT zuweisbar (400 role_not_assignable). Die Route antwortet auch dann 200 {ok:true}, wenn zur id kein Nutzer des Mandanten gehört — sie prüft nicht, ob eine Zeile geändert wurde.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string"}}},"example":{"role":"string"}}}}},"delete":{"responses":{"200":{"description":"Nutzer entfernt — `deleted_at` gesetzt, die Zeile bleibt bestehen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Unauthorized"},"403":{"description":"Sich selbst oder den letzten Admin entfernen"},"404":{"description":"Not Found"}},"operationId":"deleteApiV1SettingsUsersById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Remove user from tenant (soft delete)","description":"Setzt deleted_at auf der Nutzerzeile (DSGVO-Karenz) — kein endgültiges Löschen, die Zeile bleibt bestehen und erscheint weiter in GET /settings/users. Erfordert Rolle admin oder höher. Zwei Sperren antworten 403: sich selbst entfernen und den letzten Admin des Mandanten entfernen."}},"/api/v1/settings/notifications":{"get":{"responses":{"200":{"description":"Der gespeicherte Benachrichtigungs-Block des Mandanten, unveraendert. Bewusst offen: hat der Mandant nichts gesetzt, kommt ein leeres Objekt.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}},"example":{"order.created":{"email":true,"push":false,"inapp":true},"invoice.overdue":{"email":true,"push":true,"inapp":true}}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1SettingsNotifications","tags":["settings"],"parameters":[],"summary":"Get notification settings","description":"Der gespeicherte Benachrichtigungs-Block des Mandanten, unverändert durchgereicht. Die Einstellung gilt für den ganzen Mandanten, nicht für den angemeldeten Nutzer. Hat niemand etwas gesetzt, kommt ein leeres Objekt."},"patch":{"responses":{"200":{"description":"Quittung — der Block wurde vollstaendig ersetzt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"404":{"description":"Not Found"}},"operationId":"patchApiV1SettingsNotifications","tags":["settings"],"parameters":[],"summary":"Replace notification settings","description":"Der gesendete Rumpf ERSETZT den Benachrichtigungs-Block vollständig, er wird nicht zusammengeführt — nicht gesendete Schlüssel sind danach weg. Der Rumpf wird nicht geprüft: jedes JSON-Objekt wird übernommen. Die Einstellung gilt für den ganzen Mandanten, und jede angemeldete Rolle darf sie überschreiben."}},"/api/v1/settings/logo":{"post":{"responses":{"200":{"description":"Logo gespeichert — `url` ist die data:-URI selbst, kein Verweis","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"url":{"type":"string","description":"data:<mime>;base64,… — die Datei selbst, nicht ein Verweis darauf"}},"required":["ok","url"],"additionalProperties":false},"example":{"ok":true,"url":"string"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1SettingsLogo","tags":["settings"],"parameters":[],"summary":"Upload tenant logo","description":"Logo als multipart/form-data im Feld file. Erlaubt sind PNG, JPEG, SVG und WebP bis 2 MB; alles andere antwortet 400. Gespeichert wird kein Objekt im Speicherdienst, sondern eine base64-Data-URI in tenants.settings.logoUrl — dieselbe Zeichenkette gibt die Antwort zurück und liefert später GET /settings/company. Erfordert Rolle admin oder höher."}},"/api/v1/settings/number-format":{"get":{"responses":{"200":{"description":"Muster je Belegart — immer alle vier. Leerstring heisst: kein eigenes Muster, es gilt das eingebaute Format.","content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"string"},"quote":{"type":"string"},"delivery":{"type":"string"},"invoice":{"type":"string"}},"required":["order","quote","delivery","invoice"],"additionalProperties":false},"example":{"order":"string","quote":"string","delivery":"string","invoice":"string"}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1SettingsNumber-format","tags":["settings"],"parameters":[],"summary":"Get document number patterns","description":"Belegnummern-Muster je Belegart (Aufträge/Angebote/Lieferscheine/Rechnungen). Immer alle vier Schlüssel; Leerstring heißt: kein eigenes Muster, es gilt das eingebaute Format."},"patch":{"responses":{"200":{"description":"Der zusammengefuehrte Stand aller vier Belegarten — nicht nur der gesendeten","content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"string"},"quote":{"type":"string"},"delivery":{"type":"string"},"invoice":{"type":"string"}},"required":["order","quote","delivery","invoice"],"additionalProperties":false},"example":{"order":"string","quote":"string","delivery":"string","invoice":"string"}}}},"400":{"description":"Ungültiges Muster"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"patchApiV1SettingsNumber-format","tags":["settings"],"parameters":[],"summary":"Update document number patterns","description":"Belegnummern-Muster je Belegart setzen; nicht gesendete Belegarten bleiben unverändert. Leerer String = zurücksetzen auf das eingebaute Format. Ein nicht leeres Muster muss eine laufende Nummer enthalten ({lfd:N} oder {seq:N}), sonst 400. Geändert wird NUR die Darstellung künftiger Belege: die Sequenz kommt unverändert aus number_ranges, und bereits vergebene Nummern werden nie umformatiert. Erfordert Rolle admin oder höher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"string","maxLength":120},"quote":{"type":"string","maxLength":120},"delivery":{"type":"string","maxLength":120},"invoice":{"type":"string","maxLength":120}}},"example":{"order":"string","quote":"string","delivery":"string","invoice":"string"}}}}}},"/api/v1/settings/billing":{"get":{"responses":{"200":{"description":"Der gespeicherte Abrechnungs-Block. Hat der Mandant nichts gesetzt, kommt ein leeres Objekt — die Maske zeigt dann ihren Platzhalter.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}},"example":{"defaultHourlyRate":85,"currency":"EUR"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not Found"}},"operationId":"getApiV1SettingsBilling","tags":["settings"],"parameters":[],"summary":"Get default hourly rate","description":"Der gespeicherte Abrechnungs-Block des Mandanten (defaultHourlyRate, currency). Hat niemand etwas gesetzt, kommt ein leeres Objekt — kein Stundensatz und 0 sind nicht dasselbe."},"patch":{"responses":{"200":{"description":"Der zusammengefuehrte Abrechnungs-Block — auch die Schluessel, die nicht gesendet wurden. Nicht nur das Gesendete.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Mandant nicht gefunden"}},"operationId":"patchApiV1SettingsBilling","tags":["settings"],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"defaultHourlyRate":{"type":["number","null"],"minimum":0,"maximum":100000},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"}}},"example":{"defaultHourlyRate":0}}}},"summary":"Update default hourly rate","description":"Setzt Stundensatz und/oder Währung; der bestehende Block wird zusammengeführt, wer nur die Währung schickt, verliert den Satz nicht. defaultHourlyRate: null heißt ausdrücklich \"kein Standardsatz\" und ist etwas anderes als 0. currency ist ein ISO-4217-Code und wird in Großbuchstaben gespeichert. Erfordert Rolle admin oder höher."}},"/api/v1/settings/tenant-profile":{"get":{"responses":{"200":{"description":"Profil und Sitzland; beide koennen null sein.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantNumber":{"type":["string","null"]},"name":{"type":"string"},"sitzland":{"type":["string","null"]},"profil":{"type":["object","null"],"properties":{"branchen":{"type":"array","items":{"type":"string"}},"groesse":{"type":"string"},"laender":{"type":"array","items":{"type":"string"}},"sprachen":{"type":"array","items":{"type":"string"}}},"required":["branchen","groesse","laender","sprachen"]}},"required":["tenantNumber","name","sitzland","profil"]},"example":{"tenantNumber":"string","name":"string","sitzland":"string","profil":{"branchen":["string"],"groesse":"string","laender":["string"],"sprachen":["string"]}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1SettingsTenant-profile","tags":["settings"],"parameters":[],"summary":"Get the tenant profile","description":"Branche, Groesse, aktive Laender und Sprachen des Mandanten, dazu sein Sitzland. `profil` ist null, wenn beim Anlegen keines mitkam — das ist etwas anderes als ein leeres Profil (nichts ausgewaehlt) und wird deshalb unterschieden. `sitzland` ist null nur bei Mandanten, die vor dem 10.09.2026 angelegt wurden."},"patch":{"responses":{"200":{"description":"Das gespeicherte Profil.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantNumber":{"type":["string","null"]},"name":{"type":"string"},"sitzland":{"type":["string","null"]},"profil":{"type":["object","null"],"properties":{"branchen":{"type":"array","items":{"type":"string"}},"groesse":{"type":"string"},"laender":{"type":"array","items":{"type":"string"}},"sprachen":{"type":"array","items":{"type":"string"}}},"required":["branchen","groesse","laender","sprachen"]}},"required":["tenantNumber","name","sitzland","profil"]},"example":{"tenantNumber":"string","name":"string","sitzland":"string","profil":{"branchen":["string"],"groesse":"string","laender":["string"],"sprachen":["string"]}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"patchApiV1SettingsTenant-profile","tags":["settings"],"parameters":[],"summary":"Update the tenant profile","description":"Setzt Branche, Groesse, aktive Laender und Sprachen des Mandanten. Das Profil wird als GANZES ersetzt, nicht feldweise gemischt: die vier Angaben werden zusammen ausgewaehlt, und ein Teil-Schreiben liesse einen Stand zurueck, den niemand so gewaehlt hat. Das Sitzland bleibt unberuehrt — es ist fest und hat hier keinen Eingang.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"branchen":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":200,"default":[]},"groesse":{"type":"string","maxLength":32,"default":""},"laender":{"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"maxItems":200,"default":[]},"sprachen":{"type":"array","items":{"type":"string","minLength":2,"maxLength":8},"maxItems":200,"default":[]}}},"example":{"branchen":["string"],"groesse":"string","laender":["st"],"sprachen":["string"]}}}}}},"/api/v1/modules/enabled":{"get":{"responses":{"200":{"description":"Karte Modulschluessel -> an/aus. Ohne Mandantenkontext leer. Nicht zu verwechseln mit /permissions/me/modules — das beantwortet eine andere Frage.","content":{"application/json":{"schema":{"type":"object","properties":{"modules":{"type":"object","additionalProperties":{"type":"boolean"}}},"required":["modules"],"additionalProperties":false},"example":{"modules":{"beispiel":true}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ModulesEnabled","tags":["modules"],"parameters":[],"summary":"Effektiver Zustand aller schaltbaren Module fuer diesen Mandanten","description":"Effektiver Enabled-Zustand aller gateable Module für den aktuellen Mandanten/Nutzer."}},"/api/v1/voice/status":{"get":{"responses":{"200":{"description":"Status","content":{"application/json":{"schema":{"type":"object","properties":{"configured":{"type":"boolean"},"engine":{"type":"string"},"provider":{"type":"string"},"bridgeAvailable":{"type":"boolean"},"readiness":{"type":"object","additionalProperties":{}},"useCases":{}},"required":["configured","engine","provider","bridgeAvailable","readiness"]},"example":{"configured":true,"engine":"string","provider":"string","bridgeAvailable":true,"readiness":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1VoiceStatus","tags":["voice"],"parameters":[],"summary":"Telefonie-Status: Anbieter, Engine und Erreichbarkeit der Bruecke","description":"Telefonie-Status: configured (Provider-Config nutzbar), engine (Standard), provider, bridgeAvailable (VOICE_BRIDGE_URL gesetzt), readiness (Ja/Nein je Pflicht-Secret + Namen der fehlenden) + Use-Case-Vorlagen."}},"/api/v1/voice/agents":{"get":{"responses":{"200":{"description":"Agenten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"name":{"type":"string"},"use_case":{"type":["string","null"]},"persona":{"type":["string","null"]},"system_prompt":{"type":["string","null"]},"knowledge_binding":{"type":["string","null"]},"elevenlabs_agent_id":{"type":["string","null"]},"voice_id":{"type":["string","null"]},"engine":{"type":"string"},"voice":{"type":["string","null"]},"language":{"type":"string"},"enabled":{"type":"boolean"},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","name","use_case","persona","system_prompt","knowledge_binding","elevenlabs_agent_id","voice_id","engine","voice","language","enabled","created_by","created_at","updated_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","tenant_id":"string","name":"string","use_case":"string","persona":"string","system_prompt":"string","knowledge_binding":"string","elevenlabs_agent_id":"string","voice_id":"string","engine":"string","voice":"string","language":"string","enabled":true,"created_by":"string","created_at":"string","updated_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1VoiceAgents","tags":["voice"],"parameters":[],"description":"Voice-Agenten des Mandanten. Liest public.tenant_voice_agents, neueste zuerst — ohne Blätterung und ohne Filter. Ist der Speicher nicht erreichbar, kommt eine LEERE Liste mit 200 zurück und kein Fehler. Wie jede Route dieser Datei hängt sie am Modul-Gate: ist das Voice-Modul für den Mandanten nicht freigeschaltet, antwortet sie mit 403.","summary":"Voice-Agenten des Mandanten","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"name":{"type":"string"},"use_case":{"type":["string","null"]},"persona":{"type":["string","null"]},"system_prompt":{"type":["string","null"]},"knowledge_binding":{"type":["string","null"]},"elevenlabs_agent_id":{"type":["string","null"]},"voice_id":{"type":["string","null"]},"engine":{"type":"string"},"voice":{"type":["string","null"]},"language":{"type":"string"},"enabled":{"type":"boolean"},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","name","use_case","persona","system_prompt","knowledge_binding","elevenlabs_agent_id","voice_id","engine","voice","language","enabled","created_by","created_at","updated_at"]}},"required":["data"]},"example":{"data":{"id":"string","tenant_id":"string","name":"string","use_case":"string","persona":"string","system_prompt":"string","knowledge_binding":"string","elevenlabs_agent_id":"string","voice_id":"string","engine":"string","voice":"string","language":"string","enabled":true,"created_by":"string","created_at":"string","updated_at":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Speicher nicht verfuegbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1VoiceAgents","tags":["voice"],"parameters":[],"description":"Voice-Agent anlegen. Braucht mindestens die Rolle manager. Ohne Angabe gelten engine=bedrock, language=de und enabled=true; der angemeldete Nutzer wird als created_by vermerkt. Die Antwort ist 201 mit dem angelegten Datensatz; konnte der Speicher nicht schreiben, kommt 503 statt eines halben Datensatzes.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"useCase":{"type":["string","null"],"maxLength":64},"persona":{"type":["string","null"],"maxLength":4000},"systemPrompt":{"type":["string","null"],"maxLength":8000},"knowledgeBinding":{"type":["string","null"],"maxLength":200},"engine":{"type":"string","enum":["bedrock","elevenlabs"]},"voice":{"type":["string","null"],"maxLength":64},"elevenlabsAgentId":{"type":["string","null"],"maxLength":200},"voiceId":{"type":["string","null"],"maxLength":200},"language":{"type":"string","maxLength":8},"enabled":{"type":"boolean"}},"required":["name"]},"example":{"name":"string","useCase":"string","persona":"string","systemPrompt":"string","knowledgeBinding":"string","engine":"bedrock","voice":"string","elevenlabsAgentId":"string","voiceId":"string","language":"string","enabled":true}}}},"summary":"Voice-Agent anlegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/voice/agents/{id}":{"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"name":{"type":"string"},"use_case":{"type":["string","null"]},"persona":{"type":["string","null"]},"system_prompt":{"type":["string","null"]},"knowledge_binding":{"type":["string","null"]},"elevenlabs_agent_id":{"type":["string","null"]},"voice_id":{"type":["string","null"]},"engine":{"type":"string"},"voice":{"type":["string","null"]},"language":{"type":"string"},"enabled":{"type":"boolean"},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","name","use_case","persona","system_prompt","knowledge_binding","elevenlabs_agent_id","voice_id","engine","voice","language","enabled","created_by","created_at","updated_at"]}},"required":["data"]},"example":{"data":{"id":"string","tenant_id":"string","name":"string","use_case":"string","persona":"string","system_prompt":"string","knowledge_binding":"string","elevenlabs_agent_id":"string","voice_id":"string","engine":"string","voice":"string","language":"string","enabled":true,"created_by":"string","created_at":"string","updated_at":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"putApiV1VoiceAgentsById","tags":["voice"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Voice-Agent aktualisieren. Braucht mindestens die Rolle manager. Geschrieben wird als Upsert auf die Id aus dem Pfad, und zwar ALLE Felder des Rumpfes: ein optionales Feld, das nicht mitkommt, wird geleert und nicht behalten. Eine Id, die einem anderen Mandanten gehört, ändert nichts und ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"useCase":{"type":["string","null"],"maxLength":64},"persona":{"type":["string","null"],"maxLength":4000},"systemPrompt":{"type":["string","null"],"maxLength":8000},"knowledgeBinding":{"type":["string","null"],"maxLength":200},"engine":{"type":"string","enum":["bedrock","elevenlabs"]},"voice":{"type":["string","null"],"maxLength":64},"elevenlabsAgentId":{"type":["string","null"],"maxLength":200},"voiceId":{"type":["string","null"],"maxLength":200},"language":{"type":"string","maxLength":8},"enabled":{"type":"boolean"}},"required":["name"]},"example":{"name":"string","useCase":"string","persona":"string","systemPrompt":"string","knowledgeBinding":"string","engine":"bedrock","voice":"string","elevenlabsAgentId":"string","voiceId":"string","language":"string","enabled":true}}}},"summary":"Voice-Agent aktualisieren","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Ergebnis","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1VoiceAgentsById","tags":["voice"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Voice-Agent löschen. Braucht mindestens die Rolle manager. Gelöscht wird endgültig — kein Papierkorb, kein deleted_at — und nur innerhalb des eigenen Mandanten. Die Antwort ist immer 200: ok=true sagt, dass der Löschbefehl gelaufen ist, nicht dass es die Id gab; ok=false heisst, dass er gar nicht ausgeführt werden konnte.","summary":"Voice-Agent löschen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/voice/call":{"post":{"responses":{"200":{"description":"Ergebnis (ok | dormant | blocked)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"dormant":{"type":"boolean"},"blocked":{"type":"string"},"callLogId":{"type":"string"},"callId":{"type":"string"},"error":{"type":"string"}},"required":["ok"]},"example":{"ok":true,"dormant":true,"blocked":"string","callLogId":"string","callId":"string","error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1VoiceCall","tags":["voice"],"parameters":[],"description":"Startet EINEN ausgehenden KI-Anruf (Naht #1). Dormant ohne Keys → { dormant:true }. Consent-Gate + KI-Ansage werden im Service erzwungen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string","minLength":1},"toNumber":{"type":"string","minLength":4,"maxLength":20},"agentPhoneNumberId":{"type":"string","minLength":1,"maxLength":200},"useCase":{"type":"string","maxLength":64},"contactId":{"type":["string","null"],"maxLength":64},"contactType":{"type":"string","enum":["customer","contact"]},"consentConfirmed":{"type":"boolean"},"dynamicVariables":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}}},"required":["agentId","toNumber"]},"example":{"agentId":"string","toNumber":"string","agentPhoneNumberId":"string","useCase":"string","contactId":"string","contactType":"customer","consentConfirmed":true,"dynamicVariables":{"beispiel":"string"}}}}},"summary":"Startet EINEN ausgehenden KI-Anruf (Naht #1)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/voice/provider-config":{"get":{"responses":{"200":{"description":"Config ohne Secret","content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string"},"account_ref":{"type":["string","null"]},"from_number":{"type":["string","null"]},"sip_host":{"type":["string","null"]},"sip_port":{"type":["number","null"]},"hasSecret":{"type":"boolean"}},"required":["provider","account_ref","from_number","sip_host","sip_port","hasSecret"]},"example":{"provider":"string","account_ref":"string","from_number":"string","sip_host":"string","sip_port":0,"hasSecret":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1VoiceProvider-config","tags":["voice"],"parameters":[],"description":"Telefonie-Provider-Config des Mandanten (Secret NIE — nur hasSecret). Braucht mindestens die Rolle admin. Liest public.tenant_voice_provider_config; ist nichts hinterlegt, antwortet die Route trotzdem mit 200 und provider=ainemix, alle übrigen Felder null. Das gespeicherte Secret wird nicht entschlüsselt und nicht mitgeschickt — hasSecret sagt allein, ob eines vorliegt.","summary":"Telefonie-Provider-Config des Mandanten (Secret NIE — nur hasSecret)","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Gespeichert, ohne Secret","content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string"},"account_ref":{"type":["string","null"]},"from_number":{"type":["string","null"]},"sip_host":{"type":["string","null"]},"sip_port":{"type":["number","null"]},"hasSecret":{"type":"boolean"}},"required":["provider","account_ref","from_number","sip_host","sip_port","hasSecret"]},"example":{"provider":"string","account_ref":"string","from_number":"string","sip_host":"string","sip_port":0,"hasSecret":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Speicher weg oder FIELD_ENCRYPTION_MASTER_KEY fehlt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"putApiV1VoiceProvider-config","tags":["voice"],"parameters":[],"description":"Telefonie-Provider-Config upsert. Secret verschlüsselt (@nemix/crypto).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["ainemix","twilio","sipgate","other"]},"accountRef":{"type":["string","null"],"maxLength":200},"fromNumber":{"type":["string","null"],"maxLength":32},"sipHost":{"type":["string","null"],"maxLength":255},"sipPort":{"type":["integer","null"],"minimum":1,"maximum":65535},"secret":{"type":"string","maxLength":2000}},"required":["provider"]},"example":{"provider":"ainemix","accountRef":"string","fromNumber":"string","sipHost":"string","sipPort":1,"secret":"string"}}}},"summary":"Telefonie-Provider-Config upsert","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/voice/number-requests":{"post":{"responses":{"201":{"description":"Angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"provider_pref":{"type":["string","null"]},"area_code":{"type":["string","null"]},"number_type":{"type":["string","null"]},"note":{"type":["string","null"]},"status":{"type":"string"},"assigned_e164":{"type":["string","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","provider_pref","area_code","number_type","note","status","assigned_e164","created_by","created_at","updated_at"]}},"required":["data"]},"example":{"data":{"id":"string","tenant_id":"string","provider_pref":"string","area_code":"string","number_type":"string","note":"string","status":"string","assigned_e164":"string","created_by":"string","created_at":"string","updated_at":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Speicher nicht verfuegbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1VoiceNumber-requests","tags":["voice"],"parameters":[],"description":"Nummern-Anfrage an AiNemix anlegen (status=pending). Braucht mindestens die Rolle manager. Die Anfrage wird nur vermerkt — sie bestellt keine Rufnummer und schaltet keine frei; AiNemix teilt aus dem Vorrat zu und setzt danach status und assigned_e164. Die Antwort ist 201 mit dem angelegten Datensatz, 503 wenn der Speicher nicht schreiben konnte.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"providerPref":{"type":["string","null"],"maxLength":32},"areaCode":{"type":["string","null"],"maxLength":16},"numberType":{"type":"string","enum":["local","mobile"]},"note":{"type":["string","null"],"maxLength":500}}},"example":{"providerPref":"string","areaCode":"string","numberType":"local","note":"string"}}}},"summary":"Nummern-Anfrage an AiNemix anlegen (status=pending)","x-nemix-summary-source":"description:first-sentence"},"get":{"responses":{"200":{"description":"Anfragen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"provider_pref":{"type":["string","null"]},"area_code":{"type":["string","null"]},"number_type":{"type":["string","null"]},"note":{"type":["string","null"]},"status":{"type":"string"},"assigned_e164":{"type":["string","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","provider_pref","area_code","number_type","note","status","assigned_e164","created_by","created_at","updated_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","tenant_id":"string","provider_pref":"string","area_code":"string","number_type":"string","note":"string","status":"string","assigned_e164":"string","created_by":"string","created_at":"string","updated_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1VoiceNumber-requests","tags":["voice"],"parameters":[],"description":"Nummern-Anfragen des Mandanten. Liest public.voice_number_requests, neueste zuerst. status zeigt, wie weit AiNemix mit der Anfrage ist; ist eine Nummer zugeteilt, steht sie in assigned_e164. Ist der Speicher nicht erreichbar, kommt eine leere Liste mit 200 zurück.","summary":"Nummern-Anfragen des Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/voice/campaigns":{"get":{"responses":{"200":{"description":"Kampagnen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"agent_id":{"type":"string"},"use_case":{"type":["string","null"]},"name":{"type":"string"},"status":{"type":"string"},"schedule_at":{"type":["string","null"]},"targets_jsonb":{},"created_at":{"type":"string"}},"required":["id","tenant_id","agent_id","use_case","name","status","schedule_at","created_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","tenant_id":"string","agent_id":"string","use_case":"string","name":"string","status":"string","schedule_at":"string","created_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1VoiceCampaigns","tags":["voice"],"parameters":[],"description":"Anruf-Kampagnen des Mandanten. Liest public.voice_call_campaigns, neueste zuerst — ohne Blätterung und ohne Statusfilter. Ist der Speicher nicht erreichbar, kommt eine leere Liste mit 200 zurück und kein Fehler.","summary":"Anruf-Kampagnen des Mandanten","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"agent_id":{"type":"string"},"use_case":{"type":["string","null"]},"name":{"type":"string"},"status":{"type":"string"},"schedule_at":{"type":["string","null"]},"targets_jsonb":{},"created_at":{"type":"string"}},"required":["id","tenant_id","agent_id","use_case","name","status","schedule_at","created_at"]}},"required":["data"]},"example":{"data":{"id":"string","tenant_id":"string","agent_id":"string","use_case":"string","name":"string","status":"string","schedule_at":"string","created_at":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Speicher nicht verfuegbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1VoiceCampaigns","tags":["voice"],"parameters":[],"description":"Anruf-Kampagne anlegen (status=draft). Braucht mindestens die Rolle manager. Die Kampagne wird nur angelegt, nicht gestartet: der Status ist fest draft, und targets landet als JSON-Liste im Datensatz (leer, wenn nichts mitkommt). Die Antwort ist 201 mit dem angelegten Datensatz, 503 wenn der Speicher nicht schreiben konnte.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string","minLength":1},"useCase":{"type":["string","null"],"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":200},"scheduleAt":{"type":["string","null"],"format":"date-time"},"targets":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["agentId","name"]},"example":{"agentId":"string","useCase":"string","name":"string","scheduleAt":"2026-01-01T12:00:00.000Z","targets":[{}]}}}},"summary":"Anruf-Kampagne anlegen (status=draft)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/voice/logs":{"get":{"responses":{"200":{"description":"Call-Logs","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"campaign_id":{"type":["string","null"]},"agent_id":{"type":["string","null"]},"contact_id":{"type":["string","null"]},"phone":{"type":["string","null"]},"status":{"type":"string"},"transcript":{"type":["string","null"]},"result_jsonb":{},"duration_sec":{"type":["number","null"]},"started_at":{"type":["string","null"]},"ended_at":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","tenant_id","campaign_id","agent_id","contact_id","phone","status","transcript","duration_sec","started_at","ended_at","created_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","tenant_id":"string","campaign_id":"string","agent_id":"string","contact_id":"string","phone":"string","status":"string","transcript":"string","duration_sec":0,"started_at":"string","ended_at":"string","created_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1VoiceLogs","tags":["voice"],"parameters":[],"description":"Letzte Call-Logs. Liest public.voice_call_logs, neueste zuerst. Die Abfragezeichenkette limit steuert die Menge: ohne Angabe sind es 50, mehr als 200 gibt es nicht. Ist der Speicher nicht erreichbar, kommt eine leere Liste mit 200 zurück.","summary":"Letzte Call-Logs","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/voice/numbers":{"get":{"responses":{"200":{"description":"Telefonnummern","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"e164":{"type":"string"},"label":{"type":["string","null"]},"active":{"type":"boolean"}},"required":["id","e164","label","active"]}}},"required":["data"]},"example":{"data":[{"id":"string","e164":"string","label":"string","active":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1VoiceNumbers","tags":["voice"],"parameters":[],"description":"Hinterlegte Telefonnummern. Liest public.voice_phone_numbers, neueste zuerst, und liefert je Nummer nur id, e164, label und active — mehr wählt die Abfrage nicht aus. Ist der Speicher nicht erreichbar, kommt eine leere Liste mit 200 zurück.","summary":"Hinterlegte Telefonnummern","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/settings/email-outbound":{"get":{"responses":{"200":{"description":"Aktuelle SMTP-Konfiguration des Mandanten, oder die Vorbelegung, wenn noch keine gespeichert ist","content":{"application/json":{"schema":{"type":"object","properties":{"configured":{"type":"boolean","description":"false, wenn fuer den Mandanten noch keine Zeile existiert — dann sind alle folgenden Felder Vorbelegungen, keine gespeicherten Werte."},"status":{"type":"string","enum":["not_configured","verified","unverified","error","inactive"],"description":"Abgeleitet aus is_active, verified_at und verify_error: `verified` bei aktiver und bestaetigter Konfiguration, sonst `error` bei hinterlegtem Verifikationsfehler, sonst `unverified` solange aktiv, sonst `inactive`."},"provider":{"type":"string","description":"Versandweg. Beim Speichern auf smtp, ses oder resend beschraenkt."},"smtpHost":{"type":"string","description":"Leerer String, wenn nicht gesetzt."},"smtpPort":{"type":"integer","description":"587, solange nichts gespeichert ist."},"smtpSecure":{"type":"boolean","description":"true = implizites TLS (Port 465), false = STARTTLS."},"smtpUsername":{"type":"string","description":"Leerer String, wenn nicht gesetzt."},"smtpPasswordSet":{"type":"boolean","description":"Ob ueberhaupt ein verschluesseltes Passwort hinterlegt ist."},"smtpPassword":{"type":"string","description":"Enthaelt nie das Passwort selbst: `***`, wenn eines hinterlegt ist, sonst leerer String. Fehlt ganz, solange keine Konfiguration existiert."},"fromAddress":{"type":"string","description":"Leerer String, solange keine Konfiguration existiert."},"fromName":{"type":"string","description":"Leerer String, solange keine Konfiguration existiert."},"replyTo":{"type":"string","description":"Leerer String, wenn nicht gesetzt."},"dkimDomain":{"type":"string","description":"Leerer String, wenn nicht gesetzt."},"presetKey":{"type":"string","description":"Gewaehltes SMTP-Preset oder `custom`."},"isActive":{"type":"boolean","description":"Ob die Konfiguration fuer den Versand herangezogen wird."},"verifiedAt":{"type":["string","null"],"description":"Zeitpunkt der erfolgreichen Test-Mail als ISO-8601-Zeichenkette, sonst null."},"verifyError":{"type":["string","null"],"description":"Fehlertext des letzten fehlgeschlagenen Verifikationsversuchs, sonst null."}},"required":["configured","status","provider","smtpHost","smtpPort","smtpSecure","smtpUsername","smtpPasswordSet","fromAddress","fromName","replyTo","dkimDomain","presetKey","isActive","verifiedAt","verifyError"]},"example":{"configured":true,"status":"not_configured","provider":"string","smtpHost":"string","smtpPort":0,"smtpSecure":true,"smtpUsername":"string","smtpPasswordSet":true,"smtpPassword":"string","fromAddress":"string","fromName":"string","replyTo":"string","dkimDomain":"string","presetKey":"string","isActive":true,"verifiedAt":"string","verifyError":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unterhalb von admin"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getApiV1SettingsEmail-outbound","tags":["settings"],"parameters":[],"description":"Liest die eine Zeile des Mandanten aus `public.tenant_email_outbound_config`. Fehlt sie, antwortet der Endpunkt trotzdem mit 200 und `configured: false` samt Vorbelegungen (Port 587, Preset `custom`) statt mit 404 — die Oberflaeche kann das Formular so ohne Sonderfall aufbauen. In der Antwort steht nie das Passwort selbst: `smtpPassword` ist `***`, wenn eines hinterlegt ist, sonst leer; ob ueberhaupt eines gespeichert ist, sagt `smtpPasswordSet`. Ohne erreichbare Datenbank 503, unterhalb der Rolle `admin` 403.","summary":"Liest die eine Zeile des Mandanten aus `public.tenant_email_outbound_config`","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Konfiguration gespeichert. Eine frueher erteilte Verifizierung ist damit zurueckgesetzt — verified_at und verify_error stehen wieder auf null","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"passwordChanged":{"type":"boolean","description":"true nur, wenn ein neues Passwort mitgeschickt und verschluesselt abgelegt wurde. false heisst: das bisher gespeicherte Passwort blieb unveraendert."}},"required":["ok","passwordChanged"]},"example":{"ok":true,"passwordChanged":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unterhalb von admin"},"503":{"description":"Datenbank nicht verfuegbar, oder die Feldverschluesselung fehlt, waehrend ein neues Passwort gespeichert werden soll"}},"operationId":"putApiV1SettingsEmail-outbound","tags":["settings"],"parameters":[],"description":"Speichert die Tenant-SMTP-Konfiguration. Password nur wenn nicht-leer mitgeschickt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["smtp","ses","resend"],"default":"smtp"},"smtpHost":{"anyOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"string","const":""}]},"smtpPort":{"type":"integer","minimum":1,"maximum":65535},"smtpSecure":{"type":"boolean","default":false},"smtpUsername":{"anyOf":[{"type":"string","maxLength":255},{"type":"string","const":""}]},"smtpPassword":{"type":"string","maxLength":512},"fromAddress":{"type":"string","format":"email","maxLength":255},"fromName":{"type":"string","minLength":1,"maxLength":255},"replyTo":{"anyOf":[{"type":"string","format":"email","maxLength":255},{"type":"string","const":""}]},"dkimDomain":{"anyOf":[{"type":"string","maxLength":255},{"type":"string","const":""}]},"presetKey":{"type":"string","maxLength":32},"isActive":{"type":"boolean","default":false}},"required":["fromAddress","fromName"]},"example":{"provider":"smtp","smtpHost":"string","smtpPort":1,"smtpSecure":true,"smtpUsername":"string","smtpPassword":"string","fromAddress":"beispiel@example.com","fromName":"string","replyTo":"beispiel@example.com","dkimDomain":"string","presetKey":"string","isActive":true}}}},"summary":"Speichert die Tenant-SMTP-Konfiguration","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Konfiguration deaktiviert. Zeile, Zugangsdaten und verified_at bleiben erhalten; derselbe ok:true kommt auch, wenn gar keine Zeile existierte","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unterhalb von admin"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"deleteApiV1SettingsEmail-outbound","tags":["settings"],"parameters":[],"description":"Deaktiviert die Tenant-SMTP-Konfiguration (is_active=false). Kein hard-delete — Audit-Trail bleibt.","summary":"Deaktiviert die Tenant-SMTP-Konfiguration (is_active=false)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/settings/email-outbound/verify":{"post":{"responses":{"200":{"description":"Test-Mail versandt. verified_at ist gesetzt, verify_error geleert und die Konfiguration auf aktiv geschaltet","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"messageId":{"type":["string","null"],"description":"Message-ID des Mailservers, sofern der Versand eine geliefert hat, sonst null."}},"required":["success","messageId"]},"example":{"success":true,"messageId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unterhalb von admin"},"422":{"description":"Kein Versand moeglich oder fehlgeschlagen — `error` traegt die Kennung (not_configured, incomplete_config, decrypt_failed oder der Fehler des Mailservers). Nur beim gescheiterten Versand wird der Fehlertext zusaetzlich in verify_error abgelegt"},"503":{"description":"Datenbank nicht verfuegbar oder Feldverschluesselung nicht eingerichtet"}},"operationId":"postApiV1SettingsEmail-outboundVerify","tags":["settings"],"parameters":[],"description":"Sendet eine Test-Mail an die E-Mail des eingeloggten Users. Bei Erfolg setzt verified_at.","summary":"Sendet eine Test-Mail an die E-Mail des eingeloggten Users","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/settings/email-outbound/presets":{"get":{"responses":{"200":{"description":"Alle bekannten SMTP-Presets in fester Reihenfolge","content":{"application/json":{"schema":{"type":"object","properties":{"presets":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Wert fuer `presetKey` beim Speichern: hostinger, gmail, ionos oder outlook."},"label":{"type":"string","description":"Anzeigename fuer die Oberflaeche."},"smtpHost":{"type":"string"},"smtpPort":{"type":"integer"},"smtpSecure":{"type":"boolean","description":"true = implizites TLS (Port 465), false = STARTTLS."},"helpUrl":{"type":"string","description":"Anleitung des Anbieters."},"note":{"type":"string","description":"Zusatzhinweis, nur bei einzelnen Presets vorhanden."}},"required":["key","label","smtpHost","smtpPort","smtpSecure","helpUrl"]}}},"required":["presets"]},"example":{"presets":[{"key":"string","label":"string","smtpHost":"string","smtpPort":0,"smtpSecure":true,"helpUrl":"string","note":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unterhalb von admin"}},"operationId":"getApiV1SettingsEmail-outboundPresets","tags":["settings"],"parameters":[],"description":"Liste der vordefinierten SMTP-Presets (Hostinger / Gmail / IONOS / Outlook). Die Werte stehen fest im Code — kein Datenbankzugriff, kein Mandantenbezug, fuer jeden Aufrufer dieselbe Antwort. `key` ist der Wert, den PUT / als `presetKey` erwartet; laesst der Aufrufer dort Host, Port oder TLS-Schalter leer, ergaenzt das Speichern sie aus dem Preset. `note` steht nur bei einzelnen Eintraegen. Wie alle Endpunkte dieser Datei ab Rolle `admin`.","summary":"Liste der vordefinierten SMTP-Presets (Hostinger / Gmail / IONOS / Outlook)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/settings/tax-rates":{"get":{"responses":{"200":{"description":"Liste der Steuersätze — leer auch dann, wenn kein Datenbank-Client da war","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Steuersatzes"},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Bezeichnung, etwa \"Regelsteuersatz\""},"ratePct":{"type":"number","minimum":0,"maximum":100,"description":"Satz in Prozent — Zahl, nicht Zeichenkette"},"category":{"type":"string","enum":["standard","reduced","exempt","special"],"description":"Einordnung: regulaer, ermaessigt, befreit oder Sonderfall"},"isDefault":{"type":"boolean","description":"Genau einer je Mandant traegt hier true"},"active":{"type":"boolean","description":"Steht der Satz zur Auswahl?"},"sortOrder":{"type":"integer","description":"Reihenfolge in der Auswahlliste, aufsteigend"}},"required":["id","name","ratePct","category","isDefault","active","sortOrder"],"additionalProperties":false},"description":"Alle Steuersaetze des Mandanten, sortiert"}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","name":"string","ratePct":0,"category":"standard","isDefault":true,"active":true,"sortOrder":0}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false,"description":"Aus dem catch-Zweig — mit Wartezeit"},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false,"description":"Kein Datenbank-Client vorhanden — ohne Wartezeit"}]}}}}},"operationId":"getApiV1SettingsTax-rates","tags":["settings"],"parameters":[],"summary":"List tax rates","description":"Listet alle MwSt-Sätze des Mandanten, sortiert nach Reihenfolge und Satz. Fehlen die Stammsätze, werden sie vorher angelegt. ACHTUNG: ist gar kein Datenbank-Client vorhanden, kommt 200 mit einer LEEREN Liste — „keine Sätze hinterlegt\" ist von „Datenbank stumm\" in diesem Fall nicht zu unterscheiden."},"post":{"responses":{"201":{"description":"Steuersatz angelegt — der neue Satz, inklusive vergebener Id","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Steuersatzes"},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Bezeichnung, etwa \"Regelsteuersatz\""},"ratePct":{"type":"number","minimum":0,"maximum":100,"description":"Satz in Prozent — Zahl, nicht Zeichenkette"},"category":{"type":"string","enum":["standard","reduced","exempt","special"],"description":"Einordnung: regulaer, ermaessigt, befreit oder Sonderfall"},"isDefault":{"type":"boolean","description":"Genau einer je Mandant traegt hier true"},"active":{"type":"boolean","description":"Steht der Satz zur Auswahl?"},"sortOrder":{"type":"integer","description":"Reihenfolge in der Auswahlliste, aufsteigend"}},"required":["id","name","ratePct","category","isDefault","active","sortOrder"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","ratePct":0,"category":"standard","isDefault":true,"active":true,"sortOrder":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `admin`"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false,"description":"Aus dem catch-Zweig — mit Wartezeit"},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false,"description":"Kein Datenbank-Client vorhanden — ohne Wartezeit"}]}}}}},"operationId":"postApiV1SettingsTax-rates","tags":["settings"],"parameters":[],"summary":"Create tax rate","description":"Legt einen neuen MwSt-Satz an. Wird er als Standard markiert, verliert der bisherige Standard dieses Kennzeichen — beides passiert nacheinander, nicht in einer gemeinsamen Transaktion. Schreiben ab Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"ratePct":{"type":"number","minimum":0,"maximum":100},"category":{"type":"string","enum":["standard","reduced","exempt","special"],"default":"standard"},"isDefault":{"type":"boolean","default":false},"active":{"type":"boolean","default":true},"sortOrder":{"type":"integer","default":0}},"required":["name","ratePct"]},"example":{"name":"string","ratePct":0,"category":"standard","isDefault":true,"active":true,"sortOrder":0}}}}}},"/api/v1/settings/tax-rates/{id}":{"put":{"responses":{"200":{"description":"Steuersatz aktualisiert — der Satz nach der Änderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Steuersatzes"},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Bezeichnung, etwa \"Regelsteuersatz\""},"ratePct":{"type":"number","minimum":0,"maximum":100,"description":"Satz in Prozent — Zahl, nicht Zeichenkette"},"category":{"type":"string","enum":["standard","reduced","exempt","special"],"description":"Einordnung: regulaer, ermaessigt, befreit oder Sonderfall"},"isDefault":{"type":"boolean","description":"Genau einer je Mandant traegt hier true"},"active":{"type":"boolean","description":"Steht der Satz zur Auswahl?"},"sortOrder":{"type":"integer","description":"Reihenfolge in der Auswahlliste, aufsteigend"}},"required":["id","name","ratePct","category","isDefault","active","sortOrder"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","ratePct":0,"category":"standard","isDefault":true,"active":true,"sortOrder":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `admin`"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false,"description":"Aus dem catch-Zweig — mit Wartezeit"},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false,"description":"Kein Datenbank-Client vorhanden — ohne Wartezeit"}]}}}}},"operationId":"putApiV1SettingsTax-ratesById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace tax rate","description":"Ersetzt einen MwSt-Satz vollständig — alle Felder werden geschrieben, nicht mitgeschickte fallen auf ihren Vorgabewert zurück. Wird er als Standard markiert, verliert der bisherige Standard sein Kennzeichen. Schreiben ab Rolle `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"ratePct":{"type":"number","minimum":0,"maximum":100},"category":{"type":"string","enum":["standard","reduced","exempt","special"],"default":"standard"},"isDefault":{"type":"boolean","default":false},"active":{"type":"boolean","default":true},"sortOrder":{"type":"integer","default":0}},"required":["name","ratePct"]},"example":{"name":"string","ratePct":0,"category":"standard","isDefault":true,"active":true,"sortOrder":0}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Quittung, der gelöschte Satz kommt nicht zurück","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Der Satz wurde geloescht"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `admin`"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default kann nicht gelöscht werden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"cannot_delete_default","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Begruendung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false,"description":"Aus dem catch-Zweig — mit Wartezeit"},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false,"description":"Kein Datenbank-Client vorhanden — ohne Wartezeit"}]}}}}},"operationId":"deleteApiV1SettingsTax-ratesById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete tax rate","description":"Löscht einen MwSt-Satz endgültig — kein Soft-Delete, kein Wiederherstellen. Der Standard-Satz ist gesperrt (409); um ihn zu löschen, muss zuerst ein anderer zum Standard gemacht werden. Schreiben ab Rolle `admin`."}},"/api/v1/settings/tax-profile":{"get":{"responses":{"200":{"description":"Steuerprofil — gespeicherter Stand oder Standardprofil","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"kleinunternehmer":{"type":"boolean","description":"Kleinunternehmerregelung §19 UStG — alle Positionen werden mit 0 % gerechnet"},"versteuerung":{"type":"string","enum":["soll","ist"],"description":"Soll- oder Ist-Versteuerung, §13 bzw. §20 UStG"},"skr":{"type":"string","enum":["03","04"],"description":"Standardkontenrahmen (DATEV)"},"bundesland":{"type":["string","null"],"enum":["BW","BY","BE","BB","HB","HH","HE","MV","NI","NW","RP","SL","SN","ST","SH","TH",null],"description":"Bundesland als Kfz-Kuerzel; null wenn nicht gesetzt"},"euOss":{"type":"boolean","description":"One-Stop-Shop fuer EU-Fernverkaeufe aktiv"},"reverseChargeDefault":{"type":"boolean","description":"Ruecklage §13b, wenn am Kunden kein eigener Status hinterlegt ist"},"steuerberaterName":{"type":["string","null"],"maxLength":120,"description":"Name des Steuerberaters; null wenn nicht gesetzt"},"steuerberaterBeraterNr":{"type":["string","null"],"maxLength":20,"description":"Beraternummer; null wenn nicht gesetzt"},"steuerberaterMandantenNr":{"type":["string","null"],"maxLength":20,"description":"Mandantennummer beim Berater; null wenn nicht gesetzt"},"steuerberaterEmail":{"type":["string","null"],"maxLength":160,"description":"E-Mail des Steuerberaters; null wenn nicht gesetzt"}},"required":["kleinunternehmer","versteuerung","skr","bundesland","euOss","reverseChargeDefault","steuerberaterName","steuerberaterBeraterNr","steuerberaterMandantenNr","steuerberaterEmail"],"description":"Das Steuerprofil"}},"required":["data"]},"example":{"data":{"kleinunternehmer":true,"versteuerung":"soll","skr":"03","bundesland":"BW","euOss":true,"reverseChargeDefault":true,"steuerberaterName":"string","steuerberaterBeraterNr":"string","steuerberaterMandantenNr":"string","steuerberaterEmail":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1SettingsTax-profile","tags":["settings"],"parameters":[],"description":"Liefert das Steuerprofil des Mandanten (genau eins, Self-Heal-Default falls neu). Ist gar kein Datenbank-Client verfügbar, kommt trotzdem 200 mit dem Standardprofil — nicht mit den gespeicherten Werten.","summary":"Liefert das Steuerprofil des Mandanten (genau eins, Self-Heal-Default falls neu)","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Steuerprofil gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"kleinunternehmer":{"type":"boolean","description":"Kleinunternehmerregelung §19 UStG — alle Positionen werden mit 0 % gerechnet"},"versteuerung":{"type":"string","enum":["soll","ist"],"description":"Soll- oder Ist-Versteuerung, §13 bzw. §20 UStG"},"skr":{"type":"string","enum":["03","04"],"description":"Standardkontenrahmen (DATEV)"},"bundesland":{"type":["string","null"],"enum":["BW","BY","BE","BB","HB","HH","HE","MV","NI","NW","RP","SL","SN","ST","SH","TH",null],"description":"Bundesland als Kfz-Kuerzel; null wenn nicht gesetzt"},"euOss":{"type":"boolean","description":"One-Stop-Shop fuer EU-Fernverkaeufe aktiv"},"reverseChargeDefault":{"type":"boolean","description":"Ruecklage §13b, wenn am Kunden kein eigener Status hinterlegt ist"},"steuerberaterName":{"type":["string","null"],"maxLength":120,"description":"Name des Steuerberaters; null wenn nicht gesetzt"},"steuerberaterBeraterNr":{"type":["string","null"],"maxLength":20,"description":"Beraternummer; null wenn nicht gesetzt"},"steuerberaterMandantenNr":{"type":["string","null"],"maxLength":20,"description":"Mandantennummer beim Berater; null wenn nicht gesetzt"},"steuerberaterEmail":{"type":["string","null"],"maxLength":160,"description":"E-Mail des Steuerberaters; null wenn nicht gesetzt"}},"required":["kleinunternehmer","versteuerung","skr","bundesland","euOss","reverseChargeDefault","steuerberaterName","steuerberaterBeraterNr","steuerberaterMandantenNr","steuerberaterEmail"],"description":"Das Steuerprofil"}},"required":["data"]},"example":{"data":{"kleinunternehmer":true,"versteuerung":"soll","skr":"03","bundesland":"BW","euOss":true,"reverseChargeDefault":true,"steuerberaterName":"string","steuerberaterBeraterNr":"string","steuerberaterMandantenNr":"string","steuerberaterEmail":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Nicht gespeichert. Zwei Formen: ohne Datenbank-Client nur `error`, nach einem Fehler zusätzlich `retryAfter`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}]}}}}},"operationId":"putApiV1SettingsTax-profile","tags":["settings"],"parameters":[],"description":"Setzt das Steuerprofil des Mandanten (Upsert). Braucht mindestens die Rolle admin. Es gibt genau ein Profil je Mandant, und der Aufruf ersetzt es VOLLSTÄNDIG: nicht gesendete Felder fallen auf ihre Vorgabe zurück (kleinunternehmer false, Soll-Versteuerung, SKR 03, übrige leer) — nicht auf den gespeicherten Wert. Leere Zeichenketten in den Steuerberater-Feldern werden als „nicht gesetzt\" abgelegt. Die Änderung wird im Aktivitätsverlauf vermerkt. Das Profil steuert die Steuerberechnung der Belege.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kleinunternehmer":{"type":"boolean","default":false},"versteuerung":{"type":"string","enum":["soll","ist"],"default":"soll"},"skr":{"type":"string","enum":["03","04"],"default":"03"},"bundesland":{"type":["string","null"],"enum":["BW","BY","BE","BB","HB","HH","HE","MV","NI","NW","RP","SL","SN","ST","SH","TH",null]},"euOss":{"type":"boolean","default":false},"reverseChargeDefault":{"type":"boolean","default":false},"steuerberaterName":{"type":["string","null"],"maxLength":120},"steuerberaterBeraterNr":{"type":["string","null"],"maxLength":20},"steuerberaterMandantenNr":{"type":["string","null"],"maxLength":20},"steuerberaterEmail":{"type":["string","null"],"maxLength":160}}},"example":{"kleinunternehmer":true,"versteuerung":"soll","skr":"03","bundesland":"BW","euOss":true,"reverseChargeDefault":true,"steuerberaterName":"string","steuerberaterBeraterNr":"string","steuerberaterMandantenNr":"string","steuerberaterEmail":"string"}}}},"summary":"Setzt das Steuerprofil des Mandanten (Upsert)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/settings/document-defaults":{"get":{"responses":{"200":{"description":"Ein Satz je Belegart. `null` heisst „kein Standard hinterlegt\" — nicht 0 und nicht Leerstring.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"belegart":{"type":"string"},"belegtitel":{"type":["string","null"]},"einleitungstext":{"type":["string","null"]},"schlusstext":{"type":["string","null"]},"zahlungszielTage":{"type":["number","null"]},"gueltigkeitTage":{"type":["number","null"]}},"required":["belegart","belegtitel","einleitungstext","schlusstext","zahlungszielTage","gueltigkeitTage"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"belegart":"string","belegtitel":"string","einleitungstext":"string","schlusstext":"string","zahlungszielTage":0,"gueltigkeitTage":0}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1SettingsDocument-defaults","tags":["settings"],"parameters":[],"description":"Liefert die Mandant-Standardwerte je Belegart (Lexware-Stammdaten). Zurueck kommen IMMER alle sechs Belegarten (quote, order, invoice, delivery, credit_note, dunning) — auch die ohne gespeicherte Zeile, dann mit lauter `null`. Eine leere Liste gibt es hier also nicht. Beschrieben ist ausschliesslich die Mandanten-Ebene; die Werte am Kunden und am einzelnen Beleg stehen woanders und gehen dieser vor.","summary":"Liefert die Mandant-Standardwerte je Belegart (Lexware-Stammdaten)","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Gespeichert — der Satz DIESER einen Belegart nach der Aenderung, nicht die ganze Liste und nicht im `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"belegart":{"type":"string"},"belegtitel":{"type":["string","null"]},"einleitungstext":{"type":["string","null"]},"schlusstext":{"type":["string","null"]},"zahlungszielTage":{"type":["number","null"]},"gueltigkeitTage":{"type":["number","null"]}},"required":["belegart","belegtitel","einleitungstext","schlusstext","zahlungszielTage","gueltigkeitTage"],"additionalProperties":false},"example":{"belegart":"string","belegtitel":"string","einleitungstext":"string","schlusstext":"string","zahlungszielTage":0,"gueltigkeitTage":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putApiV1SettingsDocument-defaults","tags":["settings"],"parameters":[],"description":"Speichert (UPSERT) die Standardwerte für eine Belegart. `belegart` ist Pflicht und bestimmt den Datensatz. Geschrieben werden NUR die Felder, die der Rumpf wirklich enthaelt: ein fehlender Schluessel bleibt unberuehrt, ein mitgeschicktes `null` oder ein Leerstring leert das Feld ausdruecklich. Ein Teil-PUT — etwa nur `gueltigkeitTage` — setzt die uebrigen Standardwerte also nicht auf null. Zahlungsziel 0–365 Tage, Gueltigkeit 0–3650 Tage. Der Aufruf schreibt einen Eintrag ins Aktivitaetsprotokoll; scheitert das, bleibt es folgenlos. Nur Rolle `admin` oder hoeher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"belegart":{"type":"string","enum":["quote","order","invoice","delivery","credit_note","dunning"]},"belegtitel":{"type":["string","null"],"maxLength":255},"einleitungstext":{"type":["string","null"],"maxLength":8000},"schlusstext":{"type":["string","null"],"maxLength":8000},"zahlungszielTage":{"type":["integer","null"],"minimum":0,"maximum":365},"gueltigkeitTage":{"type":["integer","null"],"minimum":0,"maximum":3650}},"required":["belegart"]},"example":{"belegart":"quote","belegtitel":"string","einleitungstext":"string","schlusstext":"string","zahlungszielTage":0,"gueltigkeitTage":0}}}},"summary":"Speichert (UPSERT) die Standardwerte für eine Belegart","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/settings/beleg-text-standards":{"get":{"responses":{"200":{"description":"Liste der Text-Bausteine","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"fieldType":{"type":"string"},"name":{"type":"string"},"content":{"type":"string"},"isDefault":{"type":"boolean"},"sortOrder":{"type":"integer"}},"required":["id","fieldType","name","content","isDefault","sortOrder"]}}},"required":["data"]},"example":{"data":[{"id":"string","fieldType":"string","name":"string","content":"string","isDefault":true,"sortOrder":0}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1SettingsBeleg-text-standards","tags":["settings"],"parameters":[],"summary":"Liefert alle benannten Belegtext-Bausteine des Mandanten","description":"Gibt die Bausteine ALLER Feldtypen (title, salutation, intro, footer, validity) in EINER Liste zurueck — ohne Blaetterung und ohne Filter —, sortiert nach Feldtyp, dann Sortierwert, dann Name. NEBENWIRKUNG: hat ein Mandant zu einem Feldtyp noch gar keinen Baustein, legt dieser Aufruf die Standardvorlagen dafuer an. Loescht jemand spaeter alle Bausteine eines Feldtyps, kommen sie NICHT zurueck. Fuer „validity\" haelt `content` keine Textvorlage, sondern eine Anzahl Tage als Zeichenkette. Lesen darf jede Rolle; Aendern nur ab „manager\"."},"post":{"responses":{"200":{"description":"Angelegt (oder bei isDefault=true der gleichnamige Baustein ueberschrieben)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"fieldType":{"type":"string"},"name":{"type":"string"},"content":{"type":"string"},"isDefault":{"type":"boolean"},"sortOrder":{"type":"integer"}},"required":["id","fieldType","name","content","isDefault","sortOrder"]}},"required":["data"]},"example":{"data":{"id":"string","fieldType":"string","name":"string","content":"string","isDefault":true,"sortOrder":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Name im Feldtyp bereits vergeben"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1SettingsBeleg-text-standards","tags":["settings"],"parameters":[],"summary":"Legt einen neuen benannten Belegtext-Baustein an","description":"Nur ab Rolle „manager\". Antwortet mit 200, nicht mit 201. Der Name muss je Mandant und Feldtyp eindeutig sein — ein bereits vergebener ergibt 409. AUSNAHME: mit `isDefault: true` wird der vorhandene Baustein desselben Namens stattdessen UEBERSCHRIEBEN (Inhalt und Default-Kennzeichen), weil die Absicht dort „dieser Text ist ab jetzt der Standard\" lautet. `isDefault: true` nimmt auszerdem allen anderen Bausteinen desselben Feldtyps das Kennzeichen — je Feldtyp ist hoechstens einer der Standard. Der Vorgang wird im Aktivitaetsverlauf festgehalten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"fieldType":{"type":"string","enum":["title","salutation","intro","footer","validity"]},"name":{"type":"string","minLength":1,"maxLength":100},"content":{"type":"string","maxLength":8000,"default":""},"isDefault":{"type":"boolean"}},"required":["fieldType","name"]},"example":{"fieldType":"title","name":"string","content":"string","isDefault":true}}}}}},"/api/v1/settings/beleg-text-standards/{id}":{"put":{"responses":{"200":{"description":"Gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"fieldType":{"type":"string"},"name":{"type":"string"},"content":{"type":"string"},"isDefault":{"type":"boolean"},"sortOrder":{"type":"integer"}},"required":["id","fieldType","name","content","isDefault","sortOrder"]}},"required":["data"]},"example":{"data":{"id":"string","fieldType":"string","name":"string","content":"string","isDefault":true,"sortOrder":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Baustein nicht gefunden"},"409":{"description":"Name im Feldtyp bereits vergeben"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1SettingsBeleg-text-standardsById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ändert einen benannten Belegtext-Baustein","description":"Nur ab Rolle „manager\". Teil-Update: nicht gesendete Felder behalten ihren Wert. Der Feldtyp laesst sich NICHT umhaengen und der Sortierwert nicht setzen. `isDefault: true` nimmt allen anderen Bausteinen desselben Feldtyps das Kennzeichen; `isDefault: false` entfernt es nur bei diesem — der Feldtyp steht danach ohne Standard da. Ein bereits vergebener Name ergibt 409. Der Vorgang wird im Aktivitaetsverlauf festgehalten. 404, wenn der Baustein nicht zu diesem Mandanten gehoert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"content":{"type":"string","maxLength":8000},"isDefault":{"type":"boolean"}}},"example":{"name":"string","content":"string","isDefault":true}}}}},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Baustein nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1SettingsBeleg-text-standardsById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Löscht einen benannten Belegtext-Baustein","description":"Nur ab Rolle „manager\". Entfernt die Zeile ENDGUELTIG — kein Soft-Delete, kein Rueckgaengig. Bereits geschriebene Belege behalten ihren Text, sie greifen nicht auf den Baustein zurueck. War es der Standard seines Feldtyps, rueckt KEIN anderer nach: der Feldtyp hat danach keinen Standard mehr. Loescht man alle Bausteine eines Feldtyps, werden die Standardvorlagen nicht erneut angelegt. Der Vorgang wird im Aktivitaetsverlauf festgehalten. 404, wenn der Baustein nicht zu diesem Mandanten gehoert — ein Fremdzugriff loescht also nichts."}},"/api/v1/settings/beleg-text-standards/{id}/set-default":{"patch":{"responses":{"200":{"description":"Default gesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"fieldType":{"type":"string"},"name":{"type":"string"},"content":{"type":"string"},"isDefault":{"type":"boolean"},"sortOrder":{"type":"integer"}},"required":["id","fieldType","name","content","isDefault","sortOrder"]}},"required":["data"]},"example":{"data":{"id":"string","fieldType":"string","name":"string","content":"string","isDefault":true,"sortOrder":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Baustein nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"patchApiV1SettingsBeleg-text-standardsByIdSet-default","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Markiert einen Baustein als Default seines Feldtyps (exklusiv)","description":"Nur ab Rolle „manager\". Braucht keinen Rumpf: der Feldtyp wird aus dem Baustein selbst gelesen. Setzt bei ALLEN anderen Bausteinen desselben Feldtyps das Kennzeichen zurueck und bei diesem auf true — je Feldtyp bleibt hoechstens einer der Standard. Der Aufruf ist wiederholbar; ein Weg, den Standard ohne Ersatz zu entfernen, fuehrt nicht ueber diesen Endpunkt, sondern ueber PUT /:id mit `isDefault: false`. Der Vorgang wird im Aktivitaetsverlauf festgehalten. 404, wenn der Baustein nicht zu diesem Mandanten gehoert."}},"/api/v1/settings/banking":{"get":{"responses":{"200":{"description":"Bankkonten, Standardkonto zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Bankkontos"},"accountHolder":{"type":"string","minLength":1,"maxLength":140,"description":"Kontoinhaber"},"ibanMasked":{"type":"string","minLength":1,"description":"Maskierte IBAN, z. B. \"****1234\" — der Klartext verlaesst den Server nicht"},"ibanLast4":{"type":["string","null"],"minLength":4,"maxLength":4,"description":"Die letzten vier Stellen der IBAN; null wenn nicht ermittelbar"},"bic":{"type":["string","null"],"maxLength":11,"description":"BIC; null wenn nicht erfasst"},"bankName":{"type":["string","null"],"maxLength":140,"description":"Name der Bank; null wenn nicht erfasst"},"isDefault":{"type":"boolean","description":"Standardkonto des Mandanten — hoechstens eines ist true"},"enabled":{"type":"boolean","description":"Wird das Konto auf Belegen und im GiroCode verwendet?"}},"required":["id","accountHolder","ibanMasked","ibanLast4","bic","bankName","isDefault","enabled"],"description":"Ein Bankkonto des Mandanten, IBAN maskiert"},"description":"Alle nicht geloeschten Konten, Standardkonto zuerst"}},"required":["data"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","accountHolder":"string","ibanMasked":"string","ibanLast4":"stri","bic":"string","bankName":"string","isDefault":true,"enabled":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1SettingsBanking","tags":["settings"],"parameters":[],"description":"Listet die Bankkonten des Mandanten (IBAN maskiert). Ist kein Datenbank-Client verfügbar, kommt 200 mit leerer Liste — nicht 503.","summary":"Listet die Bankkonten des Mandanten (IBAN maskiert)","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Bankkonto angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Bankkontos"},"accountHolder":{"type":"string","minLength":1,"maxLength":140,"description":"Kontoinhaber"},"ibanMasked":{"type":"string","minLength":1,"description":"Maskierte IBAN, z. B. \"****1234\" — der Klartext verlaesst den Server nicht"},"ibanLast4":{"type":["string","null"],"minLength":4,"maxLength":4,"description":"Die letzten vier Stellen der IBAN; null wenn nicht ermittelbar"},"bic":{"type":["string","null"],"maxLength":11,"description":"BIC; null wenn nicht erfasst"},"bankName":{"type":["string","null"],"maxLength":140,"description":"Name der Bank; null wenn nicht erfasst"},"isDefault":{"type":"boolean","description":"Standardkonto des Mandanten — hoechstens eines ist true"},"enabled":{"type":"boolean","description":"Wird das Konto auf Belegen und im GiroCode verwendet?"}},"required":["id","accountHolder","ibanMasked","ibanLast4","bic","bankName","isDefault","enabled"],"description":"Das betroffene Bankkonto"}},"required":["data"]},"example":{"data":{"id":"00000000-0000-4000-8000-000000000000","accountHolder":"string","ibanMasked":"string","ibanLast4":"stri","bic":"string","bankName":"string","isDefault":true,"enabled":true}}}}},"400":{"description":"Ungültige IBAN","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_iban","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext fuer die Oberflaeche"}},"required":["error","message"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Nicht angelegt. Zwei Formen: ohne Datenbank-Client nur `error`, nach einem Fehler zusätzlich `retryAfter`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}]}}}}},"operationId":"postApiV1SettingsBanking","tags":["settings"],"parameters":[],"description":"Legt ein Bankkonto an. Die IBAN wird verschlüsselt abgelegt und nur maskiert zurückgegeben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"accountHolder":{"type":"string","minLength":1,"maxLength":140},"iban":{"type":"string","minLength":1,"maxLength":42},"bic":{"type":["string","null"],"maxLength":11},"bankName":{"type":["string","null"],"maxLength":140},"isDefault":{"type":"boolean","default":false},"enabled":{"type":"boolean","default":true}},"required":["accountHolder","iban"]},"example":{"accountHolder":"string","iban":"string","bic":"string","bankName":"string","isDefault":true,"enabled":true}}}},"summary":"Legt ein Bankkonto an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/settings/banking/{id}":{"patch":{"responses":{"200":{"description":"Bankkonto geändert","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Bankkontos"},"accountHolder":{"type":"string","minLength":1,"maxLength":140,"description":"Kontoinhaber"},"ibanMasked":{"type":"string","minLength":1,"description":"Maskierte IBAN, z. B. \"****1234\" — der Klartext verlaesst den Server nicht"},"ibanLast4":{"type":["string","null"],"minLength":4,"maxLength":4,"description":"Die letzten vier Stellen der IBAN; null wenn nicht ermittelbar"},"bic":{"type":["string","null"],"maxLength":11,"description":"BIC; null wenn nicht erfasst"},"bankName":{"type":["string","null"],"maxLength":140,"description":"Name der Bank; null wenn nicht erfasst"},"isDefault":{"type":"boolean","description":"Standardkonto des Mandanten — hoechstens eines ist true"},"enabled":{"type":"boolean","description":"Wird das Konto auf Belegen und im GiroCode verwendet?"}},"required":["id","accountHolder","ibanMasked","ibanLast4","bic","bankName","isDefault","enabled"],"description":"Das betroffene Bankkonto"}},"required":["data"]},"example":{"data":{"id":"00000000-0000-4000-8000-000000000000","accountHolder":"string","ibanMasked":"string","ibanLast4":"stri","bic":"string","bankName":"string","isDefault":true,"enabled":true}}}}},"400":{"description":"Ungültige IBAN — nur geprüft, wenn eine mitgeschickt wurde","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_iban","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext fuer die Oberflaeche"}},"required":["error","message"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Nicht geändert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"patchApiV1SettingsBankingById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Bankkonto ändern — einzelne Felder, IBAN optional","description":"Ändert einzelne Felder eines Bankkontos; nicht gesendete bleiben stehen, und ein Rumpf ohne jede Änderung gibt einfach den aktuellen Stand zurück. Eine IBAN wird nur geprüft und neu verschlüsselt, wenn tatsächlich eine mitkommt — leer oder null lässt die gespeicherte unangetastet und löscht sie nicht. Mit `isDefault: true` verlieren zuerst alle anderen Konten des Mandanten diese Rolle. Ein Erfolg wird als Aktivitätseintrag (`banking_account`, `update`) vermerkt; die IBAN bleibt auch in der Antwort maskiert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"accountHolder":{"type":"string","minLength":1,"maxLength":140},"iban":{"type":["string","null"],"maxLength":42},"bic":{"type":["string","null"],"maxLength":11},"bankName":{"type":["string","null"],"maxLength":140},"isDefault":{"type":"boolean"},"enabled":{"type":"boolean"}}},"example":{"accountHolder":"string","iban":"string","bic":"string","bankName":"string","isDefault":true,"enabled":true}}}}},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des geloeschten Kontos"},"deleted":{"type":"boolean","const":true,"description":"Bestaetigung des Soft-Deletes"}},"required":["id","deleted"],"description":"Ergebnis des Loeschens"}},"required":["data"]},"example":{"data":{"id":"00000000-0000-4000-8000-000000000000","deleted":true}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Nicht gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"deleteApiV1SettingsBankingById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt `deleted_at` und nimmt dem Konto zugleich die Standardrolle — die Zeile selbst bleibt stehen, damit bereits erzeugte Belege nachvollziehbar bleiben. Eine unbekannte oder schon gelöschte Kennung ergibt 404, ein zweiter Aufruf also ebenfalls. Das Löschen wird als Aktivitätseintrag (`banking_account`, `delete`) vermerkt. War es das Standardkonto, ernennt die Route kein neues — danach ist kein Konto mehr als Standard markiert.","summary":"Setzt `deleted_at` und nimmt dem Konto zugleich die Standardrolle","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/developer/api-keys":{"get":{"responses":{"200":{"description":"Liste der API-Keys ohne Schluesselwert","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Schluessels"},"name":{"type":"string","description":"Vergebener Name"},"prefix":{"type":"string","description":"Erkennungsteil des Schluessels (Spalte `key_prefix`)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Gesetzte Scopes; leere Liste wenn keine — dann laesst der Schluessel nichts durch"},"lastUsedAt":{"type":["string","null"],"description":"Letzte Verwendung; null solange nie benutzt"},"expiresAt":{"type":["string","null"],"description":"Ablaufzeitpunkt; null wenn der Schluessel nicht ablaeuft"},"createdAt":{"type":["string","null"],"description":"Anlagezeitpunkt"}},"required":["id","name","prefix","permissions","lastUsedAt","expiresAt","createdAt"]},"description":"Alle Schluessel des Mandanten, aelteste zuerst — OHNE den Schluesselwert"}},"required":["keys"]},"example":{"keys":[{"id":"string","name":"string","prefix":"string","permissions":["string"],"lastUsedAt":"string","expiresAt":"string","createdAt":"string"}]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Manager-Rolle erforderlich"},"500":{"description":"Abfrage fehlgeschlagen"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"getApiV1DeveloperApi-keys","tags":["developer"],"parameters":[],"summary":"API-Schluessel des Mandanten auflisten (ohne den Schluesselwert)","description":"Liest alle Schluessel des Mandanten aus `api_keys`, nach Anlagezeitpunkt AUFSTEIGEND (aelteste zuerst). Zurueck kommen Kennung, Name, Erkennungsteil, Scopes, letzte Verwendung, Ablauf und Anlagezeitpunkt — der Schluesselwert selbst NIE, gespeichert ist nur sein SHA-256-Abdruck. Abgelaufene Schluessel werden NICHT ausgefiltert; das steht in `expiresAt`. Es wird nicht geblaettert. Erfordert mindestens die Rolle `manager`."},"post":{"responses":{"201":{"description":"API-Key erstellt — der Wert steht NUR in dieser Antwort","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Schluessels"},"name":{"type":"string","description":"Vergebener Name"},"prefix":{"type":"string","description":"Erkennungsteil; erscheint spaeter in der Liste"},"key":{"type":"string","description":"Der vollstaendige Schluessel — NUR in dieser einen Antwort. Gespeichert wird nur sein Abdruck"},"createdAt":{"type":["string","null"],"description":"Anlagezeitpunkt"},"expiresAt":{"type":["string","null"],"description":"Ablaufzeitpunkt; null wenn kein `expiresInDays` uebergeben wurde"},"warning":{"type":"string","description":"Hinweis, dass der Wert nicht erneut abrufbar ist"}},"required":["id","name","prefix","key","createdAt","expiresAt","warning"]},"example":{"id":"string","name":"string","prefix":"string","key":"string","createdAt":"string","expiresAt":"string","warning":"string"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"403":{"description":"Manager-Rolle erforderlich"},"500":{"description":"Anlegen fehlgeschlagen"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"postApiV1DeveloperApi-keys","tags":["developer"],"parameters":[],"description":"Erzeugt einen Schluessel und gibt seinen Wert GENAU EINMAL zurueck — gespeichert wird nur sein SHA-256-Abdruck, ein spaeterer Abruf ist unmoeglich. `permissions` nimmt nur die Formen `*`, `admin`, `write`, `delete`, `read` oder `<bereich>:read|write|delete|admin|*` an; ein Tippfehler wird abgelehnt statt still gespeichert. Wird `permissions` weggelassen, entsteht ein Schluessel mit LEERER Scope-Liste, der nichts durchlaesst. `expiresInDays` (1 bis 3650) rechnet den Ablauf ab jetzt; ohne Angabe laeuft der Schluessel nie ab. Antwortet mit 201, nicht mit 200. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"permissions":{"type":"array","items":{"type":"string"}},"expiresInDays":{"type":"integer","exclusiveMinimum":0,"maximum":3650}},"required":["name"]},"example":{"name":"string","permissions":["string"],"expiresInDays":1}}}},"summary":"Erzeugt einen Schluessel und gibt seinen Wert GENAU EINMAL zurueck","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/developer/api-keys/{id}":{"delete":{"responses":{"200":{"description":"API-Key gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"revoked":{"type":"string","description":"Kennung des geloeschten Schluessels"}},"required":["ok","revoked"]},"example":{"ok":true,"revoked":"string"}}}},"400":{"description":"Keine Schluessel-Kennung im Pfad"},"401":{"description":"Unauthorized"},"403":{"description":"Manager-Rolle erforderlich"},"404":{"description":"Kein API-Key mit dieser Kennung im eigenen Mandanten"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"deleteApiV1DeveloperApi-keysById","tags":["developer"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Loescht die Zeile in `api_keys` ENDGUELTIG — kein Soft-Delete, kein Wiederherstellen. Der Schluessel ist ab sofort ungueltig. Geloescht wird nur innerhalb des eigenen Mandanten; ein fremder oder unbekannter Schluessel ergibt 404, nicht 403. Erfordert mindestens die Rolle `manager`.","summary":"Loescht die Zeile in `api_keys` ENDGUELTIG","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Der Schluessel mit neuem Namen, verkuerzt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"key":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Schluessels"},"name":{"type":"string","description":"Der neue Name"},"keyPrefix":{"type":"string","description":"Erkennungsteil — hier in camelCase, anders als `prefix` in den anderen Antworten"}},"required":["id","name","keyPrefix"],"description":"Der geaenderte Schluessel, verkuerzt"}},"required":["ok","key"]},"example":{"ok":true,"key":{"id":"string","name":"string","keyPrefix":"string"}}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"403":{"description":"Manager-Rolle erforderlich"},"404":{"description":"Kein API-Key mit dieser Kennung im eigenen Mandanten"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"patchApiV1DeveloperApi-keysById","tags":["developer"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aendert AUSSCHLIESSLICH den Namen — `name` ist Pflicht. Scopes, Ablauf und der Schluessel selbst lassen sich hier nicht aendern; dafuer den Schluessel widerrufen und einen neuen anlegen. Geaendert wird nur innerhalb des eigenen Mandanten; ein fremder oder unbekannter Schluessel ergibt 404. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255}},"required":["name"]},"example":{"name":"string"}}}},"summary":"Aendert AUSSCHLIESSLICH den Namen — `name` ist Pflicht","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/employees/departments":{"get":{"responses":{"200":{"description":"Liste der Abteilungen — Umschlag { data, total }, keine Paginierung","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1EmployeesDepartments","tags":["employees"],"parameters":[],"summary":"Listet alle Abteilungen des Mandanten","description":"Liest `departments` des Mandanten nach Name aufsteigend und gibt die Zeilen ROH zurück (`SELECT *`, kein Serialisierer). Weder Filter noch Paginierung; `total` ist die Länge der gelieferten Liste, kein getrennt gezähltes Gesamtergebnis."},"post":{"responses":{"201":{"description":"Abteilung angelegt — die eingefuegte Zeile roh, wie sie in der Tabelle steht","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1EmployeesDepartments","tags":["employees"],"parameters":[],"summary":"Legt eine neue Abteilung an","description":"Fügt eine Zeile in `departments` ein und gibt sie roh zurück (201). Pflicht ist allein `name`; Beschreibung und `managerId` sind optional. Ein bereits vergebener Name wird NICHT abgelehnt — doppelte Abteilungen sind möglich. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string"},"managerId":{"type":"string","format":"uuid"}},"required":["name"]},"example":{"name":"string","description":"string","managerId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/employees":{"get":{"responses":{"200":{"description":"Liste der Mitarbeiter. `salary` nur bei entsprechender Berechtigung, sonst null.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"employeeNumber":{},"firstName":{},"lastName":{},"email":{},"phone":{},"department":{},"position":{},"hiredAt":{},"status":{},"salaryType":{},"salary":{"type":["number","null"]},"managerId":{},"userId":{},"address":{},"metadata":{},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["salary","customFields"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"salary":0,"customFields":{}}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1Employees","tags":["employees"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"department","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive","on-leave"]}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"Listet alle Mitarbeiter des Mandanten","description":"Liest `employees` ohne gesetztes `deleted_at`, sortiert nach Nachname und Vorname. Filtert wahlweise nach `status`, nach `department` (Teiltreffer, Groß-/Kleinschreibung egal) und mit `search` über Vorname, Nachname, E-Mail und Personalnummer. Blättert über `limit` (1-200, Standard 50) und `offset`. `salary` bekommt nur ein HR-Manager als Zahl; für alle anderen steht dort null im Sinne von nicht sichtbar, nicht im Sinne von kein Gehalt hinterlegt."},"post":{"responses":{"201":{"description":"Mitarbeiter angelegt — hier IST `salary` gefuellt (Anleger ist HR-Manager)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"employeeNumber":{},"firstName":{},"lastName":{},"email":{},"phone":{},"department":{},"position":{},"hiredAt":{},"status":{},"salaryType":{},"salary":{"type":["number","null"]},"managerId":{},"userId":{},"address":{},"metadata":{},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["salary","customFields"],"additionalProperties":false},"example":{"salary":0,"customFields":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine HR-Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1Employees","tags":["employees"],"parameters":[],"summary":"Legt einen neuen Mitarbeiter an (nur HR-Manager+)","description":"Vergibt die Personalnummer selbst im Format `MA-0001`: Zeilenzahl der Tabelle plus eins, wobei soft-gelöschte Mitarbeiter mitzählen. Legt die Zeile in `employees` an (201); `address` und `metadata` werden als JSONB gespeichert, `status` steht ohne Angabe auf `active`. Erfordert mindestens die Rolle HR-Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string","minLength":1,"maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email"},"phone":{"type":"string","maxLength":50},"department":{"type":"string","minLength":1,"maxLength":100},"position":{"type":"string","minLength":1,"maxLength":100},"hiredAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"status":{"type":"string","enum":["active","inactive","on-leave"],"default":"active"},"salaryType":{"type":"string","enum":["monthly","hourly"],"default":"monthly"},"salary":{"type":"number","exclusiveMinimum":0},"managerId":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid"},"address":{"type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"}}},"metadata":{"type":"object","additionalProperties":{}}},"required":["firstName","lastName","email","department","position","hiredAt"]},"example":{"firstName":"string","lastName":"string","email":"beispiel@example.com","phone":"string","department":"string","position":"string","hiredAt":"2026-01-01T12:00:00.000Z","status":"active","salaryType":"monthly","salary":1,"managerId":"00000000-0000-4000-8000-000000000000","userId":"00000000-0000-4000-8000-000000000000","address":{"street":"string","city":"string","zip":"string","country":"string"},"metadata":{}}}}}}},"/api/v1/employees/capacity":{"get":{"responses":{"200":{"description":"Heatmap-Daten. `warning: \"no_time_data\"` heisst: Zeittabelle fehlt, die Nullstunden sind kein Befund. Ohne dieses Feld saehe „niemand hat gebucht\" identisch aus.","content":{"application/json":{"schema":{"type":"object","properties":{"employees":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"department":{"type":["string","null"]},"weeklyHours":{"type":"object","additionalProperties":{"type":"number"}}},"required":["id","firstName","lastName","department","weeklyHours"]}},"weeks":{"type":"array","items":{"type":"string"}},"warning":{"type":"string"}},"required":["employees","weeks"],"additionalProperties":false},"example":{"employees":[{"id":"string","firstName":"string","lastName":"string","department":"string","weeklyHours":{"beispiel":0}}],"weeks":["string"],"warning":"string"}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1EmployeesCapacity","tags":["employees"],"parameters":[{"in":"query","name":"from","schema":{"type":"string","pattern":"^\\d{4}-W\\d{2}$"}},{"in":"query","name":"to","schema":{"type":"string","pattern":"^\\d{4}-W\\d{2}$"}}],"summary":"Kapazitäts-Heatmap: gebuchte Stunden je Mitarbeiter und KW","description":"Summiert die Stunden aus `time_entries` je aktivem, nicht gelöschtem Mitarbeiter und ISO-Kalenderwoche. `from` und `to` im Format `YYYY-Www`; ohne Angabe reicht der Zeitraum von vier Wochen vor bis vier Wochen nach der laufenden Woche. Fehlt die Tabelle `time_entries` im Mandanten-Schema, bleibt die Antwort 200 mit Nullstunden und trägt zusätzlich `warning: \"no_time_data\"`."}},"/api/v1/employees/{id}":{"get":{"responses":{"200":{"description":"Mitarbeiter-Datensatz. `salary` nur bei Berechtigung, sonst null.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"employeeNumber":{},"firstName":{},"lastName":{},"email":{},"phone":{},"department":{},"position":{},"hiredAt":{},"status":{},"salaryType":{},"salary":{"type":["number","null"]},"managerId":{},"userId":{},"address":{},"metadata":{},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["salary","customFields"],"additionalProperties":false},"example":{"salary":0,"customFields":{}}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Mitarbeiter nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1EmployeesById","tags":["employees"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Liefert einen einzelnen Mitarbeiter","description":"Liest genau eine Zeile aus `employees` über die id im Pfad. Ein soft-gelöschter Mitarbeiter (`deleted_at` gesetzt) gilt als nicht vorhanden und ergibt 404 mit `{ error: \"employee_not_found\" }`. `salary` ist nur für HR-Manager gefüllt, sonst null."},"patch":{"responses":{"200":{"description":"Mitarbeiter aktualisiert. `salary` nur bei Berechtigung, sonst null.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"employeeNumber":{},"firstName":{},"lastName":{},"email":{},"phone":{},"department":{},"position":{},"hiredAt":{},"status":{},"salaryType":{},"salary":{"type":["number","null"]},"managerId":{},"userId":{},"address":{},"metadata":{},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["salary","customFields"],"additionalProperties":false},"example":{"salary":0,"customFields":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Mitarbeiter nicht gefunden"},"422":{"description":"Eigene Regeln haben den Schreibvorgang abgelehnt — es wurde NICHTS gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"entity_rule_violation"},"violations":{"type":"array","items":{"type":"object","additionalProperties":{}}},"message_de":{"type":"string"}},"required":["error","violations","message_de"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchApiV1EmployeesById","tags":["employees"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aktualisiert die Eigenen Felder (custom_fields) eines Mitarbeiters","description":"Schreibt AUSSCHLIESSLICH die JSONB-Spalte `custom_fields`; alle übrigen Stammdaten laufen über PUT /:id. Zusammengeführt wird im UPDATE selbst: ein nicht mitgeschickter Schlüssel bleibt stehen, `null` löscht ihn, ein Wert setzt ihn. Davor prüfen die Eigenen Regeln den Vorgang — bei einer Verletzung antwortet die Route mit 422 und speichert NICHTS. Danach wird die Zeile frisch gelesen und zurückgegeben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customFields":{"type":"object","additionalProperties":{}}}},"example":{"customFields":{}}}}}},"put":{"responses":{"200":{"description":"Mitarbeiter aktualisiert — hier IST `salary` gefuellt (HR-Manager)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"employeeNumber":{},"firstName":{},"lastName":{},"email":{},"phone":{},"department":{},"position":{},"hiredAt":{},"status":{},"salaryType":{},"salary":{"type":["number","null"]},"managerId":{},"userId":{},"address":{},"metadata":{},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["salary","customFields"],"additionalProperties":false},"example":{"salary":0,"customFields":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine HR-Manager-Rolle"},"404":{"description":"Mitarbeiter nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1EmployeesById","tags":["employees"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aktualisiert einen Mitarbeiter (nur HR-Manager+)","description":"VOLLSTÄNDIGES Überschreiben, kein Teil-Update: der Körper muss alle Pflichtfelder tragen, weggelassene optionale Felder werden auf null bzw. das leere JSONB-Objekt gesetzt. Personalnummer, Eigene Felder und `deleted_at` bleiben unberührt. Ein soft-gelöschter Mitarbeiter wird nicht getroffen und ergibt 404. Erfordert mindestens die Rolle HR-Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string","minLength":1,"maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email"},"phone":{"type":"string","maxLength":50},"department":{"type":"string","minLength":1,"maxLength":100},"position":{"type":"string","minLength":1,"maxLength":100},"hiredAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"status":{"type":"string","enum":["active","inactive","on-leave"],"default":"active"},"salaryType":{"type":"string","enum":["monthly","hourly"],"default":"monthly"},"salary":{"type":"number","exclusiveMinimum":0},"managerId":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid"},"address":{"type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"}}},"metadata":{"type":"object","additionalProperties":{}}},"required":["firstName","lastName","email","department","position","hiredAt"]},"example":{"firstName":"string","lastName":"string","email":"beispiel@example.com","phone":"string","department":"string","position":"string","hiredAt":"2026-01-01T12:00:00.000Z","status":"active","salaryType":"monthly","salary":1,"managerId":"00000000-0000-4000-8000-000000000000","userId":"00000000-0000-4000-8000-000000000000","address":{"street":"string","city":"string","zip":"string","country":"string"},"metadata":{}}}}}},"delete":{"responses":{"200":{"description":"Mitarbeiter gelöscht — nur eine Quittung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Admin-Rolle"},"404":{"description":"Mitarbeiter nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1EmployeesById","tags":["employees"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Löscht einen Mitarbeiter, nur Admin","description":"Soft-Delete: setzt `deleted_at` und `updated_at` auf NOW(), die Zeile bleibt in `employees` stehen und zählt weiter bei der Vergabe der nächsten Personalnummer. Die Verträge des Mitarbeiters werden nicht mitgelöscht. Quittiert mit einem deutschen Satz statt mit dem Datensatz. Erfordert die Rolle Admin."}},"/api/v1/employees/{id}/status":{"patch":{"responses":{"200":{"description":"Status aktualisiert. `salary` ist hier IMMER null — der Serialisierer wird ohne die Gehalts-Berechtigung aufgerufen, obwohl die Route Manager verlangt.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"employeeNumber":{},"firstName":{},"lastName":{},"email":{},"phone":{},"department":{},"position":{},"hiredAt":{},"status":{},"salaryType":{},"salary":{"type":["number","null"]},"managerId":{},"userId":{},"address":{},"metadata":{},"customFields":{"type":"object","additionalProperties":{}},"createdAt":{},"updatedAt":{}},"required":["salary","customFields"],"additionalProperties":false},"example":{"salary":0,"customFields":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Mitarbeiter nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchApiV1EmployeesByIdStatus","tags":["employees"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Setzt den Beschäftigungsstatus eines Mitarbeiters (nur Manager+)","description":"Schreibt allein die Spalte `status` (`active`, `inactive` oder `on-leave`) und `updated_at`; der übrige Datensatz bleibt unverändert. Ein soft-gelöschter Mitarbeiter wird nicht getroffen und ergibt 404. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["active","inactive","on-leave"]}},"required":["status"]},"example":{"status":"active"}}}}}},"/api/v1/employees/{id}/contracts":{"get":{"responses":{"200":{"description":"Liste der Verträge — Umschlag { data, total }, keine Paginierung","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"employeeId":{},"type":{},"startDate":{},"endDate":{},"salary":{"type":["number","null"]},"salaryType":{},"hoursPerWeek":{"type":["number","null"]},"notes":{},"createdAt":{},"updatedAt":{}},"required":["salary","hoursPerWeek"],"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"salary":0,"hoursPerWeek":0}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine HR-Berechtigung"},"404":{"description":"Mitarbeiter nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1EmployeesByIdContracts","tags":["employees"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Liefert alle Arbeitsverträge eines Mitarbeiters (nur HR-Manager+)","description":"Prüft zuerst, ob es den Mitarbeiter gibt und er nicht soft-gelöscht ist (sonst 404), und liest dann `employee_contracts` zu dieser `employee_id`, sortiert nach `start_date` absteigend. Keine Paginierung, keine Filter. Erfordert mindestens die Rolle HR-Manager."},"post":{"responses":{"201":{"description":"Vertrag angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"employeeId":{},"type":{},"startDate":{},"endDate":{},"salary":{"type":["number","null"]},"salaryType":{},"hoursPerWeek":{"type":["number","null"]},"notes":{},"createdAt":{},"updatedAt":{}},"required":["salary","hoursPerWeek"],"additionalProperties":false},"example":{"salary":0,"hoursPerWeek":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine HR-Manager-Rolle"},"404":{"description":"Mitarbeiter nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1EmployeesByIdContracts","tags":["employees"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Legt einen neuen Arbeitsvertrag für einen Mitarbeiter an (nur HR-Manager+)","description":"Legt eine Zeile in `employee_contracts` an und hängt sie über `employee_id` an den Mitarbeiter aus dem Pfad (201); ist der unbekannt oder soft-gelöscht, kommt 404. Pflicht sind `type` und `startDate`. Bereits bestehende Verträge werden weder beendet noch auf Überschneidung geprüft. Erfordert mindestens die Rolle HR-Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["permanent","fixed-term","freelance","apprenticeship"]},"startDate":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"endDate":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"salary":{"type":"number","exclusiveMinimum":0},"salaryType":{"type":"string","enum":["monthly","hourly"]},"hoursPerWeek":{"type":"number","exclusiveMinimum":0},"notes":{"type":"string"}},"required":["type","startDate"]},"example":{"type":"permanent","startDate":"2026-01-01T12:00:00.000Z","endDate":"2026-01-01T12:00:00.000Z","salary":1,"salaryType":"monthly","hoursPerWeek":1,"notes":"string"}}}}}},"/api/v1/purchasing/orders":{"get":{"responses":{"200":{"description":"Liste der Bestellungen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"orderNumber":{},"supplierId":{},"status":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"orderedAt":{},"expectedDelivery":{},"receivedAt":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","subtotal":0,"tax":0,"total":0}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1PurchasingOrders","tags":["purchasing"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","ordered","partial","received","invoiced","cancelled"]}},{"in":"query","name":"supplierId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"dateFrom","schema":{"type":"string","format":"date"}},{"in":"query","name":"dateTo","schema":{"type":"string","format":"date"}}],"summary":"List purchase orders","description":"Listet Bestellungen des Mandanten. `dateFrom`/`dateTo` filtern auf das Anlagedatum (created_at), NICHT auf das Bestelldatum (ordered_at). Soft-geloeschte Bestellungen sind nicht enthalten."},"post":{"responses":{"201":{"description":"Bestellung angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"orderNumber":{},"supplierId":{},"status":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"orderedAt":{},"expectedDelivery":{},"receivedAt":{},"notes":{},"createdAt":{},"updatedAt":{},"budgetCheck":{"type":"object","properties":{"mode":{},"reason":{},"hint":{}}}},"required":["id","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"tax":0,"total":0,"budgetCheck":{}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Budget-Hartgrenze erreicht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"hint":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PurchasingOrders","tags":["purchasing"],"parameters":[],"summary":"Create purchase order","description":"Legt eine neue Bestellung an; die Bestellnummer kommt aus dem Nummernkreis `purchase_number`. Ist eine `kostenstelle` gesetzt und `total` groesser 0, wird zuvor das Budget geprueft. Eine erreichte Hartgrenze lehnt die Anlage ab — der Handler antwortet dabei mit 409, nicht mit dem unten aufgefuehrten 403. Ein Soft-Limit legt die Bestellung an, schreibt den Ist-Betrag des Budgets fort und hinterlegt eine Warnung in der Inbox.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"supplierId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["draft","ordered","partial","received","invoiced","cancelled"],"default":"draft"},"items":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1},"quantity":{"type":"number","exclusiveMinimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0},"totalPrice":{"type":"number","minimum":0},"articleId":{"type":"string","format":"uuid"}},"required":["description","quantity","unitPrice","totalPrice"]},"default":[]},"subtotal":{"type":"number","minimum":0,"default":0},"tax":{"type":"number","minimum":0,"default":0},"total":{"type":"number","minimum":0,"default":0},"orderedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"expectedDelivery":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"receivedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"notes":{"type":"string"},"kostenstelle":{"type":"string"}},"required":["supplierId"]},"example":{"supplierId":"00000000-0000-4000-8000-000000000000","status":"draft","items":[{"description":"string","quantity":1,"unit":"string","unitPrice":0,"totalPrice":0,"articleId":"00000000-0000-4000-8000-000000000000"}],"subtotal":0,"tax":0,"total":0,"orderedAt":"2026-01-01T12:00:00.000Z","expectedDelivery":"2026-01-01T12:00:00.000Z","receivedAt":"2026-01-01T12:00:00.000Z","notes":"string","kostenstelle":"string"}}}}}},"/api/v1/purchasing/orders/{id}":{"get":{"responses":{"200":{"description":"Bestelldatensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"orderNumber":{},"supplierId":{},"status":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"orderedAt":{},"expectedDelivery":{},"receivedAt":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"tax":0,"total":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Bestellung nicht gefunden"}},"operationId":"getApiV1PurchasingOrdersById","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get purchase order","description":"Liefert eine einzelne Bestellung samt Positionen (`items`)."},"put":{"responses":{"200":{"description":"Bestellung aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"orderNumber":{},"supplierId":{},"status":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"orderedAt":{},"expectedDelivery":{},"receivedAt":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"tax":0,"total":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Bestellung nicht gefunden"}},"operationId":"putApiV1PurchasingOrdersById","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace purchase order","description":"Ersetzt eine Bestellung VOLLSTAENDIG. Nicht mitgesendete Felder bleiben nicht erhalten, sondern fallen auf den Vorgabewert des Schemas zurueck: `status` auf \"draft\", `items` auf [], `subtotal`/`tax`/`total` auf 0. Fuer eine reine Statusaenderung PATCH /purchasing/orders/{id}/status verwenden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"supplierId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["draft","ordered","partial","received","invoiced","cancelled"],"default":"draft"},"items":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1},"quantity":{"type":"number","exclusiveMinimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0},"totalPrice":{"type":"number","minimum":0},"articleId":{"type":"string","format":"uuid"}},"required":["description","quantity","unitPrice","totalPrice"]},"default":[]},"subtotal":{"type":"number","minimum":0,"default":0},"tax":{"type":"number","minimum":0,"default":0},"total":{"type":"number","minimum":0,"default":0},"orderedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"expectedDelivery":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"receivedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"notes":{"type":"string"},"kostenstelle":{"type":"string"}},"required":["supplierId"]},"example":{"supplierId":"00000000-0000-4000-8000-000000000000","status":"draft","items":[{"description":"string","quantity":1,"unit":"string","unitPrice":0,"totalPrice":0,"articleId":"00000000-0000-4000-8000-000000000000"}],"subtotal":0,"tax":0,"total":0,"orderedAt":"2026-01-01T12:00:00.000Z","expectedDelivery":"2026-01-01T12:00:00.000Z","receivedAt":"2026-01-01T12:00:00.000Z","notes":"string","kostenstelle":"string"}}}}},"delete":{"responses":{"200":{"description":"Bestellung gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Admin-Rolle"},"404":{"description":"Bestellung nicht gefunden"}},"operationId":"deleteApiV1PurchasingOrdersById","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete purchase order","description":"Soft-Delete: setzt `deleted_at`. Der Datensatz bleibt in der Datenbank, ist ueber die API aber nicht mehr sichtbar. Nur mit Admin-Rolle. Die Quittung ist ein Satz unter `message`, kein `{ok:true}`."}},"/api/v1/purchasing/orders/{id}/status":{"patch":{"responses":{"200":{"description":"Status aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"orderNumber":{},"supplierId":{},"status":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"orderedAt":{},"expectedDelivery":{},"receivedAt":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"tax":0,"total":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Bestellung nicht gefunden"}},"operationId":"patchApiV1PurchasingOrdersByIdStatus","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Set purchase order status","description":"Setzt den Status einer Bestellung. Jeder Wechsel ist erlaubt, es gibt keine geprueften Uebergaenge. \"received\" bucht KEINEN Wareneingang und veraendert keinen Lagerbestand — dafuer ist POST /api/v1/inventur/wareneingang zustaendig.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","ordered","partial","received","invoiced","cancelled"]}},"required":["status"]},"example":{"status":"draft"}}}}}},"/api/v1/purchasing/suppliers":{"get":{"responses":{"200":{"description":"Liste der Lieferanten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"supplierNumber":{},"name":{},"email":{},"phone":{},"address":{},"taxId":{},"vatId":{},"iban":{},"isOneTime":{},"paymentTerms":{},"currency":{},"status":{},"metadata":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1PurchasingSuppliers","tags":["purchasing"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive"]}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"List suppliers","description":"Listet Lieferanten des Mandanten, sortiert nach Name. `search` sucht in Name, E-Mail und Lieferantennummer. Soft-geloeschte Lieferanten sind nicht enthalten."},"post":{"responses":{"201":{"description":"Lieferant angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"supplierNumber":{},"name":{},"email":{},"phone":{},"address":{},"taxId":{},"vatId":{},"iban":{},"isOneTime":{},"paymentTerms":{},"currency":{},"status":{},"metadata":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"}},"operationId":"postApiV1PurchasingSuppliers","tags":["purchasing"],"parameters":[],"summary":"Create supplier","description":"Legt einen neuen Lieferanten an. Die Lieferantennummer kommt aus dem Kontakt-Nummernkreis `customer_number` — Lieferanten sind Kontakte vom Typ \"Lieferant\" und teilen sich den Nummernkreis mit den Kunden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"email":{"type":"string","format":"email"},"phone":{"type":"string","maxLength":50},"address":{"type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"}}},"taxId":{"type":"string","maxLength":50},"vatId":{"type":"string","maxLength":20},"iban":{"type":"string","maxLength":34},"isOneTime":{"type":"boolean"},"paymentTerms":{"type":"string","maxLength":100},"currency":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"status":{"type":"string","enum":["active","inactive"],"default":"active"},"metadata":{"type":"object","additionalProperties":{}}},"required":["name"]},"example":{"name":"string","email":"beispiel@example.com","phone":"string","address":{"street":"string","city":"string","zip":"string","country":"string"},"taxId":"string","vatId":"string","iban":"string","isOneTime":true,"paymentTerms":"string","currency":"str","status":"active","metadata":{}}}}}}},"/api/v1/purchasing/suppliers/{id}":{"get":{"responses":{"200":{"description":"Lieferanten-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"supplierNumber":{},"name":{},"email":{},"phone":{},"address":{},"taxId":{},"vatId":{},"iban":{},"isOneTime":{},"paymentTerms":{},"currency":{},"status":{},"metadata":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","supplierNumber":"LF-0001","name":"Stahlhandel Nord GmbH","email":"einkauf@example.com","phone":"+49 40 1234567","address":{"street":"Hafenstr. 1","zip":"20457","city":"Hamburg","country":"DE"},"taxId":null,"vatId":"DE123456789","iban":"DE00 XXXX XXXX XXXX XXXX XX","isOneTime":false,"paymentTerms":"30 Tage netto","currency":"EUR","status":"active","metadata":{},"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Lieferant nicht gefunden"}},"operationId":"getApiV1PurchasingSuppliersById","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get supplier","description":"Liefert einen einzelnen Lieferanten."},"put":{"responses":{"200":{"description":"Lieferant aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"supplierNumber":{},"name":{},"email":{},"phone":{},"address":{},"taxId":{},"vatId":{},"iban":{},"isOneTime":{},"paymentTerms":{},"currency":{},"status":{},"metadata":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Lieferant nicht gefunden"}},"operationId":"putApiV1PurchasingSuppliersById","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace supplier","description":"Ersetzt die Stammdaten eines Lieferanten. Nicht mitgesendete Felder werden geleert — mit DREI Ausnahmen: `vatId`, `iban` und `isOneTime` behalten ihren bisherigen Wert, wenn sie fehlen (COALESCE), damit aeltere Formulare sie nicht bei jedem Speichern zuruecksetzen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"email":{"type":"string","format":"email"},"phone":{"type":"string","maxLength":50},"address":{"type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"}}},"taxId":{"type":"string","maxLength":50},"vatId":{"type":"string","maxLength":20},"iban":{"type":"string","maxLength":34},"isOneTime":{"type":"boolean"},"paymentTerms":{"type":"string","maxLength":100},"currency":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"status":{"type":"string","enum":["active","inactive"],"default":"active"},"metadata":{"type":"object","additionalProperties":{}}},"required":["name"]},"example":{"name":"string","email":"beispiel@example.com","phone":"string","address":{"street":"string","city":"string","zip":"string","country":"string"},"taxId":"string","vatId":"string","iban":"string","isOneTime":true,"paymentTerms":"string","currency":"str","status":"active","metadata":{}}}}}},"delete":{"responses":{"200":{"description":"Lieferant gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Admin-Rolle"},"404":{"description":"Lieferant nicht gefunden"}},"operationId":"deleteApiV1PurchasingSuppliersById","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete supplier","description":"Soft-Delete: setzt `deleted_at`. Der Datensatz bleibt in der Datenbank, ist ueber die API aber nicht mehr sichtbar. Bestellungen und Eingangsrechnungen des Lieferanten bleiben unveraendert bestehen. Nur mit Admin-Rolle."}},"/api/v1/purchasing/suppliers/{id}/stats":{"get":{"responses":{"200":{"description":"Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"orders":{"type":"number"},"deliveries":{"type":"number"},"invoices":{"type":"number"},"credit_notes":{"type":"number"}},"required":["orders","deliveries","invoices","credit_notes"],"additionalProperties":false},"example":{"orders":0,"deliveries":0,"invoices":0,"credit_notes":0}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1PurchasingSuppliersByIdStats","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get supplier document counts","description":"Belegzaehler fuer die SmartButtons der Lieferanten-Detailseite. NUR `orders` und `invoices` werden wirklich gezaehlt; `deliveries` und `credit_notes` sind fest 0 und derzeit NICHT erhoben. Faellt eine Zaehlung aus, antwortet die Route weiterhin mit HTTP 200 und lauter Nullen — eine 0 heisst hier also nicht zwingend \"keine Belege\"."}},"/api/v1/purchasing/stats":{"get":{"responses":{"200":{"description":"Einkauf-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"openOrders":{"type":"number"},"totalVolume":{"type":"number"},"topSuppliers":{"type":"array","items":{"type":"object","properties":{"supplierId":{},"supplierName":{"type":"string"},"orderCount":{"type":"number"}},"required":["supplierName","orderCount"]}}},"required":["openOrders","totalVolume","topSuppliers"],"additionalProperties":false},"example":{"openOrders":0,"totalVolume":0,"topSuppliers":[{"supplierName":"string","orderCount":0}]}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1PurchasingStats","tags":["purchasing"],"parameters":[],"summary":"Get purchasing statistics","description":"Einkaufs-Kennzahlen. `openOrders` zaehlt alles ausser received/invoiced/cancelled, `totalVolume` summiert alle nicht stornierten Bestellungen. `topSuppliers` sind die fuenf Lieferanten mit dem groessten Bestellvolumen; ein Lieferant, dessen Stammsatz fehlt, erscheint als \"Unbekannt\"."}},"/api/v1/purchasing/eingangsrechnungen/stats":{"get":{"responses":{"200":{"description":"Eingangsrechnungs-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"offen":{"type":"number"},"fälligDieseWoche":{"type":"number"},"geprueft":{"type":"number"},"bezahltDiesenMonat":{"type":"number"},"summenOffen":{"type":"number"},"summenBezahlt":{"type":"number"}},"required":["offen","fälligDieseWoche","geprueft","bezahltDiesenMonat","summenOffen","summenBezahlt"],"additionalProperties":false},"example":{"offen":0,"fälligDieseWoche":0,"geprueft":0,"bezahltDiesenMonat":0,"summenOffen":0,"summenBezahlt":0}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1PurchasingEingangsrechnungenStats","tags":["purchasing"],"parameters":[],"summary":"Get Eingangsrechnung statistics","description":"Kennzahlen zu Eingangsrechnungen. \"faellig diese Woche\" heisst: offen und Faelligkeit zwischen heute und heute+7 Tagen — bereits ueberfaellige Rechnungen sind darin NICHT enthalten. \"bezahlt diesen Monat\" geht nach dem Aenderungsdatum, nicht nach einem Zahldatum. Der Schluessel `fälligDieseWoche` traegt einen Umlaut."}},"/api/v1/purchasing/eingangsrechnungen":{"get":{"responses":{"200":{"description":"Liste der Eingangsrechnungen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"rechnungsnummer":{},"lieferantName":{},"lieferantId":{},"betreff":{},"betragNetto":{"type":"number"},"mwstSatz":{"type":"number"},"mwstBetrag":{"type":"number"},"betragBrutto":{"type":"number"},"faelligAm":{},"status":{},"purchaseOrderId":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["betragNetto","mwstSatz","mwstBetrag","betragBrutto"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"betragNetto":0,"mwstSatz":0,"mwstBetrag":0,"betragBrutto":0}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1PurchasingEingangsrechnungen","tags":["purchasing"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["offen","bezahlt","storniert","geprueft"]}}],"summary":"List Eingangsrechnungen (supplier invoices)","description":"Listet Eingangsrechnungen (Lieferantenrechnungen) des Mandanten, neueste zuerst. Diese Tabelle kennt kein Soft-Delete: was hier fehlt, ist endgueltig geloescht."},"post":{"responses":{"201":{"description":"Eingangsrechnung angelegt und gebucht","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"rechnungsnummer":{},"lieferantName":{},"lieferantId":{},"betreff":{},"betragNetto":{"type":"number"},"mwstSatz":{"type":"number"},"mwstBetrag":{"type":"number"},"betragBrutto":{"type":"number"},"faelligAm":{},"status":{},"purchaseOrderId":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["betragNetto","mwstSatz","mwstBetrag","betragBrutto"],"additionalProperties":false},"example":{"betragNetto":0,"mwstSatz":0,"mwstBetrag":0,"betragBrutto":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"423":{"description":"Buchungsperiode geschlossen — zurueckgerollt, die Rechnung wurde nicht angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"hint":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank- oder Journalfehler — zurueckgerollt, die Rechnung wurde nicht angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"hint":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PurchasingEingangsrechnungen","tags":["purchasing"],"parameters":[],"summary":"Create Eingangsrechnung","description":"Legt eine Eingangsrechnung an und BUCHT sie zugleich ins Journal: Wareneingang (SKR03 3400) und Vorsteuer (1576 bzw. 1571 bei 7 %) gegen das Verbindlichkeitskonto der Buchungsgruppe des Lieferanten, ersatzweise 1600. MwSt-Betrag und Bruttobetrag rechnet der Server aus `betragNetto` und `mwstSatz` — sie sind keine Eingabe. Rechnung und Journalbuchung sind EIN Vorgang in EINER Transaktion: gelingt die Buchung nicht, existiert auch die Rechnung nicht. Bei 423 (Buchungsperiode geschlossen) ist demnach nichts angelegt, und der Wiederholversuch nach dem Oeffnen der Periode legt den Beleg genau einmal an. Scheitert die Buchung aus einem anderen Grund, antwortet die Route 503 statt 201 — eine Rechnung ohne Journalsatz entsteht nicht. Einzige Ausnahme: bei `betragNetto` = 0 gibt es nichts zu buchen (das Journal weist Betrag 0 ab). Die Rechnung wird dann angelegt und bleibt ohne Journalsatz.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lieferantName":{"type":"string","minLength":1,"maxLength":255},"lieferantId":{"type":"string"},"rechnungsnummer":{"type":"string","maxLength":100},"betreff":{"type":"string","default":""},"betragNetto":{"type":"number","minimum":0},"mwstSatz":{"type":"number","minimum":0,"maximum":100,"default":19},"faelligAm":{"type":"string","format":"date"},"notizen":{"type":"string"},"purchaseOrderId":{"type":"string"}},"required":["lieferantName","betragNetto"]},"example":{"lieferantName":"string","lieferantId":"string","rechnungsnummer":"string","betreff":"string","betragNetto":0,"mwstSatz":0,"faelligAm":"2026-01-01","notizen":"string","purchaseOrderId":"string"}}}}}},"/api/v1/purchasing/eingangsrechnungen/{id}":{"get":{"responses":{"200":{"description":"Eingangsrechnungs-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"rechnungsnummer":{},"lieferantName":{},"lieferantId":{},"betreff":{},"betragNetto":{"type":"number"},"mwstSatz":{"type":"number"},"mwstBetrag":{"type":"number"},"betragBrutto":{"type":"number"},"faelligAm":{},"status":{},"purchaseOrderId":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["betragNetto","mwstSatz","mwstBetrag","betragBrutto"],"additionalProperties":false},"example":{"betragNetto":0,"mwstSatz":0,"mwstBetrag":0,"betragBrutto":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Eingangsrechnung nicht gefunden"}},"operationId":"getApiV1PurchasingEingangsrechnungenById","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get Eingangsrechnung","description":"Liefert eine einzelne Eingangsrechnung des Mandanten."},"delete":{"responses":{"200":{"description":"Eingangsrechnung gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Eingangsrechnung nicht gefunden"},"409":{"description":"Nur offene oder stornierte Rechnungen sind löschbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"hint":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1PurchasingEingangsrechnungenById","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete Eingangsrechnung","description":"ENDGUELTIGES Loeschen — anders als bei Bestellungen und Lieferanten wird die Zeile wirklich entfernt, nicht nur als geloescht markiert. Erlaubt nur im Status \"offen\" oder \"storniert\", sonst 409. Bereits erzeugte Journalbuchungen bleiben stehen und zeigen danach auf einen Beleg, den es nicht mehr gibt. Nur mit Admin-Rolle."}},"/api/v1/purchasing/eingangsrechnungen/{id}/status":{"patch":{"responses":{"200":{"description":"Status aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"rechnungsnummer":{},"lieferantName":{},"lieferantId":{},"betreff":{},"betragNetto":{"type":"number"},"mwstSatz":{"type":"number"},"mwstBetrag":{"type":"number"},"betragBrutto":{"type":"number"},"faelligAm":{},"status":{},"purchaseOrderId":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["betragNetto","mwstSatz","mwstBetrag","betragBrutto"],"additionalProperties":false},"example":{"betragNetto":0,"mwstSatz":0,"mwstBetrag":0,"betragBrutto":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Eingangsrechnung nicht gefunden"}},"operationId":"patchApiV1PurchasingEingangsrechnungenByIdStatus","tags":["purchasing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Set Eingangsrechnung status","description":"Setzt den Status (offen | geprueft | bezahlt | storniert). Es werden nur das Statusfeld und `updated_at` geschrieben: die Buchung im Journal bleibt unveraendert, \"bezahlt\" erzeugt KEINE Zahlung und \"storniert\" KEINE Stornobuchung. Achtung, die Statistik leitet \"bezahlt diesen Monat\" aus `updated_at` ab — jede Statusaenderung verschiebt die Rechnung dort.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["offen","bezahlt","storniert","geprueft"]}},"required":["status"]},"example":{"status":"offen"}}}}}},"/api/v1/einkauf/lieferanten/{id}/catalog":{"get":{"responses":{"200":{"description":"Die Konditionen dieses Lieferanten. Als einziger Endpunkt dieser Datei fuellt er `artikelSku` und `artikelName` aus dem eigenen Artikelstamm.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Eintrags (UUID als Text, vom Server vergeben)"},"lieferantId":{"type":"string","minLength":1,"description":"Der Lieferant. Zusammen mit `artikelId` eindeutig."},"artikelId":{"type":"string","minLength":1,"description":"Der eigene Artikel. Zusammen mit `lieferantId` eindeutig."},"lieferantenArtikelNr":{"type":["string","null"],"description":"Artikelnummer des Lieferanten fuer dieses Teil. `null`, wenn nicht gepflegt."},"lieferantenPreis":{"type":"number","minimum":0,"description":"Preis des Lieferanten, bis zu vier Nachkommastellen. Fehlt er, steht hier `0`."},"preisEinheit":{"type":"string","description":"Bezugsgroesse des Preises, z. B. `STK` oder `100M`. Voreinstellung ist `STK`."},"leadTimeTage":{"type":["integer","null"],"description":"Zugesagte Lieferzeit in Tagen. `null`, wenn nicht gepflegt."},"mindestBestellmenge":{"type":"number","minimum":0,"description":"Kleinste bestellbare Menge. Ohne Angabe `1`, nicht `null`."},"gueltigAb":{"type":["string","null"],"description":"Beginn der Preisgueltigkeit als Kalendertag. `null` heisst: ab sofort."},"gueltigBis":{"type":["string","null"],"description":"Ende der Preisgueltigkeit als Kalendertag. `null` heisst: unbefristet."},"isPreferred":{"type":"boolean","description":"Vorzugslieferant fuer diesen Artikel. Sortiert die Liste des Lieferanten nach oben."},"notes":{"type":["string","null"],"description":"Freitext zur Kondition. `null`, wenn keiner erfasst ist."},"importedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des letzten CSV-Imports. `null` bei von Hand gepflegten Eintraegen."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."},"artikelSku":{"type":["string","null"],"description":"Artikelnummer aus dem eigenen Stamm. Nur die Katalogliste eines Lieferanten fuellt sie; sonst immer `null`."},"artikelName":{"type":["string","null"],"description":"Artikelbezeichnung aus dem eigenen Stamm. Sonst wie `artikelSku` immer `null`."}},"required":["id","lieferantId","artikelId","lieferantenArtikelNr","lieferantenPreis","preisEinheit","leadTimeTage","mindestBestellmenge","gueltigAb","gueltigBis","isPreferred","notes","importedAt","createdAt","updatedAt","artikelSku","artikelName"],"additionalProperties":false},"description":"Die Konditionen dieses Lieferanten: Vorzugseintraege zuerst, dann guenstigster Preis."},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, dessen Katalog gelesen wurde."},"source":{"type":"string","const":"db","description":"Immer `db`. Die Liste stammt nie aus einem Zwischenspeicher."},"count":{"type":"integer","minimum":0,"description":"Anzahl der zurueckgegebenen Eintraege."}},"required":["tenantId","source","count"],"additionalProperties":false,"description":"Angaben zur Abfrage. Eine Blaetterung gibt es hier nicht."}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","lieferantId":"string","artikelId":"string","lieferantenArtikelNr":"string","lieferantenPreis":0,"preisEinheit":"string","leadTimeTage":0,"mindestBestellmenge":0,"gueltigAb":"string","gueltigBis":"string","isPreferred":true,"notes":"string","importedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","artikelSku":"string","artikelName":"string"}],"meta":{"tenantId":"string","source":"db","count":0}}}}},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"503":{"description":"Datenbank nicht erreichbar oder das Anlegen der Tabellen schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1EinkaufLieferantenByIdCatalog","tags":["einkauf","catalog"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lieferantenkatalog auflisten","description":"Listet den Artikelkatalog eines Lieferanten"},"post":{"responses":{"201":{"description":"Der Eintrag, unverpackt. IMMER 201 — auch wenn es die Kondition schon gab und sie nur ueberschrieben wurde. Ob angelegt oder ersetzt, sagt diese Antwort nicht. `artikelSku` und `artikelName` sind hier immer `null`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Eintrags (UUID als Text, vom Server vergeben)"},"lieferantId":{"type":"string","minLength":1,"description":"Der Lieferant. Zusammen mit `artikelId` eindeutig."},"artikelId":{"type":"string","minLength":1,"description":"Der eigene Artikel. Zusammen mit `lieferantId` eindeutig."},"lieferantenArtikelNr":{"type":["string","null"],"description":"Artikelnummer des Lieferanten fuer dieses Teil. `null`, wenn nicht gepflegt."},"lieferantenPreis":{"type":"number","minimum":0,"description":"Preis des Lieferanten, bis zu vier Nachkommastellen. Fehlt er, steht hier `0`."},"preisEinheit":{"type":"string","description":"Bezugsgroesse des Preises, z. B. `STK` oder `100M`. Voreinstellung ist `STK`."},"leadTimeTage":{"type":["integer","null"],"description":"Zugesagte Lieferzeit in Tagen. `null`, wenn nicht gepflegt."},"mindestBestellmenge":{"type":"number","minimum":0,"description":"Kleinste bestellbare Menge. Ohne Angabe `1`, nicht `null`."},"gueltigAb":{"type":["string","null"],"description":"Beginn der Preisgueltigkeit als Kalendertag. `null` heisst: ab sofort."},"gueltigBis":{"type":["string","null"],"description":"Ende der Preisgueltigkeit als Kalendertag. `null` heisst: unbefristet."},"isPreferred":{"type":"boolean","description":"Vorzugslieferant fuer diesen Artikel. Sortiert die Liste des Lieferanten nach oben."},"notes":{"type":["string","null"],"description":"Freitext zur Kondition. `null`, wenn keiner erfasst ist."},"importedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des letzten CSV-Imports. `null` bei von Hand gepflegten Eintraegen."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."},"artikelSku":{"type":["string","null"],"description":"Artikelnummer aus dem eigenen Stamm. Nur die Katalogliste eines Lieferanten fuellt sie; sonst immer `null`."},"artikelName":{"type":["string","null"],"description":"Artikelbezeichnung aus dem eigenen Stamm. Sonst wie `artikelSku` immer `null`."}},"required":["id","lieferantId","artikelId","lieferantenArtikelNr","lieferantenPreis","preisEinheit","leadTimeTage","mindestBestellmenge","gueltigAb","gueltigBis","isPreferred","notes","importedAt","createdAt","updatedAt","artikelSku","artikelName"],"additionalProperties":false},"example":{"id":"string","lieferantId":"string","artikelId":"string","lieferantenArtikelNr":"string","lieferantenPreis":0,"preisEinheit":"string","leadTimeTage":0,"mindestBestellmenge":0,"gueltigAb":"string","gueltigBis":"string","isPreferred":true,"notes":"string","importedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","artikelSku":"string","artikelName":"string"}}}},"400":{"description":"Eingabe ungueltig"},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"503":{"description":"Datenbank nicht erreichbar oder das Schreiben schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufLieferantenByIdCatalog","tags":["einkauf","catalog"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Katalog-Eintrag anlegen oder ersetzen","description":"Erstellt oder aktualisiert (UPSERT) einen Katalog-Eintrag","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"artikelId":{"type":"string","minLength":1},"lieferantenArtikelNr":{"type":["string","null"]},"lieferantenPreis":{"type":"number","minimum":0},"preisEinheit":{"type":"string","default":"STK"},"leadTimeTage":{"type":["integer","null"],"minimum":0},"mindestBestellmenge":{"type":"number","minimum":0,"default":1},"gueltigAb":{"type":["string","null"],"format":"date"},"gueltigBis":{"type":["string","null"],"format":"date"},"isPreferred":{"type":"boolean","default":false},"notes":{"type":["string","null"]}},"required":["artikelId","lieferantenPreis"]},"example":{"artikelId":"string","lieferantenArtikelNr":"string","lieferantenPreis":0,"preisEinheit":"string","leadTimeTage":0,"mindestBestellmenge":0,"gueltigAb":"2026-01-01","gueltigBis":"2026-01-01","isPreferred":true,"notes":"string"}}}}}},"/api/v1/einkauf/lieferanten/{id}/catalog/{entryId}":{"patch":{"responses":{"200":{"description":"Der geaenderte Eintrag im Zustand nach der Aenderung. `artikelSku` und `artikelName` sind hier immer `null`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Eintrags (UUID als Text, vom Server vergeben)"},"lieferantId":{"type":"string","minLength":1,"description":"Der Lieferant. Zusammen mit `artikelId` eindeutig."},"artikelId":{"type":"string","minLength":1,"description":"Der eigene Artikel. Zusammen mit `lieferantId` eindeutig."},"lieferantenArtikelNr":{"type":["string","null"],"description":"Artikelnummer des Lieferanten fuer dieses Teil. `null`, wenn nicht gepflegt."},"lieferantenPreis":{"type":"number","minimum":0,"description":"Preis des Lieferanten, bis zu vier Nachkommastellen. Fehlt er, steht hier `0`."},"preisEinheit":{"type":"string","description":"Bezugsgroesse des Preises, z. B. `STK` oder `100M`. Voreinstellung ist `STK`."},"leadTimeTage":{"type":["integer","null"],"description":"Zugesagte Lieferzeit in Tagen. `null`, wenn nicht gepflegt."},"mindestBestellmenge":{"type":"number","minimum":0,"description":"Kleinste bestellbare Menge. Ohne Angabe `1`, nicht `null`."},"gueltigAb":{"type":["string","null"],"description":"Beginn der Preisgueltigkeit als Kalendertag. `null` heisst: ab sofort."},"gueltigBis":{"type":["string","null"],"description":"Ende der Preisgueltigkeit als Kalendertag. `null` heisst: unbefristet."},"isPreferred":{"type":"boolean","description":"Vorzugslieferant fuer diesen Artikel. Sortiert die Liste des Lieferanten nach oben."},"notes":{"type":["string","null"],"description":"Freitext zur Kondition. `null`, wenn keiner erfasst ist."},"importedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des letzten CSV-Imports. `null` bei von Hand gepflegten Eintraegen."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."},"artikelSku":{"type":["string","null"],"description":"Artikelnummer aus dem eigenen Stamm. Nur die Katalogliste eines Lieferanten fuellt sie; sonst immer `null`."},"artikelName":{"type":["string","null"],"description":"Artikelbezeichnung aus dem eigenen Stamm. Sonst wie `artikelSku` immer `null`."}},"required":["id","lieferantId","artikelId","lieferantenArtikelNr","lieferantenPreis","preisEinheit","leadTimeTage","mindestBestellmenge","gueltigAb","gueltigBis","isPreferred","notes","importedAt","createdAt","updatedAt","artikelSku","artikelName"],"additionalProperties":false},"example":{"id":"string","lieferantId":"string","artikelId":"string","lieferantenArtikelNr":"string","lieferantenPreis":0,"preisEinheit":"string","leadTimeTage":0,"mindestBestellmenge":0,"gueltigAb":"string","gueltigBis":"string","isPreferred":true,"notes":"string","importedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","artikelSku":"string","artikelName":"string"}}}},"400":{"description":"ZWEI FORMEN, weil zwei Stellen ablehnen: der Handler, wenn der Rumpf kein bekanntes Feld enthaelt, und der Eingabe-Validator bei ungueltigen Werten.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"no_fields_to_update","description":"Fester Fehlerschluessel. Der Rumpf trug kein Feld, das dieser Endpunkt kennt."}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false`. Daran ist die Antwort des Validators erkennbar."},"error":{"type":"object","additionalProperties":{},"description":"Der Zod-Fehler als Objekt; die Einzelbefunde stehen in seiner Liste `issues`."}},"required":["success","error"],"additionalProperties":false}]}}}},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"404":{"description":"Kein Eintrag mit dieser Kennung bei diesem Lieferanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"entry_not_found","description":"Fester Fehlerschluessel. Auch ein Eintrag, den es gibt, der aber zu einem ANDEREN Lieferanten gehoert, faellt hierunter."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das UPDATE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1EinkaufLieferantenByIdCatalogByEntryId","tags":["einkauf","catalog"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"entryId","required":true}],"summary":"Katalog-Eintrag aendern","description":"Aktualisiert einen Katalog-Eintrag","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"artikelId":{"type":"string","minLength":1},"lieferantenArtikelNr":{"type":["string","null"]},"lieferantenPreis":{"type":"number","minimum":0},"preisEinheit":{"type":"string","default":"STK"},"leadTimeTage":{"type":["integer","null"],"minimum":0},"mindestBestellmenge":{"type":"number","minimum":0,"default":1},"gueltigAb":{"type":["string","null"],"format":"date"},"gueltigBis":{"type":["string","null"],"format":"date"},"isPreferred":{"type":"boolean","default":false},"notes":{"type":["string","null"]}}},"example":{"artikelId":"string","lieferantenArtikelNr":"string","lieferantenPreis":0,"preisEinheit":"string","leadTimeTage":0,"mindestBestellmenge":0,"gueltigAb":"2026-01-01","gueltigBis":"2026-01-01","isPreferred":true,"notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung mit der geloeschten Kennung. Endgueltig, kein Soft-Delete.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`. Ein Fehlschlag kommt als 404 oder 503."},"deletedId":{"type":"string","minLength":1,"description":"Kennung des geloeschten Eintrags."}},"required":["ok","deletedId"],"additionalProperties":false},"example":{"ok":true,"deletedId":"string"}}}},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"404":{"description":"Kein Eintrag mit dieser Kennung bei diesem Lieferanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"entry_not_found","description":"Fester Fehlerschluessel. Auch ein Eintrag, den es gibt, der aber zu einem ANDEREN Lieferanten gehoert, faellt hierunter."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das DELETE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1EinkaufLieferantenByIdCatalogByEntryId","tags":["einkauf","catalog"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"entryId","required":true}],"summary":"Katalog-Eintrag loeschen","description":"Löscht einen Katalog-Eintrag"}},"/api/v1/einkauf/lieferanten/{id}/catalog/import-csv":{"post":{"responses":{"200":{"description":"Der Import ist durchgelaufen. ACHTUNG: 200 heisst NICHT „alles hat geklappt\" — abgelehnte Zeilen stehen in `errors`, waehrend die uebrigen gebucht wurden. Wer nur die Kennzahl prueft, uebersieht sie.","content":{"application/json":{"schema":{"type":"object","properties":{"imported":{"type":"integer","minimum":0,"description":"Wie viele Eintraege NEU angelegt wurden."},"updated":{"type":"integer","minimum":0,"description":"Wie viele bestehende Eintraege ueberschrieben wurden (gleicher Lieferant und Artikel)."},"errors":{"type":"array","items":{"type":"object","properties":{"row":{"type":"integer","minimum":0,"description":"Zeilennummer in der Datei, 1-basiert und mit Kopfzeile gezaehlt. `0` steht fuer einen Fehler OHNE Zeilenbezug — dann war die Artikelnummer im Stamm unbekannt."},"message":{"type":"string","minLength":1,"description":"Grund in Klartext."}},"required":["row","message"],"additionalProperties":false},"description":"Abgelehnte Zeilen. Der Import bricht deswegen NICHT ab — die uebrigen Zeilen laufen durch, und die Antwort kommt trotzdem als 200."}},"required":["imported","updated","errors"],"additionalProperties":false},"example":{"imported":0,"updated":0,"errors":[{"row":0,"message":"string"}]}}}},"400":{"description":"Der Kopfzeile fehlt eine Pflichtspalte. Fehler beim LESEN des Formulars (kein `file`-Feld, unlesbares Multipart) kommen dagegen als text/plain.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"csv_headers_missing","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"missing":{"type":"array","items":{"type":"string"},"description":"Welche Pflichtspalten fehlen — `artikel_nr_intern` und/oder `lieferanten_preis`."},"headersFound":{"type":"array","items":{"type":"string"},"description":"Die tatsaechlich gefundenen Spalten, kleingeschrieben. Hilft beim Trennzeichen-Verdacht."}},"required":["error","missing","headersFound"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"415":{"description":"Inhaltstyp ist nicht `multipart/form-data`. Als Text."},"503":{"description":"Datenbank nicht erreichbar. Der Import ist dann TEILWEISE gelaufen — es gibt keine umschliessende Transaktion, jede Zeile wird einzeln gebucht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufLieferantenByIdCatalogImport-csv","tags":["einkauf","catalog"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lieferanten-Katalog aus CSV importieren","description":"CSV-Bulk-Import (UPSERT) für den Lieferanten-Katalog"}},"/api/v1/einkauf/artikel/{artikelId}/lieferanten":{"get":{"responses":{"200":{"description":"Alle Lieferanten dieses Artikels mit ihren Konditionen, guenstigster Preis zuerst — ohne Ruecksicht auf `isPreferred`. Hier kommen `lieferantName` und `lieferantNummer` hinzu, waehrend `artikelSku`/`artikelName` `null` bleiben.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Eintrags (UUID als Text, vom Server vergeben)"},"lieferantId":{"type":"string","minLength":1,"description":"Der Lieferant. Zusammen mit `artikelId` eindeutig."},"artikelId":{"type":"string","minLength":1,"description":"Der eigene Artikel. Zusammen mit `lieferantId` eindeutig."},"lieferantenArtikelNr":{"type":["string","null"],"description":"Artikelnummer des Lieferanten fuer dieses Teil. `null`, wenn nicht gepflegt."},"lieferantenPreis":{"type":"number","minimum":0,"description":"Preis des Lieferanten, bis zu vier Nachkommastellen. Fehlt er, steht hier `0`."},"preisEinheit":{"type":"string","description":"Bezugsgroesse des Preises, z. B. `STK` oder `100M`. Voreinstellung ist `STK`."},"leadTimeTage":{"type":["integer","null"],"description":"Zugesagte Lieferzeit in Tagen. `null`, wenn nicht gepflegt."},"mindestBestellmenge":{"type":"number","minimum":0,"description":"Kleinste bestellbare Menge. Ohne Angabe `1`, nicht `null`."},"gueltigAb":{"type":["string","null"],"description":"Beginn der Preisgueltigkeit als Kalendertag. `null` heisst: ab sofort."},"gueltigBis":{"type":["string","null"],"description":"Ende der Preisgueltigkeit als Kalendertag. `null` heisst: unbefristet."},"isPreferred":{"type":"boolean","description":"Vorzugslieferant fuer diesen Artikel. Sortiert die Liste des Lieferanten nach oben."},"notes":{"type":["string","null"],"description":"Freitext zur Kondition. `null`, wenn keiner erfasst ist."},"importedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des letzten CSV-Imports. `null` bei von Hand gepflegten Eintraegen."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."},"artikelSku":{"type":["string","null"],"description":"Artikelnummer aus dem eigenen Stamm. Nur die Katalogliste eines Lieferanten fuellt sie; sonst immer `null`."},"artikelName":{"type":["string","null"],"description":"Artikelbezeichnung aus dem eigenen Stamm. Sonst wie `artikelSku` immer `null`."},"lieferantName":{"type":["string","null"],"description":"Name des Lieferanten aus dem Stamm. `null`, wenn der Stammsatz fehlt."},"lieferantNummer":{"type":["string","null"],"description":"Lieferantennummer aus dem Stamm. `null`, wenn der Stammsatz fehlt."}},"required":["id","lieferantId","artikelId","lieferantenArtikelNr","lieferantenPreis","preisEinheit","leadTimeTage","mindestBestellmenge","gueltigAb","gueltigBis","isPreferred","notes","importedAt","createdAt","updatedAt","artikelSku","artikelName","lieferantName","lieferantNummer"],"additionalProperties":false},"description":"Alle Lieferanten dieses Artikels, guenstigster Preis zuerst."},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, dessen Katalog gelesen wurde."},"count":{"type":"integer","minimum":0,"description":"Anzahl der zurueckgegebenen Lieferanten."}},"required":["tenantId","count"],"additionalProperties":false,"description":"Angaben zur Abfrage. Ein `source` gibt es hier — anders als bei der Katalogliste — nicht."}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","lieferantId":"string","artikelId":"string","lieferantenArtikelNr":"string","lieferantenPreis":0,"preisEinheit":"string","leadTimeTage":0,"mindestBestellmenge":0,"gueltigAb":"string","gueltigBis":"string","isPreferred":true,"notes":"string","importedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","artikelSku":"string","artikelName":"string","lieferantName":"string","lieferantNummer":"string"}],"meta":{"tenantId":"string","count":0}}}}},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"503":{"description":"Datenbank nicht erreichbar oder das Anlegen der Tabellen schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1EinkaufArtikelByArtikelIdLieferanten","tags":["einkauf","catalog"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"artikelId","required":true}],"summary":"Lieferanten eines Artikels auflisten","description":"Listet alle Lieferanten eines Artikels (sortiert nach Preis aufsteigend)"}},"/api/v1/einkauf/budgets":{"get":{"responses":{"200":{"description":"Die Budgets des Mandanten, neueste Periode zuerst. Ungeblaettert.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Budgets (UUID als Text, vom Server vergeben)"},"kostenstelle":{"type":"string","description":"Kostenstelle, fuer die das Budget gilt. Leer, wenn nicht gepflegt."},"periodeJahr":{"type":["integer","null"],"description":"Jahr der Periode. `null` nur bei unvollstaendigen Altdaten."},"periodeQuartal":{"type":["integer","null"],"minimum":1,"maximum":4,"description":"Quartal 1 bis 4. `null` heisst: das Budget gilt fuer das ganze Jahr."},"periodeMonat":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat 1 bis 12. `null` heisst: das Budget gilt fuer Quartal oder Jahr."},"budgetBetrag":{"type":"number","minimum":0,"description":"Bewilligtes Budget in Euro."},"istBetrag":{"type":"number","description":"Bereits verbrauchter Betrag in Euro."},"deltaBetrag":{"type":"number","description":"Budget minus Verbrauch, auf zwei Nachkommastellen. Negativ heisst: ueberzogen."},"auslastungPct":{"type":"number","minimum":0,"description":"Verbrauch in PROZENT des Budgets, zwei Nachkommastellen. Werte ueber 100 sind moeglich. Bei Budget `0` steht hier `0`, nicht „unendlich\"."},"status":{"type":"string","enum":["aktiv","gesperrt","geschlossen"],"description":"Zustand: `aktiv` bucht weiter (Ueberschreitung nur als Warnung), `gesperrt` lehnt ueberschreitende Bestellungen ab, `geschlossen` wird nicht mehr geprueft."},"notes":{"type":["string","null"],"description":"Freitext. Freigaben haengen ihre Begruendung hier an."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."}},"required":["id","kostenstelle","periodeJahr","periodeQuartal","periodeMonat","budgetBetrag","istBetrag","deltaBetrag","auslastungPct","status","notes","createdAt","updatedAt"],"additionalProperties":false},"description":"Die Budgets, neueste Periode zuerst, innerhalb der Periode nach Kostenstelle."},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, dessen Budgets gelesen wurden."},"count":{"type":"integer","minimum":0,"description":"Anzahl der zurueckgegebenen Budgets."}},"required":["tenantId","count"],"additionalProperties":false,"description":"Angaben zur Abfrage. Eine Blaetterung gibt es hier nicht."}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","kostenstelle":"string","periodeJahr":0,"periodeQuartal":1,"periodeMonat":1,"budgetBetrag":0,"istBetrag":0,"deltaBetrag":0,"auslastungPct":0,"status":"aktiv","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"meta":{"tenantId":"string","count":0}}}}},"400":{"description":"Ungueltige Abfrageparameter"},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"503":{"description":"Datenbank nicht erreichbar oder das Anlegen der Tabelle schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1EinkaufBudgets","tags":["einkauf","budgets"],"parameters":[{"in":"query","name":"kostenstelle","schema":{"type":"string"}},{"in":"query","name":"periodeJahr","schema":{"type":"integer"}},{"in":"query","name":"status","schema":{"type":"string","enum":["aktiv","gesperrt","geschlossen"]}}],"summary":"Einkauf-Budgets auflisten","description":"Listet Einkauf-Budgets mit %-Auslastung"},"post":{"responses":{"201":{"description":"Das angelegte Budget, unverpackt — kein `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Budgets (UUID als Text, vom Server vergeben)"},"kostenstelle":{"type":"string","description":"Kostenstelle, fuer die das Budget gilt. Leer, wenn nicht gepflegt."},"periodeJahr":{"type":["integer","null"],"description":"Jahr der Periode. `null` nur bei unvollstaendigen Altdaten."},"periodeQuartal":{"type":["integer","null"],"minimum":1,"maximum":4,"description":"Quartal 1 bis 4. `null` heisst: das Budget gilt fuer das ganze Jahr."},"periodeMonat":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat 1 bis 12. `null` heisst: das Budget gilt fuer Quartal oder Jahr."},"budgetBetrag":{"type":"number","minimum":0,"description":"Bewilligtes Budget in Euro."},"istBetrag":{"type":"number","description":"Bereits verbrauchter Betrag in Euro."},"deltaBetrag":{"type":"number","description":"Budget minus Verbrauch, auf zwei Nachkommastellen. Negativ heisst: ueberzogen."},"auslastungPct":{"type":"number","minimum":0,"description":"Verbrauch in PROZENT des Budgets, zwei Nachkommastellen. Werte ueber 100 sind moeglich. Bei Budget `0` steht hier `0`, nicht „unendlich\"."},"status":{"type":"string","enum":["aktiv","gesperrt","geschlossen"],"description":"Zustand: `aktiv` bucht weiter (Ueberschreitung nur als Warnung), `gesperrt` lehnt ueberschreitende Bestellungen ab, `geschlossen` wird nicht mehr geprueft."},"notes":{"type":["string","null"],"description":"Freitext. Freigaben haengen ihre Begruendung hier an."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."}},"required":["id","kostenstelle","periodeJahr","periodeQuartal","periodeMonat","budgetBetrag","istBetrag","deltaBetrag","auslastungPct","status","notes","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","kostenstelle":"string","periodeJahr":0,"periodeQuartal":1,"periodeMonat":1,"budgetBetrag":0,"istBetrag":0,"deltaBetrag":0,"auslastungPct":0,"status":"aktiv","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Eingabe ungueltig"},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"409":{"description":"Fuer diese Kostenstelle und Periode gibt es schon ein Budget.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"budget_already_exists","description":"Fester Fehlerschluessel. Je Kostenstelle und Periode ist nur ein Budget erlaubt."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das INSERT schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufBudgets","tags":["einkauf","budgets"],"parameters":[],"summary":"Einkauf-Budget anlegen","description":"Legt ein neues Einkauf-Budget an","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kostenstelle":{"type":"string","minLength":1},"periodeJahr":{"type":"integer","minimum":2000,"maximum":2100},"periodeQuartal":{"type":["integer","null"],"minimum":1,"maximum":4},"periodeMonat":{"type":["integer","null"],"minimum":1,"maximum":12},"budgetBetrag":{"type":"number","minimum":0},"status":{"type":"string","enum":["aktiv","gesperrt","geschlossen"],"default":"aktiv"},"notes":{"type":["string","null"]}},"required":["kostenstelle","periodeJahr","budgetBetrag"]},"example":{"kostenstelle":"string","periodeJahr":2000,"periodeQuartal":1,"periodeMonat":1,"budgetBetrag":0,"status":"aktiv","notes":"string"}}}}}},"/api/v1/einkauf/budgets/{id}":{"patch":{"responses":{"200":{"description":"Das geaenderte Budget im Zustand nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Budgets (UUID als Text, vom Server vergeben)"},"kostenstelle":{"type":"string","description":"Kostenstelle, fuer die das Budget gilt. Leer, wenn nicht gepflegt."},"periodeJahr":{"type":["integer","null"],"description":"Jahr der Periode. `null` nur bei unvollstaendigen Altdaten."},"periodeQuartal":{"type":["integer","null"],"minimum":1,"maximum":4,"description":"Quartal 1 bis 4. `null` heisst: das Budget gilt fuer das ganze Jahr."},"periodeMonat":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat 1 bis 12. `null` heisst: das Budget gilt fuer Quartal oder Jahr."},"budgetBetrag":{"type":"number","minimum":0,"description":"Bewilligtes Budget in Euro."},"istBetrag":{"type":"number","description":"Bereits verbrauchter Betrag in Euro."},"deltaBetrag":{"type":"number","description":"Budget minus Verbrauch, auf zwei Nachkommastellen. Negativ heisst: ueberzogen."},"auslastungPct":{"type":"number","minimum":0,"description":"Verbrauch in PROZENT des Budgets, zwei Nachkommastellen. Werte ueber 100 sind moeglich. Bei Budget `0` steht hier `0`, nicht „unendlich\"."},"status":{"type":"string","enum":["aktiv","gesperrt","geschlossen"],"description":"Zustand: `aktiv` bucht weiter (Ueberschreitung nur als Warnung), `gesperrt` lehnt ueberschreitende Bestellungen ab, `geschlossen` wird nicht mehr geprueft."},"notes":{"type":["string","null"],"description":"Freitext. Freigaben haengen ihre Begruendung hier an."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."}},"required":["id","kostenstelle","periodeJahr","periodeQuartal","periodeMonat","budgetBetrag","istBetrag","deltaBetrag","auslastungPct","status","notes","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","kostenstelle":"string","periodeJahr":0,"periodeQuartal":1,"periodeMonat":1,"budgetBetrag":0,"istBetrag":0,"deltaBetrag":0,"auslastungPct":0,"status":"aktiv","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"ZWEI FORMEN, weil zwei Stellen ablehnen: der Handler, wenn der Rumpf kein aenderbares Feld enthaelt, und der Eingabe-Validator bei ungueltigen Werten.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"no_fields_to_update","description":"Fester Fehlerschluessel. Aenderbar sind nur `budgetBetrag`, `status` und `notes`."}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false`. Daran ist die Antwort des Validators erkennbar."},"error":{"type":"object","additionalProperties":{},"description":"Der Zod-Fehler als Objekt; die Einzelbefunde stehen in seiner Liste `issues`."}},"required":["success","error"],"additionalProperties":false}]}}}},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"404":{"description":"Kein Budget mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"budget_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das UPDATE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1EinkaufBudgetsById","tags":["einkauf","budgets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einkauf-Budget aendern","description":"Aktualisiert ein Budget","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"budgetBetrag":{"type":"number","minimum":0},"status":{"type":"string","enum":["aktiv","gesperrt","geschlossen"]},"notes":{"type":["string","null"]}}},"example":{"budgetBetrag":0,"status":"aktiv","notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung mit der geloeschten Kennung. Endgueltig, kein Soft-Delete.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`. Ein Fehlschlag kommt als 404 oder 503."},"deletedId":{"type":"string","minLength":1,"description":"Kennung des geloeschten Budgets."}},"required":["ok","deletedId"],"additionalProperties":false},"example":{"ok":true,"deletedId":"string"}}}},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"404":{"description":"Kein Budget mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"budget_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das DELETE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1EinkaufBudgetsById","tags":["einkauf","budgets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einkauf-Budget loeschen","description":"Löscht ein Budget"}},"/api/v1/einkauf/budgets/{id}/approve-overrun":{"post":{"responses":{"200":{"description":"Die Freigabe ist erteilt: `status` steht auf `aktiv`, und die Begruendung haengt zusaetzlich an `notes`. Der Betrag aendert sich NICHT — das Budget bleibt ueberzogen, es sperrt nur nicht mehr.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`. Ein Fehlschlag kommt als 404 oder 503."},"budget":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Budgets (UUID als Text, vom Server vergeben)"},"kostenstelle":{"type":"string","description":"Kostenstelle, fuer die das Budget gilt. Leer, wenn nicht gepflegt."},"periodeJahr":{"type":["integer","null"],"description":"Jahr der Periode. `null` nur bei unvollstaendigen Altdaten."},"periodeQuartal":{"type":["integer","null"],"minimum":1,"maximum":4,"description":"Quartal 1 bis 4. `null` heisst: das Budget gilt fuer das ganze Jahr."},"periodeMonat":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat 1 bis 12. `null` heisst: das Budget gilt fuer Quartal oder Jahr."},"budgetBetrag":{"type":"number","minimum":0,"description":"Bewilligtes Budget in Euro."},"istBetrag":{"type":"number","description":"Bereits verbrauchter Betrag in Euro."},"deltaBetrag":{"type":"number","description":"Budget minus Verbrauch, auf zwei Nachkommastellen. Negativ heisst: ueberzogen."},"auslastungPct":{"type":"number","minimum":0,"description":"Verbrauch in PROZENT des Budgets, zwei Nachkommastellen. Werte ueber 100 sind moeglich. Bei Budget `0` steht hier `0`, nicht „unendlich\"."},"status":{"type":"string","enum":["aktiv","gesperrt","geschlossen"],"description":"Zustand: `aktiv` bucht weiter (Ueberschreitung nur als Warnung), `gesperrt` lehnt ueberschreitende Bestellungen ab, `geschlossen` wird nicht mehr geprueft."},"notes":{"type":["string","null"],"description":"Freitext. Freigaben haengen ihre Begruendung hier an."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung als ISO-8601-Zeitstempel in UTC."}},"required":["id","kostenstelle","periodeJahr","periodeQuartal","periodeMonat","budgetBetrag","istBetrag","deltaBetrag","auslastungPct","status","notes","createdAt","updatedAt"],"additionalProperties":false,"description":"Das Budget nach der Freigabe — `status` steht jetzt auf `aktiv`."},"reason":{"type":"string","minLength":1,"description":"Die mitgegebene Begruendung. Sie wird zusaetzlich an `notes` angehaengt."}},"required":["ok","budget","reason"],"additionalProperties":false},"example":{"ok":true,"budget":{"id":"string","kostenstelle":"string","periodeJahr":0,"periodeQuartal":1,"periodeMonat":1,"budgetBetrag":0,"istBetrag":0,"deltaBetrag":0,"auslastungPct":0,"status":"aktiv","notes":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"},"reason":"string"}}}},"400":{"description":"Begruendung fehlt oder ist leer."},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"404":{"description":"Kein Budget mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"budget_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das UPDATE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufBudgetsByIdApprove-overrun","tags":["einkauf","budgets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Budget-Ueberschreitung freigeben","description":"Eskalierte Genehmigung — schaltet Budget auf aktiv (auch wenn überzogen)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","minLength":1}},"required":["reason"]},"example":{"reason":"string"}}}}}},"/api/v1/einkauf/spend-analysis/pareto":{"get":{"responses":{"200":{"description":"Das Ergebnis liegt FLACH im Koerper (`top20pct`, `all`, …), nicht unter `data`. Ohne Bestellungen im Zeitraum kommen leere Listen und Nullen — dasselbe Bild entsteht auch, wenn die Abfrage intern fehlschlaegt.","content":{"application/json":{"schema":{"type":"object","properties":{"top20pct":{"type":"array","items":{"type":"object","properties":{"lieferantId":{"type":"string","minLength":1,"description":"Kennung des Lieferanten."},"lieferantName":{"type":["string","null"],"description":"Name des Lieferanten. `null`, wenn der Stammsatz fehlt (die Verknuepfung ist optional)."},"spend":{"type":"number","description":"Einkaufsvolumen dieses Lieferanten im Zeitraum, in Euro."},"share":{"type":"number","minimum":0,"maximum":1,"description":"Anteil am Gesamtvolumen als ANTEIL von 0 bis 1, nicht als Prozentzahl."},"cumulativeShare":{"type":"number","minimum":0,"maximum":1,"description":"Aufsummierter Anteil bis einschliesslich dieses Lieferanten, 0 bis 1."},"rank":{"type":"integer","minimum":1,"description":"Rang nach Volumen, 1 ist der groesste Lieferant."}},"required":["lieferantId","lieferantName","spend","share","cumulativeShare","rank"],"additionalProperties":false},"description":"Das oberste Fuenftel der Lieferanten nach Volumen; mindestens einer, sobald es Daten gibt."},"pctOfTotalSpend":{"type":"number","minimum":0,"maximum":1,"description":"Welchen Anteil des Gesamtvolumens dieses Fuenftel deckt, 0 bis 1."},"total":{"type":"number","minimum":0,"description":"Gesamtes Einkaufsvolumen im Zeitraum, in Euro."},"totalSuppliers":{"type":"integer","minimum":0,"description":"Anzahl der Lieferanten mit Volumen im Zeitraum."},"all":{"type":"array","items":{"type":"object","properties":{"lieferantId":{"type":"string","minLength":1,"description":"Kennung des Lieferanten."},"lieferantName":{"type":["string","null"],"description":"Name des Lieferanten. `null`, wenn der Stammsatz fehlt (die Verknuepfung ist optional)."},"spend":{"type":"number","description":"Einkaufsvolumen dieses Lieferanten im Zeitraum, in Euro."},"share":{"type":"number","minimum":0,"maximum":1,"description":"Anteil am Gesamtvolumen als ANTEIL von 0 bis 1, nicht als Prozentzahl."},"cumulativeShare":{"type":"number","minimum":0,"maximum":1,"description":"Aufsummierter Anteil bis einschliesslich dieses Lieferanten, 0 bis 1."},"rank":{"type":"integer","minimum":1,"description":"Rang nach Volumen, 1 ist der groesste Lieferant."}},"required":["lieferantId","lieferantName","spend","share","cumulativeShare","rank"],"additionalProperties":false},"description":"Alle Lieferanten, absteigend nach Volumen."},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, dessen Bestellungen ausgewertet wurden."},"source":{"type":"string","const":"db","description":"Immer `db`. Die Zahlen stammen nie aus einem Zwischenspeicher."},"from":{"type":["string","null"],"description":"Untere Zeitgrenze `YYYY-MM-DD`, wie mitgegeben. `null` heisst: ohne Anfang."},"to":{"type":["string","null"],"description":"Obere Zeitgrenze `YYYY-MM-DD`, wie mitgegeben. `null` heisst: ohne Ende."}},"required":["tenantId","source","from","to"],"additionalProperties":false,"description":"Angaben zu Mandant, Herkunft und Zeitraum."}},"required":["top20pct","pctOfTotalSpend","total","totalSuppliers","all","meta"],"additionalProperties":false},"example":{"top20pct":[{"lieferantId":"string","lieferantName":"string","spend":0,"share":0,"cumulativeShare":0,"rank":1}],"pctOfTotalSpend":0,"total":0,"totalSuppliers":0,"all":[{"lieferantId":"string","lieferantName":"string","spend":0,"share":0,"cumulativeShare":0,"rank":1}],"meta":{"tenantId":"string","source":"db","from":"string","to":"string"}}}}},"400":{"description":"Ungueltige Abfrageparameter"},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"503":{"description":"Datenbank nicht erreichbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1EinkaufSpend-analysisPareto","tags":["einkauf","spend"],"parameters":[{"in":"query","name":"from","schema":{"type":"string","format":"date"}},{"in":"query","name":"to","schema":{"type":"string","format":"date"}}],"summary":"Pareto-Auswertung der Lieferanten","description":"Pareto 80/20 — Top 20% Lieferanten + ihre Anteilssumme"}},"/api/v1/einkauf/spend-analysis/abc":{"get":{"responses":{"200":{"description":"Alle Lieferanten mit Klasse, dazu die Klassenzaehler und die verwendeten Schwellen. ACHTUNG bei den Einheiten: `thresholds` steht in Prozent (80/95), `share` und `cumulativeShare` dagegen als Anteil von 0 bis 1.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"lieferantId":{"type":"string","minLength":1,"description":"Kennung des Lieferanten."},"lieferantName":{"type":["string","null"],"description":"Name des Lieferanten. `null`, wenn der Stammsatz fehlt (die Verknuepfung ist optional)."},"spend":{"type":"number","description":"Einkaufsvolumen dieses Lieferanten im Zeitraum, in Euro."},"share":{"type":"number","minimum":0,"maximum":1,"description":"Anteil am Gesamtvolumen als ANTEIL von 0 bis 1, nicht als Prozentzahl."},"cumulativeShare":{"type":"number","minimum":0,"maximum":1,"description":"Aufsummierter Anteil bis einschliesslich dieses Lieferanten, 0 bis 1."},"rank":{"type":"integer","minimum":1,"description":"Rang nach Volumen, 1 ist der groesste Lieferant."},"kategorie":{"type":"string","enum":["A","B","C"],"description":"Klasse nach kumuliertem Anteil: `A` bis zur Schwelle `thresholdA`, `B` bis `thresholdB`, darueber `C`."}},"required":["lieferantId","lieferantName","spend","share","cumulativeShare","rank","kategorie"],"additionalProperties":false},"description":"Alle Lieferanten mit Klasse, absteigend nach Volumen."},"counts":{"type":"object","properties":{"A":{"type":"integer","minimum":0,"description":"Anzahl der A-Lieferanten."},"B":{"type":"integer","minimum":0,"description":"Anzahl der B-Lieferanten."},"C":{"type":"integer","minimum":0,"description":"Anzahl der C-Lieferanten."}},"required":["A","B","C"],"additionalProperties":false,"description":"Wie viele Lieferanten in welcher Klasse stehen."},"thresholds":{"type":"object","properties":{"a":{"type":"number","minimum":0,"maximum":100,"description":"Verwendete A-Schwelle in PROZENT, Voreinstellung 80."},"b":{"type":"number","minimum":0,"maximum":100,"description":"Verwendete B-Schwelle in PROZENT, Voreinstellung 95."}},"required":["a","b"],"additionalProperties":false,"description":"Die Schwellen dieser Auswertung — hier in Prozent, waehrend `share` ein Anteil ist."},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, dessen Bestellungen ausgewertet wurden."},"source":{"type":"string","const":"db","description":"Immer `db`. Die Zahlen stammen nie aus einem Zwischenspeicher."},"from":{"type":["string","null"],"description":"Untere Zeitgrenze `YYYY-MM-DD`, wie mitgegeben. `null` heisst: ohne Anfang."},"to":{"type":["string","null"],"description":"Obere Zeitgrenze `YYYY-MM-DD`, wie mitgegeben. `null` heisst: ohne Ende."}},"required":["tenantId","source","from","to"],"additionalProperties":false,"description":"Angaben zu Mandant, Herkunft und Zeitraum."}},"required":["data","counts","thresholds","meta"],"additionalProperties":false},"example":{"data":[{"lieferantId":"string","lieferantName":"string","spend":0,"share":0,"cumulativeShare":0,"rank":1,"kategorie":"A"}],"counts":{"A":0,"B":0,"C":0},"thresholds":{"a":0,"b":0},"meta":{"tenantId":"string","source":"db","from":"string","to":"string"}}}}},"400":{"description":"Ungueltige Abfrageparameter"},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"503":{"description":"Datenbank nicht erreichbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1EinkaufSpend-analysisAbc","tags":["einkauf","spend"],"parameters":[{"in":"query","name":"from","schema":{"type":"string","format":"date"}},{"in":"query","name":"to","schema":{"type":"string","format":"date"}},{"in":"query","name":"thresholdA","schema":{"type":"number","minimum":0,"maximum":100,"default":80}},{"in":"query","name":"thresholdB","schema":{"type":"number","minimum":0,"maximum":100,"default":95}}],"summary":"ABC-Klassifikation der Lieferanten","description":"A/B/C-Klassifikation der Lieferanten nach kumulativem Spend-Anteil"}},"/api/v1/einkauf/spend-analysis/by-group":{"get":{"responses":{"200":{"description":"Die Gruppen mit ihren Summen. Bei `warengruppe` und `kostenstelle` werden die Bestellpositionen einzeln ausgewertet; eine Bestellung ohne Positionen zaehlt dann als eine Zeile mit ihrer Gesamtsumme. Bei `monat` zaehlt immer die Bestellung als Ganzes.","content":{"application/json":{"schema":{"type":"object","properties":{"dimension":{"type":"string","enum":["warengruppe","kostenstelle","monat"],"description":"Die ausgewertete Dimension, unveraendert aus der Abfrage uebernommen."},"data":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Der Gruppenwert: die Warengruppe, die Kostenstelle oder der Monat `YYYY-MM`. Nicht zugeordnete Zeilen sammeln sich unter einem eigenen Sammelschluessel."},"spend":{"type":"number","description":"Einkaufsvolumen dieser Gruppe im Zeitraum, in Euro."},"share":{"type":"number","minimum":0,"maximum":1,"description":"Anteil am Gesamtvolumen als ANTEIL von 0 bis 1."},"orderCount":{"type":"integer","minimum":0,"description":"Anzahl der Zeilen, die in diese Gruppe fielen."}},"required":["key","spend","share","orderCount"],"additionalProperties":false},"description":"Die Gruppen mit ihren Summen."},"total":{"type":"number","description":"Summe ueber alle Gruppen, auf zwei Nachkommastellen gerundet."},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, dessen Bestellungen ausgewertet wurden."},"source":{"type":"string","const":"db","description":"Immer `db`. Die Zahlen stammen nie aus einem Zwischenspeicher."},"from":{"type":["string","null"],"description":"Untere Zeitgrenze `YYYY-MM-DD`, wie mitgegeben. `null` heisst: ohne Anfang."},"to":{"type":["string","null"],"description":"Obere Zeitgrenze `YYYY-MM-DD`, wie mitgegeben. `null` heisst: ohne Ende."}},"required":["tenantId","source","from","to"],"additionalProperties":false,"description":"Angaben zu Mandant, Herkunft und Zeitraum."}},"required":["dimension","data","total","meta"],"additionalProperties":false},"example":{"dimension":"warengruppe","data":[{"key":"string","spend":0,"share":0,"orderCount":0}],"total":0,"meta":{"tenantId":"string","source":"db","from":"string","to":"string"}}}}},"400":{"description":"Ungueltige Abfrageparameter, z. B. eine unbekannte `dimension`."},"401":{"description":"Kein Mandantenkontext (`unauthorized`), als Text."},"503":{"description":"Datenbank nicht erreichbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1EinkaufSpend-analysisBy-group","tags":["einkauf","spend"],"parameters":[{"in":"query","name":"from","schema":{"type":"string","format":"date"},"required":false},{"in":"query","name":"to","schema":{"type":"string","format":"date"},"required":false},{"in":"query","name":"dimension","schema":{"type":"string","enum":["warengruppe","kostenstelle","monat"]},"required":true}],"summary":"Einkaufsvolumen nach Gruppe","description":"Spend aggregiert nach Warengruppe / Kostenstelle / Monat"}},"/api/v1/einkauf/einvoice/parse":{"post":{"responses":{"200":{"description":"Die eingelesene Rechnung. Es wird NICHTS gespeichert. `draft` ist nur dabei, wenn `asDraft=true` mitgegeben wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`. Ein Fehlschlag traegt `ok: false` samt `errorCode`."},"preview":{"type":"object","properties":{"format":{"type":"string","enum":["xrechnung","zugferd","unknown"],"description":"Erkanntes Belegformat: `xrechnung`, `zugferd` oder `unknown`."},"profil":{"type":"string","description":"Erkanntes Profil, z. B. `XRECHNUNG` oder `EN16931`. Fehlt, wenn es nicht ableitbar war."},"syntax":{"type":"string","enum":["ubl","cii","unknown"],"description":"Zugrunde liegende XML-Bauart: `ubl`, `cii` oder `unknown`."},"lieferant":{"type":"object","properties":{"name":{"type":"string","description":"Name des Rechnungsstellers. Leere Zeichenkette, wenn der Beleg keinen nennt."},"ustIdNr":{"type":"string","description":"Umsatzsteuer-Identifikationsnummer. Sie ist das sicherste Merkmal beim Zuordnen zum Stamm."},"anschrift":{"type":"string","description":"Anschrift in einer Zeile."},"email":{"type":"string","description":"Kontaktadresse aus dem Beleg."},"iban":{"type":"string","description":"Bankverbindung aus dem Beleg."}},"required":["name"],"description":"Der Rechnungssteller, wie der Beleg ihn nennt — noch NICHT dem Stamm zugeordnet."},"rechnungsnummer":{"type":"string","description":"Belegnummer des Lieferanten. Fehlt, wenn der Beleg keine nennt."},"rechnungsdatum":{"type":"string","description":"Rechnungsdatum als reiner Kalendertag `YYYY-MM-DD`, KEIN Zeitstempel."},"faelligkeitsdatum":{"type":"string","description":"Faelligkeit als reiner Kalendertag `YYYY-MM-DD`, KEIN Zeitstempel."},"positionen":{"type":"array","items":{"type":"object","properties":{"bezeichnung":{"type":"string","description":"Text der Rechnungsposition."},"menge":{"type":"number","description":"Menge laut Beleg."},"einzelpreis":{"type":"number","description":"Preis je Einheit, netto."},"steuersatz":{"type":"number","description":"Steuersatz der Position in Prozent, z. B. `19`."},"positionsNetto":{"type":"number","description":"Netto-Zeilensumme (BT-131), sofern der Beleg sie nennt. Fehlt sonst ganz."}},"required":["bezeichnung","menge","einzelpreis","steuersatz"],"description":"Eine Position der eingelesenen Rechnung."},"description":"Die Positionen des Belegs, in Belegreihenfolge."},"betragNetto":{"type":"number","description":"Nettosumme des Belegs."},"steuerbetrag":{"type":"number","description":"Ausgewiesener Steuerbetrag."},"betragBrutto":{"type":"number","description":"Bruttosumme des Belegs."},"mwstSatz":{"type":"number","description":"Vorherrschender Steuersatz in Prozent. Fehlt, wenn er sich nicht eindeutig ergab."},"waehrung":{"type":"string","description":"Waehrung des Belegs, z. B. `EUR`."},"warnungen":{"type":"array","items":{"type":"string"},"description":"Nicht toedliche Auffaelligkeiten beim Einlesen — fehlende Felder, benutzte Ersatzwerte. Eine gefuellte Liste heisst NICHT, dass das Einlesen fehlgeschlagen ist."}},"required":["format","syntax","lieferant","positionen","betragNetto","steuerbetrag","betragBrutto","waehrung","warnungen"],"description":"Die eingelesene Rechnung. Nichts davon wurde gespeichert."},"draft":{"type":"object","properties":{"lieferantName":{"type":"string","minLength":1,"description":"Name des Lieferanten, oder `Unbekannter Lieferant`, wenn der Beleg keinen nennt."},"rechnungsnummer":{"type":"string","description":"Belegnummer. Fehlt, wenn der Beleg keine nennt."},"betreff":{"type":"string","description":"Bezeichnung der ERSTEN Position als Betreffvorschlag. Leer bei Belegen ohne Positionen."},"betragNetto":{"type":"number","description":"Nettobetrag, aus der Vorschau uebernommen."},"mwstSatz":{"type":"number","description":"Steuersatz in Prozent; `19`, wenn der Beleg keinen eindeutigen nennt."},"faelligAm":{"type":"string","description":"Faelligkeit `YYYY-MM-DD`. Fehlt, wenn der Beleg kein taugliches Datum lieferte."},"notizen":{"type":"string","description":"Zusammengesetzter Hinweistext aus USt-IdNr, IBAN und Herkunft, getrennt durch „·\"."}},"required":["lieferantName","betreff","betragNetto","mwstSatz","notizen"],"additionalProperties":false,"description":"Nur vorhanden, wenn `asDraft=true` mitgegeben wurde."}},"required":["ok","preview"],"additionalProperties":false},"example":{"ok":true,"preview":{"format":"xrechnung","profil":"string","syntax":"ubl","lieferant":{"name":"string","ustIdNr":"string","anschrift":"string","email":"string","iban":"string"},"rechnungsnummer":"string","rechnungsdatum":"string","faelligkeitsdatum":"string","positionen":[{"bezeichnung":"string","menge":0,"einzelpreis":0,"steuersatz":0,"positionsNetto":0}],"betragNetto":0,"steuerbetrag":0,"betragBrutto":0,"mwstSatz":0,"waehrung":"string","warnungen":["string"]},"draft":{"lieferantName":"string","rechnungsnummer":"string","betreff":"string","betragNetto":0,"mwstSatz":0,"faelligAm":"string","notizen":"string"}}}}},"400":{"description":"ZWEI JSON-FORMEN: der gekuerzte Zod-Fehler, wenn der JSON-Rumpf nicht passt, und der Einlesefehler mit `errorCode: \"empty_input\"`, wenn das XML leer war. Die Multipart-Fehler dieses Endpunkts (fehlendes Feld, unlesbares Formular) kommen dagegen als text/plain.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade, z. B. `xml`. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false`."},"error":{"type":"string","minLength":1,"description":"Klartext, warum der Beleg nicht gelesen werden konnte."},"errorCode":{"type":"string","enum":["empty_input","unrecognized_format","parse_failed","validation_failed"],"description":"Ursache als Schluessel. `empty_input` fuehrt zu 400, die drei anderen zu 422 — der Code sagt also zugleich, welche Kennzahl kommt."}},"required":["ok","error","errorCode"],"additionalProperties":false}]}}}},"401":{"description":"Nicht authentifiziert. Als Text."},"413":{"description":"Datei oder XML groesser als 5 MB. Als Text."},"422":{"description":"Das XML ist keine lesbare E-Rechnung. `errorCode` sagt, woran es lag: `unrecognized_format`, `parse_failed` oder `validation_failed`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false`."},"error":{"type":"string","minLength":1,"description":"Klartext, warum der Beleg nicht gelesen werden konnte."},"errorCode":{"type":"string","enum":["empty_input","unrecognized_format","parse_failed","validation_failed"],"description":"Ursache als Schluessel. `empty_input` fuehrt zu 400, die drei anderen zu 422 — der Code sagt also zugleich, welche Kennzahl kommt."}},"required":["ok","error","errorCode"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufEinvoiceParse","tags":["einkauf","e-rechnung"],"parameters":[],"description":"Parst eine eingehende XRechnung (UBL oder CII) / ZUGFeRD-XML in eine strukturierte Eingangsrechnungs-Vorschau. Keine Persistierung.","summary":"E-Rechnung einlesen (Vorschau)"}},"/api/v1/einkauf/einvoice/commit":{"post":{"responses":{"200":{"description":"Es gab die Rechnung schon (gleiche Belegnummer und Lieferant): der Koerper traegt `idempotent: true` und die bestehende Kennung. Es wurde NICHTS geschrieben. Gleiche Form wie bei 201 — nur die Kennzahl und `idempotent` unterscheiden sich.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`."},"id":{"type":"string","minLength":1,"description":"Kennung der angelegten oder bereits vorhandenen Eingangsrechnung."},"idempotent":{"type":"boolean","description":"`true` heisst: es gab die Rechnung schon, es wurde NICHTS angelegt (dann Kennzahl 200). `false` heisst: neu angelegt (Kennzahl 201)."},"status":{"type":"string","const":"draft","description":"Immer `draft`. Die Uebernahme bucht nichts — ein Mensch bestaetigt nach."},"lieferantId":{"type":["string","null"],"description":"Zugeordneter Lieferant aus dem Stamm. `null`, wenn keiner passte."},"lieferantMatch":{"type":"string","enum":["ust_id","name","none"],"description":"Wie zugeordnet wurde: ueber die USt-IdNr (sicher), ueber den Namen (unsicher) oder gar nicht."},"lieferantMatched":{"type":"boolean","description":"Kurzform von `lieferantMatch !== \"none\"` — bequem fuer eine Abfrage in der Maske."},"rechnungsnummer":{"type":["string","null"],"description":"Die gespeicherte Belegnummer. `null`, wenn der Beleg keine nannte."},"warnungen":{"type":"array","items":{"type":"string"},"description":"Die Auffaelligkeiten aus dem Einlesen. Sie stehen der Uebernahme nicht entgegen."}},"required":["ok","id","idempotent","status","lieferantId","lieferantMatch","lieferantMatched","rechnungsnummer","warnungen"],"additionalProperties":false},"example":{"ok":true,"id":"string","idempotent":true,"status":"draft","lieferantId":"string","lieferantMatch":"ust_id","lieferantMatched":true,"rechnungsnummer":"string","warnungen":["string"]}}}},"201":{"description":"Neu angelegt. Der Koerper traegt `idempotent: false`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`."},"id":{"type":"string","minLength":1,"description":"Kennung der angelegten oder bereits vorhandenen Eingangsrechnung."},"idempotent":{"type":"boolean","description":"`true` heisst: es gab die Rechnung schon, es wurde NICHTS angelegt (dann Kennzahl 200). `false` heisst: neu angelegt (Kennzahl 201)."},"status":{"type":"string","const":"draft","description":"Immer `draft`. Die Uebernahme bucht nichts — ein Mensch bestaetigt nach."},"lieferantId":{"type":["string","null"],"description":"Zugeordneter Lieferant aus dem Stamm. `null`, wenn keiner passte."},"lieferantMatch":{"type":"string","enum":["ust_id","name","none"],"description":"Wie zugeordnet wurde: ueber die USt-IdNr (sicher), ueber den Namen (unsicher) oder gar nicht."},"lieferantMatched":{"type":"boolean","description":"Kurzform von `lieferantMatch !== \"none\"` — bequem fuer eine Abfrage in der Maske."},"rechnungsnummer":{"type":["string","null"],"description":"Die gespeicherte Belegnummer. `null`, wenn der Beleg keine nannte."},"warnungen":{"type":"array","items":{"type":"string"},"description":"Die Auffaelligkeiten aus dem Einlesen. Sie stehen der Uebernahme nicht entgegen."}},"required":["ok","id","idempotent","status","lieferantId","lieferantMatch","lieferantMatched","rechnungsnummer","warnungen"],"additionalProperties":false},"example":{"ok":true,"id":"string","idempotent":true,"status":"draft","lieferantId":"string","lieferantMatch":"ust_id","lieferantMatched":true,"rechnungsnummer":"string","warnungen":["string"]}}}},"400":{"description":"Das XML war leer (`errorCode: \"empty_input\"`). Fehler beim LESEN des Rumpfes — kein `xml`-Feld, unlesbares Multipart, falscher Inhaltstyp — kommen dagegen als text/plain.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false`."},"error":{"type":"string","minLength":1,"description":"Klartext, warum der Beleg nicht gelesen werden konnte."},"errorCode":{"type":"string","enum":["empty_input","unrecognized_format","parse_failed","validation_failed"],"description":"Ursache als Schluessel. `empty_input` fuehrt zu 400, die drei anderen zu 422 — der Code sagt also zugleich, welche Kennzahl kommt."}},"required":["ok","error","errorCode"],"additionalProperties":false}}}},"401":{"description":"Nicht authentifiziert. Als Text."},"413":{"description":"Datei oder XML groesser als 5 MB. Als Text."},"422":{"description":"Nicht als E-Rechnung lesbar. Auch ein PDF OHNE eingebettetes XML landet hier — dann allerdings als text/plain, weil es vor dem Einleser abgefangen wird.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false`."},"error":{"type":"string","minLength":1,"description":"Klartext, warum der Beleg nicht gelesen werden konnte."},"errorCode":{"type":"string","enum":["empty_input","unrecognized_format","parse_failed","validation_failed"],"description":"Ursache als Schluessel. `empty_input` fuehrt zu 400, die drei anderen zu 422 — der Code sagt also zugleich, welche Kennzahl kommt."}},"required":["ok","error","errorCode"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das Schreiben schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false`."},"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["ok","error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufEinvoiceCommit","tags":["einkauf","e-rechnung"],"parameters":[],"description":"Parst eine eingehende XRechnung (UBL/CII) bzw. ZUGFeRD — als XML oder als PDF/A-3 mit eingebettetem XML — und PERSISTIERT sie tenant-gescopt als Eingangsrechnung (Status draft). Idempotent ueber Belegnummer + Lieferant.","summary":"E-Rechnung uebernehmen"}},"/api/v1/documents/search":{"get":{"responses":{"200":{"description":"Treffer-Liste — `backend: \"unavailable\"` heiszt „nicht gesucht\"","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"backend":{"type":"string","enum":["fts","ilike","unavailable"]},"total":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"documentId":{"type":"string"},"name":{"type":"string"},"documentType":{"type":["string","null"]},"folderId":{"type":["string","null"]},"snippet":{"type":"string"},"score":{"type":"number"},"createdAt":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"requiredRole":{"type":["string","null"]}},"required":["documentId","name","documentType","folderId","snippet","score","createdAt","tags","requiredRole"]}}},"required":["query","backend","total","results"]},"example":{"query":"string","backend":"fts","total":0,"results":[{"documentId":"string","name":"string","documentType":"string","folderId":"string","snippet":"string","score":0,"createdAt":"string","tags":["string"],"requiredRole":"string"}]}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1DocumentsSearch","tags":["documents"],"parameters":[],"description":"Permission-aware Volltextsuche ueber Dokumente. Beruecksichtigt required_role, archived_at und optionalen folderId-Filter. `q` ist Pflicht (1…500 Zeichen), `limit` begrenzt die Treffer (1…100, Vorgabe 20). Gesucht wird im Namen und im erkannten Text des Belegs. Belege, deren geforderte Rolle ueber der des Aufrufers liegt, und archivierte erscheinen nicht — dieselbe Suche kann fuer zwei Nutzer also verschieden ausfallen. `total` zaehlt die GELIEFERTEN Treffer, nicht alle vorhandenen. `backend` sagt, wie gesucht wurde: „unavailable\" heiszt, dass gar nicht gesucht wurde — eine leere Liste bedeutet dann nicht „nichts gefunden\". Rein lesend, es wird nichts gespeichert.","summary":"Permission-aware Volltextsuche ueber Dokumente","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/documents/compare":{"post":{"responses":{"200":{"description":"Der Vergleich. Auch dann, wenn das Sprachmodell scheiterte (Fallback-Diff).","content":{"application/json":{"schema":{"type":"object","properties":{"document_a_id":{"type":"string","format":"uuid"},"document_b_id":{"type":"string","format":"uuid"},"summary":{"type":"string","description":"Zusammenfassung. Beginnt mit „Fallback-Diff: …\", wenn das Sprachmodell ausfiel."},"diffs":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["added","changed","removed"]},"section":{"type":"string"},"text_a":{"type":"string"},"text_b":{"type":"string"},"risk_score":{"type":"number","description":"0 bis 1, vom Sprachmodell geschaetzt."},"impact":{"type":"string","enum":["critical","major","minor","cosmetic"]},"description":{"type":"string"}},"required":["type","risk_score","impact","description"]}},"total_critical":{"type":"integer"},"total_major":{"type":"integer"},"total_minor":{"type":"integer"},"performed_at":{"type":"string"}},"required":["document_a_id","document_b_id","summary","diffs","total_critical","total_major","total_minor","performed_at"]},"example":{"document_a_id":"00000000-0000-4000-8000-000000000000","document_b_id":"00000000-0000-4000-8000-000000000000","summary":"string","diffs":[{"type":"added","section":"string","text_a":"string","text_b":"string","risk_score":0,"impact":"critical","description":"string"}],"total_critical":0,"total_major":0,"total_minor":0,"performed_at":"string"}}}},"400":{"description":"Der Rumpf haelt das Schema nicht ein — beide Kennungen muessen UUIDs sein."},"401":{"description":"`unauthorized` — kein Mandanten-Kontext."},"403":{"description":"`INSUFFICIENT_MODULE_PERMISSION` aus der Modul-Sperre `documents`."},"404":{"description":"`one_or_both_documents_not_found` — unbekannt, fremder Mandant, oder zweimal dieselbe Kennung."},"500":{"description":"Fehler beim LESEN der Belege. Fehler des Sprachmodells landen NICHT hier, sondern im Fallback-Diff der 200-Antwort."},"503":{"description":"`database_unavailable`; die Antwort traegt `Retry-After: 5`."}},"operationId":"postApiV1DocumentsCompare","tags":["documents"],"parameters":[],"summary":"Zwei Belege vergleichen","description":"Vergleicht den erkannten Text zweier Belege und liefert die Unterschiede\nals Liste, je mit Art (hinzugefuegt, geaendert, entfernt), geschaetztem\nRisiko und Tragweite.\n\nNur lesend. Beide Belege bleiben unveraendert, das Ergebnis wird NICHT\ngespeichert — jeder Aufruf rechnet neu und kann anders ausfallen. Der\nVerbrauch wird fuer die Kostenabrechnung mitgeschrieben.\n\nJe Beleg gehen die ersten 8.000 Zeichen in den Vergleich. Bei laengeren\nBelegen ist das Ergebnis unvollstaendig, und die Antwort sagt nicht, dass\ngekuerzt wurde. Fehlt der erkannte Text, wird „[OCR-Text nicht\nverfuegbar]\" verglichen — beide Belege ohne Text ergeben so „keine\nUnterschiede\".\n\nFAELLT DAS SPRACHMODELL AUS, KOMMT TROTZDEM 200. Die Route weicht dann\nauf einen zeilenweisen Vergleich aus: `summary` beginnt mit\n„Fallback-Diff: \" und traegt den Grund, jede Abweichung bekommt pauschal\n`risk_score: 0.3` und `impact: \"minor\"`, die Liste bricht nach 20\nEintraegen ab und `total_critical`/`total_major`/`total_minor` stehen alle\nauf 0. Diese Nullen bedeuten dann NICHT „nichts Kritisches gefunden\",\nsondern „nicht bewertet\". Der Praefix in `summary` ist das\nUnterscheidungsmerkmal.\n\nZweimal dieselbe Kennung ergibt 404 `one_or_both_documents_not_found`:\ndie Abfrage findet dann nur einen Datensatz, erwartet aber zwei.\nDenselben 404 gibt es, wenn einer der Belege einem anderen Mandanten\ngehoert — welcher der beiden fehlt, sagt die Antwort nicht.\n\nRECHTE: die Route haengt unter `/documents/*` und damit an der\nModul-Sperre `documents` — wer dort nicht schreiben darf, bekommt 403,\nobwohl die Route nichts schreibt. Die KI-Kontingente greifen NICHT: sie\nliegen auf `/ai/*` und `/ai-actions/*`, dieser Pfad gehoert nicht dazu.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"document_a_id":{"type":"string","format":"uuid"},"document_b_id":{"type":"string","format":"uuid"}},"required":["document_a_id","document_b_id"]},"example":{"document_a_id":"00000000-0000-4000-8000-000000000000","document_b_id":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/documents/tags":{"get":{"responses":{"200":{"description":"Alle Schlagworte, nach Name sortiert — ohne Obergrenze","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{},"color":{},"description":{},"created_at":{}},"required":["id"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen (`query_failed`). Bewusst KEINE leere Liste mit 200 — die haette wie „keine Schlagworte\" ausgesehen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1DocumentsTags","tags":["documents","Tags"],"parameters":[],"summary":"Alle Document-Tags des Tenants","description":"Liest document_tags im Schema des angemeldeten Mandanten, nach Name sortiert und ohne Obergrenze. Fehlen die drei Pro-Tabellen (Schlagworte, Zuordnungen, Kommentare) im Schema, legt der Aufruf sie beim ersten Mal selbst an. Scheitert die Abfrage, kommt bewusst 500 statt einer leeren Liste."},"post":{"responses":{"201":{"description":"ACHTUNG: 201 heisst hier nicht zwingend „neu angelegt\". Der Befehl traegt `ON CONFLICT (name) DO UPDATE` — gibt es das Schlagwort schon, werden Farbe und Beschreibung UEBERSCHRIEBEN und dieselbe Kennung kommt zurueck. Wer versehentlich einen bestehenden Namen sendet, aendert ihn.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{},"color":{},"description":{},"created_at":{}},"required":["id"],"additionalProperties":false},"example":{"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1DocumentsTags","tags":["documents","Tags"],"parameters":[],"summary":"Neuen Document-Tag anlegen","description":"Schreibt nach document_tags im Mandantenschema. Der Name ist dort eindeutig, und der Befehl traegt ON CONFLICT (name) DO UPDATE: ein schon vorhandenes Schlagwort wird mit Farbe und Beschreibung UEBERSCHRIEBEN statt abgelehnt. Ohne Farbe gilt #6366f1; der anmeldende Nutzer wird als created_by vermerkt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{3,8}$"},"description":{"type":"string","maxLength":500}},"required":["name"]},"example":{"name":"string","description":"string"}}}}}},"/api/v1/documents/{id}/tags":{"post":{"responses":{"200":{"description":"`ok: true` heisst „der Aufruf lief durch\". Der Befehl traegt `ON CONFLICT DO NOTHING`, es kommt also dieselbe Antwort, wenn das Schlagwort bereits hing. Ob Dokument und Schlagwort ueberhaupt existieren, prueft der Aufruf NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1DocumentsByIdTags","tags":["documents","Tags"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Tag an Dokument anhängen","description":"Traegt das Paar aus Dokument- und Schlagwortkennung in document_tag_assignments ein, mit ON CONFLICT DO NOTHING. Weder das Dokument noch das Schlagwort werden vorher auf Existenz geprueft — die Tabelle hat keine Fremdschluessel, eine unbekannte Kennung wird also einfach mitgeschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tagId":{"type":"string","format":"uuid"}},"required":["tagId"]},"example":{"tagId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/documents/{id}/tags/{tagId}":{"delete":{"responses":{"200":{"description":"`ok: true` auch dann, wenn gar keine Zuordnung bestand — die Trefferzahl des DELETE wird nicht geprueft. Der Aufruf ist wiederholbar, belegt aber nicht, dass vorher etwas dranhing.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"deleteApiV1DocumentsByIdTagsByTagId","tags":["documents","Tags"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"tagId","required":true}],"summary":"Tag vom Dokument entfernen","description":"Loescht die Zeile aus document_tag_assignments, also nur die Verknuepfung — das Schlagwort selbst bleibt bestehen und haengt an anderen Dokumenten weiter. Die Trefferzahl des DELETE wird nicht geprueft, der Aufruf ist damit beliebig oft wiederholbar."}},"/api/v1/documents/bulk-tag":{"post":{"responses":{"200":{"description":"Jedes Dokument bekommt JEDES Schlagwort — die Zahl der Paare ist das Produkt beider Listen (hoechstens 500 × 20 = 10 000). `total` nennt genau dieses Produkt, `inserted` die tatsaechlich neu angelegten Zuordnungen. `inserted < total` heisst „der Rest hing schon dran\", NICHT „fehlgeschlagen\".","content":{"application/json":{"schema":{"type":"object","properties":{"inserted":{"type":"number"},"total":{"type":"number"}},"required":["inserted","total"],"additionalProperties":false},"example":{"inserted":0,"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1DocumentsBulk-tag","tags":["documents","Tags"],"parameters":[],"summary":"Mehrere Dokumente mit mehreren Tags versehen","description":"Schreibt das Kreuzprodukt beider Listen in EINEM einzigen INSERT nach document_tag_assignments, mit ON CONFLICT DO NOTHING. Der Rumpf erlaubt hoechstens 500 Dokumente und 20 Schlagworte, also bis zu 10 000 Paare je Aufruf. Gezaehlt wird ueber RETURNING, weshalb inserted nur die wirklich neu angelegten Zuordnungen nennt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"documentIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"tagIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":20}},"required":["documentIds","tagIds"]},"example":{"documentIds":["00000000-0000-4000-8000-000000000000"],"tagIds":["00000000-0000-4000-8000-000000000000"]}}}}}},"/api/v1/documents/{id}/versions":{"get":{"responses":{"200":{"description":"Alle Versionen des Dokuments, neueste zuerst. Es ist gleich, ob die uebergebene Kennung die Wurzel oder eine spaetere Version ist — der Aufruf loest die Wurzel selbst auf und liefert immer den ganzen Strang. Genau eine Zeile traegt `is_latest_version: true`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"version_number":{},"is_latest_version":{},"title":{},"original_filename":{},"size_bytes":{},"created_at":{}},"required":["id"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen (`query_failed`) — bewusst kein leeres 200, das wie „keine Versionen\" ausgesehen haette","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1DocumentsByIdVersions","tags":["documents","Versions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Versionshistorie eines Dokuments","description":"Loest ueber parent_document_id zuerst die Wurzel des Versionsstrangs auf und liest dann Wurzel und Nachfolger aus documents, absteigend nach version_number. Die uebergebene Kennung darf deshalb jede Version des Strangs sein. Fehlen die Versionsspalten im Mandantenschema, ergaenzt der Aufruf sie selbst, bevor er liest."},"post":{"responses":{"201":{"description":"Neue Version angelegt. Die Antwort traegt NUR die drei Versionsfelder, nicht den ganzen Datensatz. Alle bisherigen Versionen stehen danach auf `is_latest_version: false`; geloescht wird nichts.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"version_number":{},"is_latest_version":{}},"required":["id"],"additionalProperties":false},"example":{"id":"string"}}}},"400":{"description":"Kein Multipart-Rumpf (`multipart_required`) oder kein Feld `file` (`file_required`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"413":{"description":"Datei groesser als 50 MB (`file_too_large`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext. ACHTUNG: ein unbekanntes Dokument landet ebenfalls HIER, mit `error: \"document_not_found\"` und Status 503 statt 404. Die Datei ist dann bereits im Speicher abgelegt, aber keiner Version zugeordnet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1DocumentsByIdVersions","tags":["documents","Versions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Neue Version eines Dokuments hochladen","description":"Multipart: file. Die alte Version bleibt erhalten, is_latest_version wird umgeschaltet."}},"/api/v1/documents/{id}/comments":{"get":{"responses":{"200":{"description":"Kommentare, neueste zuerst — GEKAPPT bei 200. Es gibt kein Blaettern und keinen Hinweis darauf, dass abgeschnitten wurde: genau 200 Eintraege koennen heissen, dass es mehr gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"user_id":{},"author_name":{},"body":{},"created_at":{}},"required":["id"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen (`query_failed`) — bewusst kein leeres 200, das wie „keine Kommentare\" ausgesehen haette","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1DocumentsByIdComments","tags":["documents","Comments"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Kommentare zum Dokument","description":"Liest document_comments zu genau diesem Dokument, neueste zuerst und mit fester Obergrenze von 200 Zeilen. Es gibt weder Blaetterung noch Filter, und die Antwort sagt nicht, ob abgeschnitten wurde."},"post":{"responses":{"201":{"description":"Kommentar angelegt. `author_name` ist die E-Mail des Verfassers zum Zeitpunkt des Schreibens — sie wird eingefroren und aendert sich nicht mehr mit, wenn das Konto spaeter umbenannt wird.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"user_id":{},"author_name":{},"body":{},"created_at":{}},"required":["id"],"additionalProperties":false},"example":{"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"postApiV1DocumentsByIdComments","tags":["documents","Comments"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Neuen Kommentar hinzufügen","description":"Schreibt eine Zeile nach document_comments. Der Rumpf erlaubt 1 bis 4000 Zeichen. Nutzerkennung und E-Mail kommen aus der Sitzung und werden mitgespeichert; ob das Dokument ueberhaupt existiert, prueft der Aufruf nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":4000}},"required":["body"]},"example":{"body":"string"}}}}}},"/api/v1/documents/{id}/comments/{commentId}":{"delete":{"responses":{"200":{"description":"Endgueltig geloescht — kein Soft-Delete, die Zeile ist weg. `ok: true` kommt auch dann, wenn es den Kommentar nie gab oder er zu einem anderen Dokument gehoert: die Trefferzahl wird nicht geprueft. Eine Rechteprueng, WER loeschen darf, findet nicht statt — jeder Nutzer des Mandanten kann jeden Kommentar entfernen.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"deleteApiV1DocumentsByIdCommentsByCommentId","tags":["documents","Comments"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"commentId","required":true}],"summary":"Kommentar löschen","description":"Loescht die Zeile aus document_comments — endgueltig, ohne Soft-Delete und ohne Papierkorb. Geprueft wird allein, dass Kommentar- und Dokumentkennung zusammenpassen; wer den Kommentar geschrieben hat, spielt keine Rolle."}},"/api/v1/documents/{id}/summarize":{"post":{"responses":{"200":{"description":"Die Zusammenfassung.","content":{"application/json":{"schema":{"type":"object","properties":{"documentId":{"type":"string"},"summary":{"type":"string"},"keyPoints":{"type":"array","items":{"type":"string"}},"language":{"type":"string","enum":["de","en"]},"provider":{"type":"string"}},"required":["documentId","summary","language","provider"],"additionalProperties":false},"example":{"documentId":"string","summary":"string","keyPoints":["string"],"language":"de","provider":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"tenant_required"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Das Dokument liegt nicht im RAG-Index.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_indexed"},"hint":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Der Modellaufruf ist gescheitert.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsByIdSummarize","tags":["documents","ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"KI-Zusammenfassung eines Dokuments","description":"Liest den Text NICHT aus dem Dokument selbst, sondern aus\n`public.tenant_rag_documents` — den ersten 50 Abschnitten, die der\nnaechtliche Indexlauf dort abgelegt hat, auf 30 000 Zeichen gekuerzt.\nEin Dokument, das noch nicht indexiert ist, ergibt 404, auch wenn es\nexistiert.\n\nEs wird nichts gespeichert: die Zusammenfassung entsteht bei jedem\nAufruf neu und wird weder abgelegt noch protokolliert.\n\nIst ein Sprachmodell eingerichtet, kostet der Aufruf Kontingent. Ist\nkeines eingerichtet, antwortet der Endpunkt trotzdem 200 — dann mit den\nersten `maxSentences` Saetzen des Dokuments und\n`provider: \"extractive-fallback\"`. `keyPoints` fehlt in diesem Fall.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"maxSentences":{"type":"integer","exclusiveMinimum":0,"maximum":20,"default":5},"language":{"type":"string","enum":["de","en"],"default":"de"}}},"example":{"maxSentences":1,"language":"de"}}}}}},"/api/v1/documents/{id}/extract-lines":{"post":{"responses":{"200":{"description":"Die erkannten Positionen — oder eine leere Liste, wenn kein Modell eingerichtet war oder die Antwort unbrauchbar blieb.","content":{"application/json":{"schema":{"type":"object","properties":{"documentId":{"type":"string"},"lineItems":{"type":"array","items":{}},"provider":{"type":"string","enum":["no-key","no-json","claude-haiku-4-5"]},"rawText":{"type":"string"}},"required":["documentId"],"additionalProperties":true},"example":{"documentId":"string","lineItems":[],"provider":"no-key","rawText":"string"}}}},"400":{"description":"Kein Mandant im Anfragekontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"tenant_required"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Das Dokument liegt nicht im RAG-Index.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_indexed"},"hint":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Der Modellaufruf ist gescheitert.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsByIdExtract-lines","tags":["documents","ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rechnungspositionen aus Dokument extrahieren","description":"Wie `/summarize` liest der Endpunkt den Text aus\n`public.tenant_rag_documents` (erste 50 Abschnitte, 30 000 Zeichen).\nEin nicht indexiertes Dokument ergibt 404.\n\nDer Aufruf nimmt keinen Rumpf entgegen und legt nichts an: es entsteht\nweder ein Beleg noch ein Entwurf, die erkannten Positionen werden nur\nzurueckgegeben.\n\nOhne eingerichtetes Sprachmodell antwortet der Endpunkt 200 mit leerer\nPositionsliste und `provider: \"no-key\"` — nicht 503. Dasselbe gilt,\nwenn die Modellantwort kein JSON enthaelt (`no-json`). Was das Modell\nliefert, wird UNGEPRUEFT in die Antwort gemischt."}},"/api/v1/documents/by-entity":{"get":{"responses":{"200":{"description":"Liste der Dokumente","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt"],"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"total":0}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1DocumentsBy-entity","tags":["documents"],"parameters":[{"in":"query","name":"entityType","schema":{"type":"string","minLength":1,"description":"Art des Datensatzes, z.B. `order`, `customer`"},"required":true},{"in":"query","name":"entityId","schema":{"type":"string","minLength":1,"description":"Id des Datensatzes"},"required":true}],"summary":"List documents for a record","description":"Alle Dokumente einer Entitaet (z.B. Kunde, Rechnung) auflisten — am Stueck, ohne Paginierung, deshalb `{ data, total }` statt `pagination`."}},"/api/v1/documents":{"get":{"responses":{"200":{"description":"Liste der Dokumente","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"page":{"type":"number"},"limit":{"type":"number"},"total":{"type":"number"},"pages":{"type":"number"}},"required":["page","limit","total","pages"]}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"pagination":{"page":0,"limit":0,"total":0,"pages":0}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Documents","tags":["documents"],"parameters":[{"in":"query","name":"type","schema":{"type":"string","enum":["invoice","contract","offer","delivery_note","other"]}},{"in":"query","name":"entityType","schema":{"type":"string"}},{"in":"query","name":"entityId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"folderId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"number","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":25}}],"summary":"List documents","description":"Dokumente listen, gefiltert nach Typ, Entitaet oder Suchbegriff"},"post":{"responses":{"201":{"description":"Dokument angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Documents","tags":["documents"],"parameters":[],"summary":"Create document metadata","description":"Dokument-Metadaten anlegen (Datei wird per S3 pre-signed URL geladen) — diese Route nimmt KEINE Datei entgegen, dafuer gibt es POST /upload. Steuerrelevante Belegarten werden sofort unveraenderbar (WORM) abgelegt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":500},"description":{"type":"string"},"type":{"type":"string","enum":["invoice","contract","offer","delivery_note","other"],"default":"other"},"mimeType":{"type":"string","minLength":1,"maxLength":200},"size":{"type":"integer","minimum":0},"storageKey":{"type":"string","minLength":1},"storageProvider":{"type":"string","enum":["s3","local"],"default":"local"},"url":{"type":"string","format":"uri"},"entityType":{"type":"string"},"entityId":{"type":"string","format":"uuid"},"tags":{"type":"array","items":{"type":"string"},"default":[]},"expiresAt":{"type":"string","format":"date-time"}},"required":["name","mimeType","size","storageKey"]},"example":{"name":"string","description":"string","type":"invoice","mimeType":"string","size":0,"storageKey":"string","storageProvider":"s3","url":"https://example.com","entityType":"string","entityId":"00000000-0000-4000-8000-000000000000","tags":["string"],"expiresAt":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/documents/trash":{"get":{"responses":{"200":{"description":"Papierkorb-Inhalt (max. 200, ohne Paginierung)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1DocumentsTrash","tags":["documents"],"parameters":[],"summary":"List trashed documents","description":"Gelöschte Dokumente im Papierkorb auflisten. Harte Obergrenze von 200 Eintraegen, kein Blaettern — bei mehr fehlen die aeltesten stillschweigend."}},"/api/v1/documents/upload":{"post":{"responses":{"201":{"description":"Upload abgelegt + Metadaten gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"extractedEntities":{"type":"object","additionalProperties":{}}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt","extractedEntities"],"additionalProperties":false},"example":{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","extractedEntities":{}}}}},"400":{"description":"Bad Request"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"402":{"description":"Speicher-Kontingent des Tarifs überschritten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"storage_quota_exceeded"},"currentGb":{"type":"number"},"maxGb":{"type":"number"},"fileSize":{"type":"number"},"upgradeUrl":{"type":"string"}},"required":["error","currentGb","maxGb","fileSize","upgradeUrl"],"additionalProperties":false}}}},"413":{"description":"File too large"},"415":{"description":"Signatur nicht auf der Positivliste oder passt nicht zum gemeldeten Typ. Der Rumpf ist JSON, `error` traegt einen anzeigbaren deutschen Satz. HEIC (iPhone-Werkseinstellung) faellt hierunter und bekommt den Hinweis auf „Maximale Kompatibilität\".","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Verstaendliche Meldung, direkt anzeigbar"},"grund":{"type":"string","description":"Technischer Grund aus der Signaturpruefung"},"erkannt":{"type":"string","description":"Erkannter Dateityp, `unknown` wenn keiner passte"}},"required":["error","grund","erkannt"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsUpload","tags":["documents"],"parameters":[],"summary":"Datei hochladen (optional mit Belegart, Kategorie und Feldern)","description":"Datei hochladen ueber @nemix/storage Provider (S3/R2/local). Antwortet 201, sobald die Datei liegt — OCR, Klassifikation und die Aufnahme in die Wissensbasis laufen DANACH; ihr Ergebnis erscheint erst beim erneuten Abruf des Belegs. Ist das Speicher-Kontingent des Tarifs erschoepft, kommt 402; ab 90 Prozent Belegung traegt die Antwort den Kopf X-Storage-Soft-Warn. Wer den Beleg schon kennt, schickt ihn gleich mit: `belegart` setzt die Belegart (classified_type, dazu classified_by = \"user:<userId>\"), `kategorie` die Ausgabe-Kategorie, `felder` ein flaches JSON-Objekt mit bereits bekannten Werten. Diese drei Angaben sind Handarbeit und werden vom spaeteren Pipeline-Lauf nicht ueberschrieben. Ein ungueltiger Wert wird mit 400 abgelehnt und nicht still verworfen. `belegart` muss im Belegarten-Katalog des Mandanten stehen, und jeder Schluessel in `felder` muss zum Feldsatz genau dieser Belegart gehoeren. Ohne `belegart` sind `felder` unzulaessig, weil dann der Feldsatz zum Pruefen fehlt.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Die Datei selbst."},"entityType":{"type":"string","description":"Verknuepfte Entitaet, z. B. `customer`."},"entityId":{"type":"string","format":"uuid","description":"ID der verknuepften Entitaet."},"type":{"type":"string","description":"Grobe Dokumentart von Hand. `auto` oder weglassen ueberlaesst sie der Pipeline."},"belegart":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","maxLength":60,"description":"Slug der Belegart, z. B. `bewirtung`. Wird als classified_type gespeichert, mit classified_by = \"user:<userId>\" und classified_at = jetzt. Muss im Belegarten-Katalog des Mandanten stehen (GET /api/v1/dms-doc-types)."},"kategorie":{"type":"string","enum":["bewirtung","reisekosten","buero","wareneinkauf","software_it","kfz","miete","marketing","sonstiges"],"description":"Slug der Ausgabe-Kategorie. Wird als category gespeichert."},"felder":{"type":"string","maxLength":32768,"description":"JSON-Objekt als Zeichenkette, flache Schluessel-Wert-Paare (Text, Zahl oder null). Wird als extracted_entities gespeichert. Hoechstens 32768 Zeichen, 50 Schluessel und 2000 Zeichen je Wert. Jeder Schluessel muss zum Feldsatz der mitgeschickten `belegart` gehoeren; ohne `belegart` ist das Feld unzulaessig.","example":"{\"vendor_name\":\"Gasthaus Krone\",\"total_gross\":84.5}"}}}}}}}},"/api/v1/documents/upload-multi":{"post":{"responses":{"201":{"description":"Seiten zusammengefuegt, Beleg angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"extractedEntities":{"type":"object","additionalProperties":{}},"fileCount":{"type":"integer"},"pageCount":{"type":"integer"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt","extractedEntities","fileCount","pageCount"],"additionalProperties":false},"example":{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","extractedEntities":{},"fileCount":0,"pageCount":0}}}},"400":{"description":"Kein multipart/form-data, keine Datei, mehr als 20 Dateien, oder eine ungueltige Angabe in belegart / kategorie / felder","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"datei":{"type":"string"},"grund":{"type":"string"},"erkannt":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"402":{"description":"Speicher-Kontingent des Tarifs überschritten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"storage_quota_exceeded"},"currentGb":{"type":"number"},"maxGb":{"type":"number"},"fileSize":{"type":"number"},"upgradeUrl":{"type":"string"}},"required":["error","currentGb","maxGb","fileSize","upgradeUrl"],"additionalProperties":false}}}},"413":{"description":"Eine Seite, die Summe der Seiten oder der fertige Beleg ueberschreitet die Obergrenze des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"datei":{"type":"string"},"grund":{"type":"string"},"erkannt":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"415":{"description":"Eine Datei hat einen Typ, der sich nicht zusammenfuegen laesst, eine Signaturpruefung ist fehlgeschlagen, oder ein Bild war nicht lesbar. HEIC (iPhone-Werkseinstellung) faellt hierunter und bekommt denselben Hinweis wie beim Einzel-Upload.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"datei":{"type":"string"},"grund":{"type":"string"},"erkannt":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsUpload-multi","tags":["documents"],"parameters":[],"summary":"Mehrere Seiten zu einem Beleg hochladen","description":"Nimmt bis zu 20 Dateien im Feld `files` entgegen und macht daraus EINEN Beleg: ein mehrseitiges PDF, ein Datensatz, ein Pipeline-Lauf. Bilder (JPG, PNG) werden je zu einer Seite in Bildgroesse, ein mitgeschicktes PDF wird mit allen seinen Seiten uebernommen. Die Reihenfolge der Seiten ist die Reihenfolge der Dateien im Formular. Antwortet 201, sobald der Beleg liegt. OCR, Klassifikation und die Aufnahme in die Wissensbasis laufen danach; ihr Ergebnis erscheint erst beim erneuten Abruf des Belegs. Die Sofort-Erfassung gilt hier genauso wie beim Einzel-Upload: `belegart` setzt die Belegart (classified_type, dazu classified_by = \"user:<userId>\"), `kategorie` die Ausgabe-Kategorie, `felder` ein flaches JSON-Objekt mit bereits bekannten Werten. Diese drei Angaben sind Handarbeit und werden vom spaeteren Pipeline-Lauf nicht ueberschrieben. Jede Datei wird einzeln geprueft, und zwar VOLLSTAENDIG bevor irgendetwas gespeichert wird. Schlaegt eine einzige fehl, wird der ganze Aufruf abgelehnt und es bleibt weder eine Datei im Speicher noch eine Zeile in der Datenbank zurueck. Mehr als 20 Dateien werden abgelehnt und nicht still abgeschnitten.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["files"],"properties":{"files":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string","format":"binary"},"description":"Die Seiten, in der Reihenfolge, in der sie im Beleg stehen sollen. Hoechstens 20. Erlaubt sind image/jpeg, image/png und application/pdf. Wer stattdessen das Feld `file` mehrfach schickt (der Feldname des Einzel-Uploads), wird ebenfalls gelesen; gemischt werden die beiden Felder nie."},"name":{"type":"string","description":"Name des entstehenden Belegs. Ohne Angabe wird der Name der ersten Datei mit der Endung .pdf verwendet."},"entityType":{"type":"string","description":"Verknuepfte Entitaet, z. B. `customer`."},"entityId":{"type":"string","format":"uuid","description":"ID der verknuepften Entitaet."},"type":{"type":"string","description":"Grobe Dokumentart von Hand. `auto` oder weglassen ueberlaesst sie der Pipeline."},"belegart":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","maxLength":60,"description":"Slug der Belegart, z. B. `bewirtung`. Wird als classified_type gespeichert, mit classified_by = \"user:<userId>\" und classified_at = jetzt. Muss im Belegarten-Katalog des Mandanten stehen (GET /api/v1/dms-doc-types)."},"kategorie":{"type":"string","enum":["bewirtung","reisekosten","buero","wareneinkauf","software_it","kfz","miete","marketing","sonstiges"],"description":"Slug der Ausgabe-Kategorie. Wird als category gespeichert."},"felder":{"type":"string","maxLength":32768,"description":"JSON-Objekt als Zeichenkette, flache Schluessel-Wert-Paare (Text, Zahl oder null). Wird als extracted_entities gespeichert. Hoechstens 32768 Zeichen, 50 Schluessel und 2000 Zeichen je Wert. Jeder Schluessel muss zum Feldsatz der mitgeschickten `belegart` gehoeren; ohne `belegart` ist das Feld unzulaessig.","example":"{\"vendor_name\":\"Gasthaus Krone\",\"total_gross\":84.5}"}}}}}}}},"/api/v1/documents/{id}/raw":{"get":{"responses":{"200":{"description":"Datei-Bytes (Content-Type = mime_type des Belegs; bei `?variant=preview` immer application/pdf, ohne hinterlegten mime_type application/octet-stream). Dazu Content-Disposition (inline, mit `?download=1` attachment), Content-Length und `Cache-Control: private, max-age=300`.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Beleg nicht gefunden oder ohne hinterlegte Datei","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["document_not_found","no_storage_key"]}},"required":["error"],"additionalProperties":false}}}},"502":{"description":"Datei liegt in der Ablage nicht (mehr) vor","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"download_failed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1DocumentsByIdRaw","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Stream file bytes","description":"Datei-Bytes streamen (inline-Vorschau oder Download). `?download=1` erzwingt den Download statt der Anzeige; `?variant=preview` liefert bei einer E-Rechnung die lesbare PDF-Ansicht — ohne den Parameter kommt immer das massgebliche Original."}},"/api/v1/documents/{id}/presigned-url":{"get":{"responses":{"200":{"description":"Presigned URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"expiresIn":{"type":"number"}},"required":["url","expiresIn"],"additionalProperties":false},"example":{"url":"string","expiresIn":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1DocumentsByIdPresigned-url","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create a download URL","description":"Praesignierte Download-URL erzeugen. Voreinstellung 15 Minuten, ueber `?ttl` (Sekunden) einstellbar — der Wert wird serverseitig auf 60 bis 3600 begrenzt. Das zurueckgegebene `expiresIn` nennt den ANGEFRAGTEN Wert, nicht den gekappten: wer 7200 anfragt, bekommt eine Stunde und liest 7200."}},"/api/v1/documents/{id}":{"get":{"responses":{"200":{"description":"Dokument-Details","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"extractedEntities":{"type":"object","additionalProperties":{}},"downloadUrl":{"type":["string","null"]}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt","extractedEntities","downloadUrl"],"additionalProperties":false},"example":{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z","extractedEntities":{},"downloadUrl":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not Found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1DocumentsById","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get document detail","description":"Dokument-Details inkl. Download-URL abrufen. `downloadUrl` ist null, wenn das Praesignieren scheitert — der Beleg kommt dann trotzdem mit 200 zurueck."},"patch":{"responses":{"200":{"description":"Dokument aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"403":{"description":"WORM-archiviert oder freigegeben — nicht mehr änderbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string","enum":["WORM_PROTECTED","LOCKED"]}},"required":["error","code"],"additionalProperties":false}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1DocumentsById","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update document metadata","description":"Dokument-Metadaten (Name, Beschreibung, Tags, eigene Felder) aktualisieren. Eigene Felder werden GEMERGT: ein nicht mitgeschickter Schluessel bleibt stehen. WORM-archivierte und freigegebene Belege: 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":500},"description":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"customFields":{"type":"object","additionalProperties":{}}}},"example":{"name":"string","description":"string","tags":["string"],"customFields":{}}}}}},"delete":{"responses":{"200":{"description":"In Papierkorb verschoben","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"trashed":{"type":"boolean","const":true}},"required":["message","trashed"],"additionalProperties":false},"example":{"message":"string","trashed":true}}}},"401":{"description":"Unauthorized"},"403":{"description":"WORM-archiviert oder freigegeben — darf nicht gelöscht werden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string","enum":["WORM_PROTECTED","LOCKED"]}},"required":["error","code"],"additionalProperties":false}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1DocumentsById","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Move document to trash","description":"Dokument in den Papierkorb verschieben (soft-delete) — die Zeile und die Datei bleiben erhalten und sind ueber POST /:id/restore zurueckholbar. WORM-archivierte und gesperrte Belege: 403."}},"/api/v1/documents/{id}/restore":{"post":{"responses":{"200":{"description":"Wiederhergestellt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not Found — oder der Beleg lag gar nicht im Papierkorb","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsByIdRestore","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Restore document from trash","description":"Dokument aus dem Papierkorb wiederherstellen"}},"/api/v1/documents/{id}/permanent":{"delete":{"responses":{"200":{"description":"Endgültig gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"permanent":{"type":"boolean","const":true}},"required":["message","permanent"],"additionalProperties":false},"example":{"message":"string","permanent":true}}}},"401":{"description":"Unauthorized"},"403":{"description":"WORM-archiviert oder freigegeben — aufbewahrungspflichtig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string","enum":["WORM_PROTECTED","LOCKED"]}},"required":["error","code"],"additionalProperties":false}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1DocumentsByIdPermanent","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete document permanently","description":"Dokument endgültig löschen — Datenbankzeile + Datei aus dem Storage. Nicht umkehrbar. WORM-archivierte und freigegebene Belege sind aufbewahrungspflichtig und werden mit 403 abgelehnt."}},"/api/v1/documents/{id}/submit":{"post":{"responses":{"200":{"description":"Eingereicht","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Beleg nicht gefunden ODER gesperrt — der Handler kann das nicht unterscheiden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found_or_locked"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsByIdSubmit","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Submit document for approval","description":"Dokument zur Freigabe einreichen (Status: in Freigabe). Setzt nur approval_status; die Kanban-Bahn (pipeline_status) bleibt, wo sie ist — eine eigene Pruef-Bahn gibt es nicht."}},"/api/v1/documents/{id}/approve":{"post":{"responses":{"200":{"description":"Freigegeben + archiviert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not Found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsByIdApprove","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Approve and archive document","description":"Dokument freigeben und archivieren (wird gesperrt). Steuerrelevante Belege werden dabei unveraenderbar (WORM) festgeschrieben — auch dann, wenn die Belegart erst durch die Klassifikation feststand. Ein gesperrter Beleg laesst sich danach weder aendern noch loeschen."}},"/api/v1/documents/{id}/reject":{"post":{"responses":{"200":{"description":"Zurückgewiesen","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"storageKey":{"type":"string"},"storageProvider":{"type":"string"},"uploadedBy":{"type":"string"},"size":{"anyOf":[{"type":"string"},{"type":"number"}]},"classifiedConfidence":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"tags":{"type":"array","items":{}},"customFields":{"type":"object","additionalProperties":{}},"description":{"type":["string","null"]},"type":{"type":["string","null"]},"classifiedType":{"type":["string","null"]},"classifiedBy":{"type":["string","null"]},"category":{"type":["string","null"]},"approvalStatus":{"type":["string","null"]},"pipelineStatus":{"type":["string","null"]},"url":{"type":["string","null"]},"entityType":{"type":["string","null"]},"entityId":{"type":["string","null"]},"submittedAt":{"type":["string","null"],"format":"date-time"},"lockedAt":{"type":["string","null"],"format":"date-time"},"approvedAt":{"type":["string","null"],"format":"date-time"},"classifiedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","mimeType","storageKey","storageProvider","uploadedBy","size","classifiedConfidence","tags","customFields","description","type","classifiedType","classifiedBy","category","approvalStatus","pipelineStatus","url","entityType","entityId","submittedAt","lockedAt","approvedAt","classifiedAt","expiresAt","deletedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","name":"string","mimeType":"string","storageKey":"string","storageProvider":"string","uploadedBy":"string","size":"string","classifiedConfidence":"string","tags":[],"customFields":{},"description":"string","type":"string","classifiedType":"string","classifiedBy":"string","category":"string","approvalStatus":"string","pipelineStatus":"string","url":"string","entityType":"string","entityId":"string","submittedAt":"2026-01-01T12:00:00.000Z","lockedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","classifiedAt":"2026-01-01T12:00:00.000Z","expiresAt":"2026-01-01T12:00:00.000Z","deletedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Beleg nicht gefunden ODER gesperrt — der Handler kann das nicht unterscheiden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found_or_locked"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsByIdReject","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Reject document approval","description":"Freigabe zurückweisen — Dokument geht zurück in Bearbeitung. Ein Rumpf ist freiwillig; ein darin mitgegebenes `note` landet im Verlauf."}},"/api/v1/documents/{id}/events":{"get":{"responses":{"200":{"description":"Event-Liste, aelteste zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"eventType":{"type":"string"},"actorId":{"type":["string","null"]},"actorName":{"type":["string","null"]},"note":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"}},"required":["id","eventType","actorId","actorName","note","createdAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","eventType":"string","actorId":"string","actorName":"string","note":"string","createdAt":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1DocumentsByIdEvents","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List document timeline events","description":"Aktivitäts-/Versionsverlauf eines Dokuments (Timeline). Die Route prueft NICHT, ob es den Beleg gibt — eine unbekannte Id liefert 200 mit leerer Liste, kein 404."}},"/api/v1/documents/{id}/share":{"post":{"responses":{"200":{"description":"Share-Token erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"shareToken":{"type":"string"},"shareUrl":{"type":"string"},"expiresAt":{"type":["string","null"]}},"required":["shareToken","shareUrl","expiresAt"],"additionalProperties":false},"example":{"shareToken":"string","shareUrl":"string","expiresAt":"string"}}}},"400":{"description":"Ungültige Laufzeit im Rumpf","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_error"},"issues":{"type":"array","items":{}}},"required":["error","issues"],"additionalProperties":false}}}},"401":{"description":"Unauthorized"},"404":{"description":"Not Found (text/plain)"},"503":{"description":"Database unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsByIdShare","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create share link","description":"Share-Link (Token) fuer ein Dokument erzeugen. Antwortet 200, nicht 201. Der Rumpf ist freiwillig (ohne ihn gelten 72 Stunden); die Laufzeit wird intern auf volle Tage aufgerundet, mindestens einen. Der Link zeigt auf die oeffentliche Seite /share/<token>."}},"/api/v1/dms-routing-rules":{"get":{"responses":{"200":{"description":"Alle Routing-Regeln des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"active":{"type":"boolean"},"rule_json":{"type":"object","additionalProperties":{}},"created_at":{},"updated_at":{}},"required":["id","name","active","rule_json"]}}},"required":["data"]},"example":{"data":[{"id":"string","name":"string","active":true,"rule_json":{}}]}}}},"401":{"description":"Kein Mandantenkontext"},"500":{"description":"Abfrage fehlgeschlagen"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"getApiV1Dms-routing-rules","tags":["dms"],"parameters":[],"summary":"Liste aller Routing-Regeln","description":"Liefert alle Regeln des aktiven Mandanten mit Bedingungen und Zeitstempeln, aufsteigend nach Anlagedatum. Es gibt weder Filter noch Blätterung, und inaktive Regeln (`active: false`) stehen mit in der Liste. Beim ersten Aufruf legt die Route die Tabelle `public.dms_routing_rules` samt Index und Row-Level-Security-Policy an."},"post":{"responses":{"201":{"description":"Regel angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"active":{"type":"boolean"},"rule_json":{"type":"object","properties":{"when":{"type":"object","properties":{"classified_type":{"type":"string"},"min_total_gross":{"type":"number"},"max_total_gross":{"type":"number"},"vendor_contains":{"type":"string"},"direction":{"type":"string","enum":["incoming","outgoing"]}}},"then":{"type":"object","properties":{"set_status":{"type":"string","enum":["routed","approved"]},"assign_to_user":{"type":"string","format":"uuid"},"move_to_folder_id":{"type":"string","format":"uuid"}}}},"required":["when","then"]}},"required":["id","name","active","rule_json"]}},"required":["data"]},"example":{"data":{"id":"string","name":"string","active":true,"rule_json":{"when":{"classified_type":"string","min_total_gross":0,"max_total_gross":0,"vendor_contains":"string","direction":"incoming"},"then":{"set_status":"routed","assign_to_user":"00000000-0000-4000-8000-000000000000","move_to_folder_id":"00000000-0000-4000-8000-000000000000"}}}}}}},"400":{"description":"Rumpf entspricht nicht dem Schema"},"401":{"description":"Kein Mandantenkontext"},"500":{"description":"Anlegen fehlgeschlagen"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"postApiV1Dms-routing-rules","tags":["dms"],"parameters":[],"summary":"Neue Routing-Regel anlegen","description":"Legt eine Regel aus `name`, `active` (Vorgabe `true`) und `rule_json` an. `rule_json` braucht beide Blöcke: `when` mit den Bedingungen (Belegart, Brutto-Unter- und -Obergrenze, Lieferantentext, Richtung) und `then` mit den Aktionen (Status setzen, Benutzer zuweisen, in einen Ordner verschieben). Die Antwort enthält nur die eingefügten Spalten ohne Zeitstempel; auf doppelte Namen wird nicht geprüft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"active":{"type":"boolean","default":true},"rule_json":{"type":"object","properties":{"when":{"type":"object","properties":{"classified_type":{"type":"string"},"min_total_gross":{"type":"number"},"max_total_gross":{"type":"number"},"vendor_contains":{"type":"string"},"direction":{"type":"string","enum":["incoming","outgoing"]}}},"then":{"type":"object","properties":{"set_status":{"type":"string","enum":["routed","approved"]},"assign_to_user":{"type":"string","format":"uuid"},"move_to_folder_id":{"type":"string","format":"uuid"}}}},"required":["when","then"]}},"required":["name","rule_json"]},"example":{"name":"string","active":true,"rule_json":{"when":{"classified_type":"string","min_total_gross":0,"max_total_gross":0,"vendor_contains":"string","direction":"incoming"},"then":{"set_status":"routed","assign_to_user":"00000000-0000-4000-8000-000000000000","move_to_folder_id":"00000000-0000-4000-8000-000000000000"}}}}}}}},"/api/v1/dms-routing-rules/{id}":{"patch":{"responses":{"200":{"description":"Aktualisierung ausgeführt, auch wenn keine Zeile passte","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]},"example":{"success":true}}}},"400":{"description":"Rumpf entspricht nicht dem Schema"},"401":{"description":"Kein Mandantenkontext"},"500":{"description":"Aktualisierung fehlgeschlagen"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"patchApiV1Dms-routing-rulesById","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Regel aktualisieren","description":"Aktualisiert wahlweise `name`, `active` oder `rule_json`; alle drei Felder sind einzeln optional, ein leerer Rumpf ist gültig und setzt nur `updated_at` neu. Die Antwort ist immer `{ \"success\": true }` und sagt nichts darüber aus, ob eine Zeile getroffen wurde: eine ID, zu der es im Mandanten keine Regel gibt, wird still mit 200 quittiert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"active":{"type":"boolean"},"rule_json":{"type":"object","properties":{"when":{"type":"object","properties":{"classified_type":{"type":"string"},"min_total_gross":{"type":"number"},"max_total_gross":{"type":"number"},"vendor_contains":{"type":"string"},"direction":{"type":"string","enum":["incoming","outgoing"]}}},"then":{"type":"object","properties":{"set_status":{"type":"string","enum":["routed","approved"]},"assign_to_user":{"type":"string","format":"uuid"},"move_to_folder_id":{"type":"string","format":"uuid"}}}},"required":["when","then"]}}},"example":{"name":"string","active":true,"rule_json":{"when":{"classified_type":"string","min_total_gross":0,"max_total_gross":0,"vendor_contains":"string","direction":"incoming"},"then":{"set_status":"routed","assign_to_user":"00000000-0000-4000-8000-000000000000","move_to_folder_id":"00000000-0000-4000-8000-000000000000"}}}}}}},"delete":{"responses":{"204":{"description":"Regel gelöscht oder nicht vorhanden. Kein Antwortrumpf."},"401":{"description":"Kein Mandantenkontext"},"500":{"description":"Löschen fehlgeschlagen"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"deleteApiV1Dms-routing-rulesById","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Regel löschen","description":"Entfernt die Regel endgültig aus `public.dms_routing_rules`. Es gibt weder Soft-Delete noch einen Weg zurück. Gelöscht wird nur im Mandanten des Aufrufers; eine unbekannte ID gilt ebenfalls als Erfolg und liefert 204."}},"/api/v1/dms-doc-types":{"get":{"responses":{"200":{"description":"Alle Belegarten des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type_key":{"type":"string"},"label":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"match_hints":{"type":"array","items":{"type":"string"}},"is_builtin":{"type":"boolean"},"is_custom":{"type":"boolean"},"sort_order":{"type":"number"},"tax_export":{"type":"boolean"},"directional":{"type":"boolean"},"fields_outgoing":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"label_incoming":{"type":["string","null"]},"label_outgoing":{"type":["string","null"]}},"required":["id","type_key","label","fields","match_hints","is_builtin","is_custom","sort_order","tax_export","directional","fields_outgoing","label_incoming","label_outgoing"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","type_key":"string","label":"string","fields":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"match_hints":["string"],"is_builtin":true,"is_custom":true,"sort_order":0,"tax_export":true,"directional":true,"fields_outgoing":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"label_incoming":"string","label_outgoing":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Dms-doc-types","tags":["dms"],"parameters":[],"summary":"Alle Belegart-Feldvorlagen","description":"Legt `belegart_templates` beim ersten Zugriff an, saet die eingebauten Belegarten hinein und zieht deren Pflichtfeld- und Richtungs-Vorgaben nach; eigene Aenderungen des Mandanten bleiben dabei stehen. Liefert danach alle nicht geloeschten Vorlagen, sortiert nach `sort_order` und bei Gleichstand nach Label. Eingebaute und selbst angelegte stehen in derselben Liste; `is_builtin` und `is_custom` unterscheiden sie."},"post":{"responses":{"201":{"description":"Die angelegte Belegart","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"type_key":{"type":"string"},"label":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"match_hints":{"type":"array","items":{"type":"string"}},"is_builtin":{"type":"boolean"},"is_custom":{"type":"boolean"},"sort_order":{"type":"number"},"tax_export":{"type":"boolean"},"directional":{"type":"boolean"},"fields_outgoing":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"label_incoming":{"type":["string","null"]},"label_outgoing":{"type":["string","null"]}},"required":["id","type_key","label","fields","match_hints","is_builtin","is_custom","sort_order","tax_export","directional","fields_outgoing","label_incoming","label_outgoing"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"id":"string","type_key":"string","label":"string","fields":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"match_hints":["string"],"is_builtin":true,"is_custom":true,"sort_order":0,"tax_export":true,"directional":true,"fields_outgoing":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"label_incoming":"string","label_outgoing":"string"}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Dms-doc-types","tags":["dms"],"parameters":[],"summary":"Neue Belegart anlegen","description":"Legt eine eigene Belegart an (201). Der `type_key` wird aus dem Label abgeleitet — Umlaute ausgeschrieben, alles Uebrige zu Unterstrichen, hoechstens 40 Zeichen — und bei Kollision mit `_2`, `_3` und so fort fortgezaehlt; mitgeben laesst er sich nicht. Die neue Belegart bekommt die hoechste vorhandene `sort_order` plus eins und traegt `is_custom`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":80},"fields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","minLength":1,"maxLength":40},"label":{"type":"string","minLength":1,"maxLength":80},"kind":{"type":"string","enum":["text","number","date","iban","currency"]},"ai_hint":{"type":"string","maxLength":240},"required":{"type":"boolean"}},"required":["key","label","kind"]},"maxItems":60,"default":[]},"match_hints":{"type":"array","items":{"type":"string","minLength":1,"maxLength":60},"maxItems":40,"default":[]}},"required":["label"]},"example":{"label":"string","fields":[],"match_hints":["string"]}}}}}},"/api/v1/dms-doc-types/{typeKey}":{"get":{"responses":{"200":{"description":"Die Belegart-Vorlage","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"type_key":{"type":"string"},"label":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"match_hints":{"type":"array","items":{"type":"string"}},"is_builtin":{"type":"boolean"},"is_custom":{"type":"boolean"},"sort_order":{"type":"number"},"tax_export":{"type":"boolean"},"directional":{"type":"boolean"},"fields_outgoing":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"label_incoming":{"type":["string","null"]},"label_outgoing":{"type":["string","null"]}},"required":["id","type_key","label","fields","match_hints","is_builtin","is_custom","sort_order","tax_export","directional","fields_outgoing","label_incoming","label_outgoing"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"id":"string","type_key":"string","label":"string","fields":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"match_hints":["string"],"is_builtin":true,"is_custom":true,"sort_order":0,"tax_export":true,"directional":true,"fields_outgoing":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"label_incoming":"string","label_outgoing":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Belegart nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Dms-doc-typesByTypeKey","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"typeKey","required":true}],"summary":"Eine Belegart-Vorlage","description":"Liest genau eine Vorlage ueber ihren `type_key` — nicht ueber die id. Ein unbekannter oder geloeschter Schluessel ergibt 404 `doc_type_not_found`. Die Antwort traegt beide Feldsaetze: `fields` fuer den Eingang und `fields_outgoing` fuer den Ausgang, letzterer nur gefuellt, wenn die Belegart richtungsabhaengig ist."},"patch":{"responses":{"200":{"description":"Die gespeicherte Belegart-Vorlage","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"type_key":{"type":"string"},"label":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"match_hints":{"type":"array","items":{"type":"string"}},"is_builtin":{"type":"boolean"},"is_custom":{"type":"boolean"},"sort_order":{"type":"number"},"tax_export":{"type":"boolean"},"directional":{"type":"boolean"},"fields_outgoing":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"label_incoming":{"type":["string","null"]},"label_outgoing":{"type":["string","null"]}},"required":["id","type_key","label","fields","match_hints","is_builtin","is_custom","sort_order","tax_export","directional","fields_outgoing","label_incoming","label_outgoing"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"id":"string","type_key":"string","label":"string","fields":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"match_hints":["string"],"is_builtin":true,"is_custom":true,"sort_order":0,"tax_export":true,"directional":true,"fields_outgoing":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"label_incoming":"string","label_outgoing":"string"}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Belegart nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1Dms-doc-typesByTypeKey","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"typeKey","required":true}],"summary":"Belegart-Vorlage bearbeiten","description":"Schreibt nur die mitgeschickten Felder; nicht gesendete bleiben unberuehrt. Wird `fields` oder `fields_outgoing` mitgeschickt, ERSETZT die gesendete Liste den gespeicherten Satz vollstaendig — ein einzelnes Feld laesst sich so nicht nachtragen. Gilt auch fuer eingebaute Belegarten; deren Auslieferungsstand holt /{typeKey}/reset zurueck. Ein unbekannter Schluessel ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":80},"fields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","minLength":1,"maxLength":40},"label":{"type":"string","minLength":1,"maxLength":80},"kind":{"type":"string","enum":["text","number","date","iban","currency"]},"ai_hint":{"type":"string","maxLength":240},"required":{"type":"boolean"}},"required":["key","label","kind"]},"maxItems":60},"match_hints":{"type":"array","items":{"type":"string","minLength":1,"maxLength":60},"maxItems":40},"tax_export":{"type":"boolean"},"fields_outgoing":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","minLength":1,"maxLength":40},"label":{"type":"string","minLength":1,"maxLength":80},"kind":{"type":"string","enum":["text","number","date","iban","currency"]},"ai_hint":{"type":"string","maxLength":240},"required":{"type":"boolean"}},"required":["key","label","kind"]},"maxItems":60},"label_incoming":{"type":["string","null"],"maxLength":80},"label_outgoing":{"type":["string","null"],"maxLength":80}}},"example":{"label":"string","fields":[],"match_hints":["string"],"tax_export":true,"fields_outgoing":[],"label_incoming":"string","label_outgoing":"string"}}}}},"delete":{"responses":{"204":{"description":"Geloescht — ohne Rumpf"},"400":{"description":"Eingebaute Belegart (`cannot_delete_builtin`)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Belegart nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1Dms-doc-typesByTypeKey","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"typeKey","required":true}],"summary":"Custom-Belegart löschen","description":"Soft-Delete: setzt `deleted_at`, die Zeile bleibt in `belegart_templates` stehen. Der `type_key` wird dadurch wieder frei, denn der Eindeutigkeits-Index gilt nur fuer nicht geloeschte Zeilen. Eingebaute Belegarten lassen sich nicht loeschen — der Versuch endet mit 400 `cannot_delete_builtin`. Bereits abgelegte Belege dieser Art fasst die Route nicht an."}},"/api/v1/dms-doc-types/reorder":{"post":{"responses":{"200":{"description":"Reihenfolge gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"],"additionalProperties":false},"example":{"success":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Dms-doc-typesReorder","tags":["dms"],"parameters":[],"summary":"Belegart-Reihenfolge speichern","description":"Nimmt eine Liste von `type_key`s (hoechstens 100) und schreibt deren Position nach `sort_order`; der erste Schluessel bekommt 0. Belegarten, die NICHT in der Liste stehen, behalten ihre bisherige Position und koennen dadurch mit einer neu vergebenen zusammenfallen. Unbekannte Schluessel werden stillschweigend uebergangen — die Antwort meldet in jedem Fall Erfolg.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string","minLength":1,"maxLength":60},"maxItems":100}},"required":["keys"]},"example":{"keys":["string"]}}}}}},"/api/v1/dms-doc-types/{typeKey}/duplicate":{"post":{"responses":{"201":{"description":"Die angelegte Kopie","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"type_key":{"type":"string"},"label":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"match_hints":{"type":"array","items":{"type":"string"}},"is_builtin":{"type":"boolean"},"is_custom":{"type":"boolean"},"sort_order":{"type":"number"},"tax_export":{"type":"boolean"},"directional":{"type":"boolean"},"fields_outgoing":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"label_incoming":{"type":["string","null"]},"label_outgoing":{"type":["string","null"]}},"required":["id","type_key","label","fields","match_hints","is_builtin","is_custom","sort_order","tax_export","directional","fields_outgoing","label_incoming","label_outgoing"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"id":"string","type_key":"string","label":"string","fields":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"match_hints":["string"],"is_builtin":true,"is_custom":true,"sort_order":0,"tax_export":true,"directional":true,"fields_outgoing":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"label_incoming":"string","label_outgoing":"string"}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Quell-Belegart nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Dms-doc-typesByTypeKeyDuplicate","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"typeKey","required":true}],"summary":"Belegart duplizieren (Felder + Einstellungen kopieren)","description":"Legt eine NEUE eigene Belegart an und uebernimmt vom Original die Felder, das Steuerberater-Kennzeichen und die gesamte Richtungs-Konfiguration (201). Die Match-Stichwoerter werden bewusst NICHT mitkopiert, sonst konkurrierten Original und Kopie um dieselbe Auto-Erkennung. Ohne eigenes `label` haengt der Handler an den Namen des Originals den Zusatz (Kopie). Die Quelle darf eingebaut oder eigen sein, die Kopie ist immer eigen. Unbekannte Quelle → 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":80}}},"example":{"label":"string"}}}}}},"/api/v1/dms-doc-types/{typeKey}/reset":{"post":{"responses":{"200":{"description":"Die zurueckgesetzte Belegart","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"type_key":{"type":"string"},"label":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"match_hints":{"type":"array","items":{"type":"string"}},"is_builtin":{"type":"boolean"},"is_custom":{"type":"boolean"},"sort_order":{"type":"number"},"tax_export":{"type":"boolean"},"directional":{"type":"boolean"},"fields_outgoing":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string"},"ai_hint":{"type":"string"},"required":{"type":"boolean"}},"required":["key","label","kind"],"additionalProperties":false}},"label_incoming":{"type":["string","null"]},"label_outgoing":{"type":["string","null"]}},"required":["id","type_key","label","fields","match_hints","is_builtin","is_custom","sort_order","tax_export","directional","fields_outgoing","label_incoming","label_outgoing"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"id":"string","type_key":"string","label":"string","fields":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"match_hints":["string"],"is_builtin":true,"is_custom":true,"sort_order":0,"tax_export":true,"directional":true,"fields_outgoing":[{"key":"string","label":"string","kind":"string","ai_hint":"string","required":true}],"label_incoming":"string","label_outgoing":"string"}}}}},"400":{"description":"Kein eingebauter Typ (`not_a_builtin`)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Belegart nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Dms-doc-typesByTypeKeyReset","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"typeKey","required":true}],"summary":"Built-in-Belegart auf Standard zurücksetzen","description":"Setzt Label, Felder, Match-Stichwoerter und die Richtungs-Konfiguration einer EINGEBAUTEN Belegart auf den Auslieferungsstand zurueck; alle Aenderungen des Mandanten daran gehen dabei verloren. `sort_order` und das Steuerberater-Kennzeichen bleiben stehen. Fuer eine selbst angelegte Belegart gibt es keinen Standard — der Aufruf endet mit 400 `not_a_builtin`."}},"/api/v1/dms-export/candidates":{"get":{"responses":{"200":{"description":"Die Auswahl plus die Belegart-Schlüssel, aus denen sie sich ableitet.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"classified_type":{"type":["string","null"],"description":"Erkannte Belegart, z. B. `rechnung_eingang`. Null, solange nichts erkannt wurde."},"created_at":{"type":"string","description":"Eingangsdatum im Archiv."},"tax_exported_at":{"type":["string","null"],"description":"Zeitpunkt des letzten Exports. Null = noch nie übertragen."},"tax_export_required":{"type":["boolean","null"],"description":"Übersteuerung pro Beleg: true/false erzwingen, null erbt von der Belegart."},"size":{"type":["number","null"],"description":"Dateigröße in Bytes."},"booking_date":{"type":["string","null"],"description":"Rechnungsdatum aus der Beleg-Erkennung, sonst der Eingangstag (Europe/Berlin), Form JJJJ-MM-TT."},"direction":{"type":["string","null"],"description":"`incoming` = Eingangsbeleg/Ausgabe, `outgoing` = Ausgangsbeleg/Einnahme. Null, wenn nicht erfasst."}},"required":["id","name","classified_type","created_at","tax_exported_at","tax_export_required","size","booking_date","direction"]}},"taxExportKeys":{"type":"array","items":{"type":"string"},"description":"`type_key` aller Belegarten mit gesetztem Steuerberater-Haken. Leer, wenn es die Belegart-Tabelle im Mandanten noch nicht gibt."}},"required":["data","taxExportKeys"]},"example":{"data":[{"id":"string","name":"string","classified_type":"string","created_at":"string","tax_exported_at":"string","tax_export_required":true,"size":0,"booking_date":"string","direction":"string"}],"taxExportKeys":["string"]}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"503":{"description":"`database_unavailable` — auch bei einem Abfragefehler; die Antwort trägt `Retry-After: 5`."}},"operationId":"getApiV1Dms-exportCandidates","tags":["dms"],"parameters":[{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}},{"in":"query","name":"includeExported","schema":{"type":"string","enum":["0","1"]}},{"in":"query","name":"mode","schema":{"type":"string","enum":["marked","all","folder"]}},{"in":"query","name":"folderIds","schema":{"type":"string"}},{"in":"query","name":"income","schema":{"type":"string","enum":["0","1"]}},{"in":"query","name":"expense","schema":{"type":"string","enum":["0","1"]}}],"summary":"Belege, die für den Steuerberater-Export vorgemerkt sind","description":"Stellt die Auswahl für den nächsten Export zusammen. Drei Modi über\n`mode`:\n\n- `marked` (Vorgabe): vorgemerkt ist ein Beleg, wenn sein eigener Haken\n  `tax_export_required` gesetzt ist ODER — solange der leer ist — seine\n  Belegart den Haken trägt.\n- `all`: alle FREIGEGEBENEN Belege des Zeitraums (`pipeline_status =\n  approved`), unabhängig von jedem Haken.\n- `folder`: wie `all`, aber auf die unter `folderIds` gewählten\n  Archiv-Ordner samt Unterordnern begrenzt. Ohne `mode`, aber mit\n  `folderIds` gilt automatisch dieser Modus.\n\n`from` und `to` grenzen über das Buchungsdatum ein und sind BEIDE\neinschließend. Verglichen wird als Text (JJJJ-MM-TT), damit ein per\nErkennung gefundenes Unsinnsdatum nicht den ganzen Aufruf abbricht.\n\nBereits übertragene Belege fehlen in der Antwort, bis `includeExported=1`\ngesetzt wird. `income`/`expense` schränken auf eine Richtung ein, sobald\ngenau eines auf `0` steht; Belege ohne erfasste Richtung bleiben dabei\nimmer drin. Gelöschte Belege sind grundsätzlich draußen.\n\nDie Liste ist NICHT blätterbar und nicht begrenzt — sie enthält jeden\nTreffer, sortiert nach Buchungsdatum aufsteigend. Bei sehr großen\nZeiträumen ist das Ergebnis entsprechend groß.\n\nDiese Route ändert nichts: gezählt wird erst beim Erzeugen des ZIP."}},"/api/v1/dms-export/document/{id}":{"patch":{"responses":{"200":{"description":"Kennung und der jetzt gültige Wert der Übersteuerung.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tax_export_required":{"type":["boolean","null"]}},"required":["id","tax_export_required"]}},"required":["data"]},"example":{"data":{"id":"string","tax_export_required":true}}}}},"400":{"description":"`required` fehlt oder ist weder Wahrheitswert noch null."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`document_not_found` — unbekannt oder gelöscht."},"503":{"description":"`database_unavailable`; die Antwort trägt `Retry-After: 5`."}},"operationId":"patchApiV1Dms-exportDocumentById","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Beleg für den Steuerberater-Export vormerken/ausschließen","description":"Setzt die Übersteuerung eines EINZELNEN Belegs. `required` ist Pflicht\nund kennt drei Werte: `true` merkt vor, `false` schließt aus, `null`\nnimmt die Übersteuerung zurück — dann entscheidet wieder der Haken der\nBelegart. `null` ist also nicht dasselbe wie `false`.\n\nGeschrieben wird ausschließlich `tax_export_required` (plus\n`updated_at`); der Beleg selbst bleibt unangetastet.\n\nDer Übertragungsstempel bleibt stehen: ein schon exportierter Beleg\ntaucht durch ein erneutes Vormerken NICHT wieder in\n`GET /candidates` auf — dafür braucht es dort `includeExported=1`.\n\nGelöschte Belege sind nicht erreichbar; sie ergeben denselben 404 wie\neine unbekannte Kennung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"required":{"type":["boolean","null"]}},"required":["required"]},"example":{"required":true}}}}}},"/api/v1/dms-export/history":{"get":{"responses":{"200":{"description":"Die letzten 50 Läufe, neueste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"file_name":{"type":"string","description":"Dateiname des damals erzeugten ZIP."},"doc_count":{"type":"integer","description":"Anzahl der BELEGE im Paket (eine E-Rechnung kann zwei Dateien beisteuern)."},"total_bytes":{"type":"integer","description":"Summe der abgelegten Beleg-Dateien in Bytes, vor der Komprimierung und ohne document.xml."},"period_from":{"type":["string","null"],"description":"Der beim Erzeugen übergebene Zeitraum — reine Notiz."},"period_to":{"type":["string","null"]},"created_by":{"type":["string","null"],"description":"E-Mail-Adresse, sonst Benutzerkennung, sonst null."},"created_at":{"type":"string"}},"required":["id","file_name","doc_count","total_bytes","period_from","period_to","created_by","created_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","file_name":"string","doc_count":0,"total_bytes":0,"period_from":"string","period_to":"string","created_by":"string","created_at":"string"}]}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"503":{"description":"`database_unavailable`; die Antwort trägt `Retry-After: 5`."}},"operationId":"getApiV1Dms-exportHistory","tags":["dms"],"parameters":[],"summary":"Vergangene Steuerberater-Export-Läufe","description":"Listet die letzten 50 Einträge aus `tax_export_runs`, neueste zuerst.\nNicht blätterbar, kein Filter, kein Zeitraum.\n\nJe erzeugtem ZIP entsteht genau eine Zeile. Sie hält fest, WAS übergeben\nwurde — nicht die Datei selbst: das ZIP wird nirgends aufbewahrt, ein\nzweiter Download ist über diese Route nicht möglich. Wird ein Paket\nerneut gebraucht, muss es über `POST /generate` neu erzeugt werden.\n\n`period_from`/`period_to` sind die beim Erzeugen mitgegebenen Werte und\nreine Notiz — sie beschreiben nicht zwingend, was tatsächlich im Paket\nlag, denn die Belege wurden über ihre Kennungen ausgewählt.\n\nFehlgeschlagene Läufe stehen NICHT hier: die Zeile entsteht erst, wenn\ndas ZIP fertig ist."}},"/api/v1/dms-export/generate":{"post":{"responses":{"200":{"description":"Das ZIP-Paket als Download: document.xml, die Belegdateien und bei `format=pdf` ggf. hinweis.txt.","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"`no_documents` — keine der Kennungen gehört zu einem lebenden Beleg. Oder der Rumpf hält das Schema nicht ein (keine UUID, leer, mehr als 1000)."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"422":{"description":"`no_files` — Belege gefunden, aber bei keinem waren Dateidaten abrufbar. Die Antwort nennt unter `failed` die betroffenen Belege."},"503":{"description":"`database_unavailable`; die Antwort trägt `Retry-After: 5`."}},"operationId":"postApiV1Dms-exportGenerate","tags":["dms"],"parameters":[],"summary":"DATEV-Belegbild-ZIP (document.xml + PDFs) erzeugen","description":"ANTWORT IST KEIN JSON: im Erfolgsfall kommt das ZIP selbst zurück\n(`application/zip`), als Download benannt `Belege_JJJJ-MM-TT.zip`.\nDie Kopfzeile `X-Export-Count` nennt die Zahl der Belege im Paket,\n`X-Export-Failed` erscheint nur, wenn Belege übersprungen wurden. Erst im\nFehlerfall (400/422/503) antwortet die Route mit JSON.\n\nAusgewählt wird über `documentIds` (1 bis 1000 Kennungen), NICHT über\neinen Zeitraum: `from`/`to` werden lediglich in der Lauf-Historie\nvermerkt und schränken die Auswahl nicht ein.\n\n`format` bestimmt, was je Beleg im Paket landet:\n\n- `original` (Vorgabe): nur die Originaldatei, bei einer X-Rechnung also\n  die XML.\n- `both`: zusätzlich die Gegenform — zur reinen XML die lesbare\n  PDF-Ansicht, zur ZUGFeRD-PDF die darin eingebettete XML. Beide Dateien\n  hängen am selben `guid`, der Steuerberater sieht EINEN Beleg.\n- `pdf`: statt der XML die erzeugte PDF-Ansicht, Bilder werden\n  umgewandelt. Klappt beides nicht, bleibt das Original im Paket und der\n  Beleg wird in einer beigelegten `hinweis.txt` genannt.\n\nEin Beleg, dessen Datei nicht abrufbar ist, wird ÜBERSPRUNGEN, nicht\nnachgeliefert — der Lauf bricht deswegen nicht ab. Die Zahl steht in\n`X-Export-Failed`, die Namen nur im Serverprotokoll.\n\nNEBENWIRKUNGEN: alle tatsächlich verpackten Belege bekommen\n`tax_exported_at = jetzt` und verschwinden damit aus `GET /candidates`;\nzusätzlich entsteht eine Zeile in der Lauf-Historie. Ein erneuter Export\nderselben Kennungen ist jederzeit möglich und setzt den Stempel neu.\n\nDie Verwaltungsdatei `document.xml` folgt der DATEV-Schnittstelle v05.0;\nje Beleg steht dort `type=2` für einen Ausgangsbeleg, sonst `type=1`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"documentIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":1000},"from":{"type":"string"},"to":{"type":"string"},"format":{"type":"string","enum":["original","both","pdf"]}},"required":["documentIds"]},"example":{"documentIds":["00000000-0000-4000-8000-000000000000"],"from":"string","to":"string","format":"original"}}}}}},"/api/v1/dms-trash":{"get":{"responses":{"200":{"description":"Papierkorb-Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"mime_type":{"type":["string","null"]},"size":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"classified_type":{"type":["string","null"]},"pipeline_status":{"type":["string","null"]},"folder_id":{"type":["string","null"]},"deleted_at":{"type":"string"},"days_left":{"type":"number"}},"required":["id","name","mime_type","size","classified_type","pipeline_status","folder_id","deleted_at","days_left"]}},"retentionDays":{"type":"number"}},"required":["data","retentionDays"]},"example":{"data":[{"id":"string","name":"string","mime_type":"string","size":"string","classified_type":"string","pipeline_status":"string","folder_id":"string","deleted_at":"string","days_left":0}],"retentionDays":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Dms-trash","tags":["dms"],"parameters":[],"description":"Papierkorb des Mandanten: weich gelöschte Belege. Gelesen werden alle Zeilen aus `documents` mit gesetztem `deleted_at`, absteigend nach Löschzeitpunkt und hart auf 500 Einträge begrenzt; es gibt weder Filter noch Blättern. `days_left` nennt die verbleibenden Tage, bis der Aufbewahrungsjob (jobs/document-retention.ts) endgültig löscht, `retentionDays` die Frist selbst. Mindestrolle `user`.","summary":"Papierkorb des Mandanten: weich gelöschte Belege","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dms-trash/{id}/restore":{"post":{"responses":{"200":{"description":"Beleg wiederhergestellt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"id":{"type":"string"}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Dms-trashByIdRestore","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Beleg aus dem Papierkorb wiederherstellen. Setzt `deleted_at` auf NULL und `updated_at` auf jetzt und macht damit das weiche Löschen rückgängig. Greift nur bei Belegen, die tatsächlich im Papierkorb liegen; alles andere ergibt 404. Mindestrolle `user` und bewusst nicht höher, weil die Aktion nichts vernichtet.","summary":"Beleg aus dem Papierkorb wiederherstellen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dms-trash/{id}":{"delete":{"responses":{"200":{"description":"Beleg endgültig gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"id":{"type":"string"}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1Dms-trashById","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Beleg endgültig aus dem Papierkorb löschen. Die Zeile verschwindet per SQL-DELETE; das ist die einzige unumkehrbare Aktion dieses Routers, ein Rückgängig gibt es nicht. Belege mit `pipeline_status = approved` sind revisionssicher und werden mit 403 abgelehnt, Belege ausserhalb des Papierkorbs mit 404. Mindestrolle `admin`.","summary":"Beleg endgültig aus dem Papierkorb löschen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dms/{id}/suggest-links":{"get":{"responses":{"200":{"description":"Vier Listen. NUR `customers` ist nach Trefferguete sortiert; bei `invoices` und `orders` steht die Reihenfolge der Datenbank, wer den besten Treffer will, sortiert nach `score` selbst. Leere Listen sind der Normalfall und kein Fehler.","content":{"application/json":{"schema":{"type":"object","properties":{"customers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","description":"Bei Kunden und Lieferanten"},"number":{"type":"string","description":"Bei Rechnungen und Auftraegen"},"score":{"type":"number","description":"0..1 — je hoeher, desto sicherer der Treffer"},"reason":{"type":"string","description":"Deutscher Klartext, worauf der Treffer beruht"},"matched_on":{"type":"string","description":"Nur bei Lieferanten: vat_id | iban | name — in dieser Rangfolge"},"is_one_time":{"type":"boolean","description":"Nur bei Lieferanten: Einmal-Adresse"}},"required":["id","score","reason"]}},"suppliers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","description":"Bei Kunden und Lieferanten"},"number":{"type":"string","description":"Bei Rechnungen und Auftraegen"},"score":{"type":"number","description":"0..1 — je hoeher, desto sicherer der Treffer"},"reason":{"type":"string","description":"Deutscher Klartext, worauf der Treffer beruht"},"matched_on":{"type":"string","description":"Nur bei Lieferanten: vat_id | iban | name — in dieser Rangfolge"},"is_one_time":{"type":"boolean","description":"Nur bei Lieferanten: Einmal-Adresse"}},"required":["id","score","reason"]}},"invoices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","description":"Bei Kunden und Lieferanten"},"number":{"type":"string","description":"Bei Rechnungen und Auftraegen"},"score":{"type":"number","description":"0..1 — je hoeher, desto sicherer der Treffer"},"reason":{"type":"string","description":"Deutscher Klartext, worauf der Treffer beruht"},"matched_on":{"type":"string","description":"Nur bei Lieferanten: vat_id | iban | name — in dieser Rangfolge"},"is_one_time":{"type":"boolean","description":"Nur bei Lieferanten: Einmal-Adresse"}},"required":["id","score","reason"]}},"orders":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","description":"Bei Kunden und Lieferanten"},"number":{"type":"string","description":"Bei Rechnungen und Auftraegen"},"score":{"type":"number","description":"0..1 — je hoeher, desto sicherer der Treffer"},"reason":{"type":"string","description":"Deutscher Klartext, worauf der Treffer beruht"},"matched_on":{"type":"string","description":"Nur bei Lieferanten: vat_id | iban | name — in dieser Rangfolge"},"is_one_time":{"type":"boolean","description":"Nur bei Lieferanten: Einmal-Adresse"}},"required":["id","score","reason"]}}},"required":["customers","suppliers","invoices","orders"]},"example":{"customers":[{"id":"string","name":"string","number":"string","score":0,"reason":"string","matched_on":"string","is_one_time":true}],"suppliers":[{"id":"string","name":"string","number":"string","score":0,"reason":"string","matched_on":"string","is_one_time":true}],"invoices":[{"id":"string","name":"string","number":"string","score":0,"reason":"string","matched_on":"string","is_one_time":true}],"orders":[{"id":"string","name":"string","number":"string","score":0,"reason":"string","matched_on":"string","is_one_time":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1DmsByIdSuggest-links","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Schlaegt vor, womit ein Dokument verknuepft werden koennte — Kunde, Lieferant, Rechnung, Auftrag. Grundlage sind AUSSCHLIESSLICH die Felder, die die Pipeline aus dem Beleg gelesen hat: ohne erkannten Namen oder ohne Belegnummer bleibt die jeweilige Liste leer. Kunden und Belege werden ueber einen unscharfen Namensvergleich gesucht (hoechstens 10 Kandidaten je Art, alles unter 0,3 faellt raus); Lieferanten laufen ueber die schaerfere Rangfolge USt-IdNr., dann IBAN, dann Name — `matched_on` sagt, welche griff. Der Aufruf ist rein lesend: er verknuepft NICHTS, das entscheidet der Anwender. Fehlt die Rechnungs- oder Auftragstabelle im Mandanten, bleibt die betreffende Liste still leer.","summary":"Schlaegt vor, womit ein Dokument verknuepft werden koennte","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dms/{id}/approval/request":{"post":{"responses":{"200":{"description":"Kette angelegt — die Antwort nennt nur ihre Laenge, nicht die Stufen.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"chainSize":{"type":"integer","description":"Zahl der angelegten Stufen"}},"required":["success","chainSize"]},"example":{"success":true,"chainSize":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Der Antragsteller steht selbst in der Kette (`self_approval_forbidden`)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1DmsByIdApprovalRequest","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Legt die Freigabekette eines Dokuments an: eine geordnete Liste von Stufen mit je einem Namen und einem zustaendigen Nutzer. Die Kette ist LINEAR — Stufe N+1 kommt erst nach der Freigabe von Stufe N, eine Ablehnung bricht alles ab. Das Dokument geht auf `pending`, zustaendig ist die erste Stufe. Vier-Augen: wer die Kette startet, darf in ihr nicht als Entscheider stehen — sonst 409. Der Aufruf ist nicht additiv: eine vorhandene Kette wird ERSETZT, samt bereits getroffener Entscheidungen. Ob es das Dokument gibt, wird nicht geprueft; ein unbekanntes ergibt trotzdem 200. Ab Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"chain":{"type":"array","items":{"type":"object","properties":{"stepName":{"type":"string","minLength":1,"maxLength":80},"assignedTo":{"type":"string","format":"uuid"},"threshold":{"type":"number"}},"required":["stepName","assignedTo"]},"minItems":1}},"required":["chain"]},"example":{"chain":[{"stepName":"string","assignedTo":"00000000-0000-4000-8000-000000000000","threshold":0}]}}}},"summary":"Legt die Freigabekette eines Dokuments an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dms/{id}/approval/approve":{"post":{"responses":{"200":{"description":"Entschieden. `newStatus` sagt, wie es weitergeht.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"newStatus":{"type":"string","description":"`pending` = es folgt eine weitere Stufe · `approved` = die Kette ist durch · `rejected` = die Kette ist abgebrochen"},"nextStep":{"type":["object","null"],"properties":{"step":{"type":"integer","description":"0-basierte Position in der Kette"},"stepName":{"type":"string"},"assignedTo":{"type":"string","description":"Der Nutzer, der DIESE Stufe entscheiden darf"},"status":{"type":"string","description":"pending | approved | rejected"},"requestedBy":{"type":"string","description":"Wer die Kette startete; fehlt bei Ketten aus der Zeit vor dieser Sperre"},"threshold":{"type":"number"},"decidedAt":{"type":"string"},"decidedBy":{"type":"string"},"comment":{"type":"string"}},"required":["step","stepName","assignedTo","status"],"description":"Die naechste Stufe, oder null wenn die Kette hier endet"}},"required":["success","newStatus","nextStep"]},"example":{"success":true,"newStatus":"string","nextStep":{"step":0,"stepName":"string","assignedTo":"string","status":"string","requestedBy":"string","threshold":0,"decidedAt":"string","decidedBy":"string","comment":"string"}}}}},"401":{"description":"Keine erkennbare Identitaet im Aufruf (`auth_required`)"},"403":{"description":"Diese Stufe ist einem anderen Nutzer zugewiesen"},"404":{"description":"Dokument nicht gefunden"},"409":{"description":"Keine offene Stufe mehr (`no_pending_step`), oder der Antragsteller wollte selbst freigeben (`self_approval_forbidden`)."},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1DmsByIdApprovalApprove","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Offene Stufe der Freigabekette entscheiden (freigeben oder ablehnen)","description":"Entscheidet die aktuell offene Stufe der Freigabekette mit `approved` oder `rejected`, wahlweise mit Kommentar. Entscheiden darf NUR der Nutzer, dem die Stufe zugewiesen ist (sonst 403), und niemals der Antragsteller der Kette (sonst 409). Ein Aufruf ohne erkennbare Identitaet — etwa mit einem API-Schluessel — wird abgelehnt (401). Bei `approved` rueckt die naechste Stufe nach; gibt es keine, gilt das Dokument als freigegeben und Zeitpunkt und Freigebender werden am Dokument vermerkt. Bei `rejected` endet die Kette sofort, ohne dass die restlichen Stufen entscheiden. Ab Rolle `user`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"decision":{"type":"string","enum":["approved","rejected"]},"comment":{"type":"string","maxLength":2000}},"required":["decision"]},"example":{"decision":"approved","comment":"string"}}}}}},"/api/v1/dms/approval/queue":{"get":{"responses":{"200":{"description":"Die wartenden Dokumente. Leer heisst „nichts zu tun\" — oder kein Nutzer.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"mime_type":{"type":["string","null"]},"classified_type":{"type":["string","null"]},"classified_confidence":{"description":"NUMERIC — hier ROH durchgereicht, also als Zeichenkette"},"extracted_entities":{},"approval_chain":{"type":"array","items":{"type":"object","properties":{"step":{"type":"integer","description":"0-basierte Position in der Kette"},"stepName":{"type":"string"},"assignedTo":{"type":"string","description":"Der Nutzer, der DIESE Stufe entscheiden darf"},"status":{"type":"string","description":"pending | approved | rejected"},"requestedBy":{"type":"string","description":"Wer die Kette startete; fehlt bei Ketten aus der Zeit vor dieser Sperre"},"threshold":{"type":"number"},"decidedAt":{"type":"string"},"decidedBy":{"type":"string"},"comment":{"type":"string"}},"required":["step","stepName","assignedTo","status"]},"description":"Die vollstaendige Kette des Dokuments"},"created_at":{"type":"string"}},"required":["id","name","mime_type","classified_type","approval_chain","created_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","name":"string","mime_type":"string","classified_type":"string","approval_chain":[{"step":0,"stepName":"string","assignedTo":"string","status":"string","requestedBy":"string","threshold":0,"decidedAt":"string","decidedBy":"string","comment":"string"}],"created_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1DmsApprovalQueue","tags":["dms"],"parameters":[],"description":"Listet die Dokumente, bei denen der ANGEMELDETE Nutzer gerade an der Reihe ist — neueste zuerst, hoechstens 100, ohne Blaetterung. Es gibt keinen Parameter fuer einen anderen Nutzer: die Eingrenzung kommt aus dem Anmeldekontext. Aufrufe ohne Nutzerkennung und solche mit einem API-Schluessel bekommen eine LEERE Liste — einem Schluessel laesst sich keine Freigabe zuweisen. Geloeschte Dokumente sind ausgenommen. Jede Zeile traegt die vollstaendige Kette mit, nicht nur die offene Stufe.","summary":"Listet die Dokumente, bei denen der ANGEMELDETE Nutzer gerade an der Reihe ist","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dms/routing-suggestions/accept":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true,"description":"Aktion ausgefuehrt"}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Regel nicht gefunden"}},"operationId":"postApiV1DmsRouting-suggestionsAccept","tags":["dms"],"parameters":[],"summary":"Accept routing suggestion","description":"Routing-Vorschlag annehmen — Regel anlegen (create) oder ändern (update). Bei kind=update ist rule_id Pflicht (sonst 422); die bestehende Regel wird ergaenzt, nicht ersetzt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["create","update"]},"vendor_key":{"type":"string","minLength":1},"vendor":{"type":"string","minLength":1},"classified_type":{"type":["string","null"]},"direction":{"type":["string","null"],"enum":["incoming","outgoing",null]},"folder_id":{"type":"string","format":"uuid"},"to_status":{"type":"string"},"rule_id":{"type":"string","format":"uuid"}},"required":["kind","vendor_key","vendor","folder_id"]},"example":{"kind":"create","vendor_key":"string","vendor":"string","classified_type":"string","direction":"incoming","folder_id":"00000000-0000-4000-8000-000000000000","to_status":"string","rule_id":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/dms/routing-suggestions/dismiss":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true,"description":"Aktion ausgefuehrt"}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1DmsRouting-suggestionsDismiss","tags":["dms"],"parameters":[],"summary":"Dismiss routing suggestion","description":"Routing-Vorschlag ablehnen — für diese Kombination nicht mehr fragen","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"vendor_key":{"type":"string","minLength":1},"classified_type":{"type":["string","null"]},"direction":{"type":["string","null"],"enum":["incoming","outgoing",null]},"folder_id":{"type":"string","format":"uuid"}},"required":["vendor_key","folder_id"]},"example":{"vendor_key":"string","classified_type":"string","direction":"incoming","folder_id":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/dms/stats":{"get":{"responses":{"200":{"description":"Counts","content":{"application/json":{"schema":{"type":"object","properties":{"stats":{"type":"object","additionalProperties":{"type":"number"},"description":"Anzahl je pipeline_status"}},"required":["stats"]},"example":{"stats":{"beispiel":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1DmsStats","tags":["dms"],"parameters":[],"summary":"Count documents per pipeline status","description":"Zaehlt die Dokumente des Mandanten nach `pipeline_status` und liefert das als Zuordnung Status → Anzahl. Geloeschte Dokumente (`deleted_at` gesetzt) sind ausgenommen. Es kommen nur Status vor, zu denen es auch Dokumente gibt — ein Status ohne Zeilen fehlt in `stats`, er steht dort nicht mit 0. Zeilen ohne Status zaehlen unter `unknown`. Ohne Datenbankverbindung 503."}},"/api/v1/dms/storage-summary":{"get":{"responses":{"200":{"description":"Summary","content":{"application/json":{"schema":{"type":"object","properties":{"files":{"type":"integer","description":"Anzahl Dateien"},"bytes":{"type":"integer","description":"Belegter Speicher in Bytes"}},"required":["files","bytes"]},"example":{"files":0,"bytes":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1DmsStorage-summary","tags":["dms"],"parameters":[],"summary":"Get storage summary","description":"Gesamtzahl Dokumente + Gesamtgröße (Bytes) des Mandanten"}},"/api/v1/dms/inbox":{"get":{"responses":{"200":{"description":"Inbox","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Datensaetze"}},"required":["data"]},"example":{"data":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1DmsInbox","tags":["dms"],"parameters":[{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"type","schema":{"type":"string"}}],"summary":"List inbox documents","description":"Belege des Eingangs als FLACHE Liste, optional nach pipeline_status und Belegart gefiltert — das Kanban-Board gruppiert selbst. Nebenwirkung: Belege, die laenger als 3 Minuten in Verarbeitung haengen, werden dabei auf Fehler gesetzt."}},"/api/v1/dms/documents":{"get":{"responses":{"200":{"description":"Belegliste mit Cursor für die nächste Seite","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"mime_type":{"type":["string","null"]},"size":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"null"}]},"type":{"type":["string","null"]},"classified_type":{"type":["string","null"]},"classified_confidence":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"null"}]},"classified_by":{"type":["string","null"]},"pipeline_status":{"type":["string","null"]},"ocr_status":{"type":["string","null"]},"extracted_entities":{},"tags":{},"entity_type":{"type":["string","null"]},"entity_id":{"type":["string","null"]},"routed_to_user":{"type":["string","null"]},"approved_at":{},"created_at":{},"url":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"folder_id":{"type":["string","null"]},"direction":{"type":["string","null"]},"hold_reason":{"type":["string","null"]},"tax_exported_at":{},"folder_name":{"type":["string","null"]},"folder_path":{"type":["string","null"]},"created_at_cursor":{"type":["string","null"]},"category":{"type":"string"},"missing_required_fields":{"type":"array","items":{}},"is_complete":{"type":"boolean"},"vendor_name":{},"customer_name":{},"total_gross":{},"currency":{},"document_date":{},"retention_until":{"type":["string","null"]},"retention_days_left":{"type":["number","null"]},"retention_status":{"type":"string","enum":["unbefristet","abgelaufen","bald","laufend"]}},"required":["id","name","mime_type","size","type","classified_type","classified_confidence","classified_by","pipeline_status","ocr_status","entity_type","entity_id","routed_to_user","url","thumbnail_url","folder_id","direction","hold_reason","folder_name","folder_path","created_at_cursor","category","missing_required_fields","is_complete","retention_until","retention_days_left","retention_status"],"additionalProperties":false}},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","mime_type":"string","size":0,"type":"string","classified_type":"string","classified_confidence":0,"classified_by":"string","pipeline_status":"string","ocr_status":"string","entity_type":"string","entity_id":"string","routed_to_user":"string","url":"string","thumbnail_url":"string","folder_id":"string","direction":"string","hold_reason":"string","folder_name":"string","folder_path":"string","created_at_cursor":"string","category":"string","missing_required_fields":[],"is_complete":true,"retention_until":"string","retention_days_left":0,"retention_status":"unbefristet"}],"nextCursor":"string"}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"getApiV1DmsDocuments","tags":["dms"],"parameters":[{"in":"query","name":"q","schema":{"type":"string"}},{"in":"query","name":"type","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"direction","schema":{"type":"string","enum":["incoming","outgoing"]}},{"in":"query","name":"folderId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"incomplete","schema":{"type":"string","enum":["1"]}},{"in":"query","name":"taxExported","schema":{"type":"string","enum":["0","1"]}},{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"summary":"List all documents across lanes","description":"Gesamtübersicht aller Belege (cross-lane, filter/suche/zeitraum)"}},"/api/v1/dms/{id}/lernstand":{"get":{"responses":{"200":{"description":"Der Lernstand zu diesem Beleg. `ocrEnthaeltWerte` steht nur im Fall MIT Schluessel, `note` nur im Fall ohne.","content":{"application/json":{"schema":{"type":"object","properties":{"vendorKey":{"type":["string","null"],"description":"`vat:<USt-IdNr>` oder `name:<Lieferantenname>`; null, wenn beides fehlt"},"belegart":{"type":["string","null"],"description":"Klassifizierte Belegart; null, wenn keine gesetzt ist"},"hints":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","description":"Feldschluessel, zu dem gelernt wurde"},"aktuellerWert":{"type":["string","null"],"description":"Der jetzt extrahierte Wert"},"wieZuletztGelernt":{"type":"boolean","description":"Stimmt der jetzige Wert mit dem zuletzt gelernten ueberein?"},"korrekturen":{"type":"integer","description":"Wie oft dieses Feld bereits korrigiert wurde"},"beispiele":{"type":"array","items":{"type":"object","properties":{"wert":{"type":"string"},"umfeld":{"type":["string","null"],"description":"Textumfeld der Fundstelle"}},"required":["wert","umfeld"]}},"muster":{"type":["string","null"],"description":"Abgeleitetes Muster, sobald es stabil ist"},"position":{"type":["object","null"],"properties":{"page":{"type":"number"},"x":{"type":"number"},"y":{"type":"number"},"w":{"type":"number"},"h":{"type":"number"}},"required":["page","x","y","w","h"],"description":"Gelernte Stelle auf der Seite, in Anteilen 0…1"},"positionBestaetigt":{"type":"integer","description":"Wie oft die Stelle bestaetigt wurde"}},"required":["field","aktuellerWert","wieZuletztGelernt","korrekturen","beispiele","muster","position","positionBestaetigt"]}},"note":{"type":"string","description":"Nur ohne Schluessel gesetzt: der Grund, warum nichts nachgeschlagen wurde"},"ocrEnthaeltWerte":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Nur MIT Schluessel: je Feld, ob der Wert so im erkannten Text steht"}},"required":["vendorKey","belegart","hints"]},"example":{"vendorKey":"string","belegart":"string","hints":[{"field":"string","aktuellerWert":"string","wieZuletztGelernt":true,"korrekturen":0,"beispiele":[{"wert":"string","umfeld":"string"}],"muster":"string","position":{"page":0,"x":0,"y":0,"w":0,"h":0},"positionBestaetigt":0}],"note":"string","ocrEnthaeltWerte":{"beispiel":true}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"getApiV1DmsByIdLernstand","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get learned field hints for a document","description":"Gelernte Feld-Hinweise für den Lieferanten dieses Belegs (nur lesend). Ohne erkannten Lieferanten oder ohne Belegart gibt es keinen Schluessel: dann kommt 200 mit leerer hints-Liste und einem note-Feld, das den Grund nennt."}},"/api/v1/dms/{id}/verlauf":{"get":{"responses":{"200":{"description":"Die Verlaufseintraege, aelteste zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Eintraege"}},"required":["data"]},"example":{"data":[{}]}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"getApiV1DmsByIdVerlauf","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List document history","description":"Chronologischer Verlauf eines Belegs (Zeitstempel + Best-Effort-Audit). Der Audit-Teil ist wirklich Best-Effort: fehlt die audit_log-Tabelle des Mandanten, bleiben diese Eintraege leer, statt dass die Abfrage scheitert. Ein unbekannter Beleg antwortet 404 (im responses-Block unten nicht gelistet)."}},"/api/v1/dms/{id}":{"get":{"responses":{"200":{"description":"Detail","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Datensaetze"}},"required":["data"]},"example":{"data":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"getApiV1DmsById","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get document detail","description":"Dokument-Detail mit OCR-Text und extrahierten Entities. Nebenwirkung: haengt der Beleg laenger als 3 Minuten in Verarbeitung, wird er beim Abruf auf Fehler gesetzt (sonst dreht sich die Anzeige endlos)."},"delete":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true,"description":"Aktion ausgefuehrt"}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"deleteApiV1DmsById","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Move document to trash","description":"Dokument in den Papierkorb verschieben (Soft-Delete). Die Zeile und die Datei bleiben erhalten, gesetzt wird nur deleted_at — endgueltiges Loeschen liegt bei DELETE /api/v1/documents/:id/permanent."}},"/api/v1/dms/{id}/duplicates":{"get":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Ergebnis der Pruefung"},"matches":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Aehnliche Belege"}},"required":["status","matches"]},"example":{"status":"string","matches":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"getApiV1DmsByIdDuplicates","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Check document for duplicates","description":"Duplikat-Prüfung mit Ampel (findet identische/ähnliche Dokumente). status=red: identische Datei oder gleiche Rechnungsnummer plus Aussteller. status=yellow: gleicher Aussteller, Betrag und Datum. status=green: nichts gefunden."}},"/api/v1/dms/{id}/classify":{"patch":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"reextracting":{"type":"boolean","description":"Laeuft eine erneute Extraktion?"}},"required":["success","reextracting"]},"example":{"success":true,"reextracting":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"patchApiV1DmsByIdClassify","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Set document type manually","description":"Manuelle Klassifikation setzen / überschreiben. Die Sicherheit wird dabei bewusst auf NULL gesetzt — eine Anwenderentscheidung ist keine Aussage ueber die Modell-Sicherheit. Liegt OCR-Text vor, laeuft eine Neu-Erkennung der Felder NACH der Antwort (reextracting=true); das Ergebnis ist erst beim erneuten Abruf da. Archivierte Belege: 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"classified_type":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","minLength":1,"maxLength":60}},"required":["classified_type"]}}}}}},"/api/v1/dms/{id}/direction":{"patch":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"reextracting":{"type":"boolean","description":"Laeuft eine erneute Extraktion?"}},"required":["success","reextracting"]},"example":{"success":true,"reextracting":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"patchApiV1DmsByIdDirection","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Set document direction","description":"Bezug/Richtung (Eingang/Ausgang) setzen + Felder neu erkennen. Die Neu-Erkennung laeuft NACH der Antwort (reextracting=true). Eine bestehende Kontakt-Verknuepfung bleibt erhalten. Archivierte Belege: 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"direction":{"type":"string","enum":["incoming","outgoing"]}},"required":["direction"]},"example":{"direction":"incoming"}}}}}},"/api/v1/dms/{id}/entities":{"patch":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true,"description":"Aktion ausgefuehrt"}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"patchApiV1DmsByIdEntities","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Correct extracted fields","description":"Extrahierte Entities manuell korrigieren. Die Antwort traegt zusaetzlich den neu berechneten Vollstaendigkeits-Stand (is_complete, missing_required_fields). Jede Aenderung wird als Hinweis fuer denselben Lieferanten gelernt. Archivierte Belege: 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}}}},"/api/v1/dms/{id}/field-zone":{"post":{"responses":{"200":{"description":"`learned` sagt, ob die Stelle uebernommen wurde. Bei false steht der Grund in `reason` — ohne dieses Feld waere ein „nicht gelernt\" nicht von einem Fehler zu unterscheiden.","content":{"application/json":{"schema":{"type":"object","properties":{"learned":{"type":"boolean","description":"Wurde die Zone uebernommen?"},"reason":{"type":"string","description":"Grund, falls nicht gelernt"}},"required":["learned"]},"example":{"learned":true,"reason":"string"}}}},"400":{"description":"Ungültige Eingabe"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Beleg nicht gefunden"}},"operationId":"postApiV1DmsByIdField-zone","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Learn field position from a marked area","description":"Markierte Stelle als gelernte Feldposition merken. ACHTUNG: ohne erkannten Lieferanten oder ohne Belegart wird NICHTS gelernt — die Antwort ist trotzdem 200, dann mit learned=false und dem Grund in reason. Ein unbrauchbares Rechteck beantwortet der Handler mit 422 (nicht mit dem unten gelisteten 400).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"fieldKey":{"type":"string","minLength":1,"maxLength":80},"zone":{"type":"object","properties":{"page":{"type":"integer","minimum":1},"x":{"type":"number"},"y":{"type":"number"},"w":{"type":"number"},"h":{"type":"number"}},"required":["page","x","y","w","h"]}},"required":["fieldKey","zone"]},"example":{"fieldKey":"string","zone":{"page":1,"x":0,"y":0,"w":0,"h":0}}}}}}},"/api/v1/dms/{id}/rename":{"patch":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"name":{"type":"string","description":"Der neue Name"}},"required":["success","name"]},"example":{"success":true,"name":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Archiviert"}},"operationId":"patchApiV1DmsByIdRename","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rename document","description":"Dokumentnamen ändern. Archivierte Belege: 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255}},"required":["name"]},"example":{"name":"string"}}}}}},"/api/v1/dms/{id}/approve":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true,"description":"Aktion ausgefuehrt"}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1DmsByIdApprove","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Approve and archive document","description":"Dokument freigeben (Approval setzen). Nur Manager und hoeher. Sperrt den Beleg gegen weitere Bearbeitung und legt ihn, wenn er steuerrelevant ist, unveraenderbar (WORM) ab. Fehlende Pflichtfelder verhindern die Freigabe NICHT — sie werden im Verlauf protokolliert."}},"/api/v1/dms/{id}/route":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true,"description":"Aktion ausgefuehrt"}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1DmsByIdRoute","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Link document to a record","description":"Dokument einer Entity (Kunde/Auftrag/Projekt) zuweisen. Archivierte Belege: 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity_type":{"type":"string","enum":["customer","order","invoice","quote","contract","project","supplier"]},"entity_id":{"type":"string","format":"uuid"},"routed_to_user":{"type":"string","format":"uuid"}},"required":["entity_type","entity_id"]},"example":{"entity_type":"customer","entity_id":"00000000-0000-4000-8000-000000000000","routed_to_user":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/dms/{id}/supplier-suggestion":{"get":{"responses":{"200":{"description":"Vorschlag","content":{"application/json":{"schema":{"type":"object","properties":{"hasVendor":{"type":"boolean","description":"Ist bereits ein Lieferant zugeordnet?"},"vendor":{"type":["object","null"],"additionalProperties":{},"description":"Der zugeordnete Lieferant"},"best":{"type":["object","null"],"additionalProperties":{},"description":"Bester Vorschlag"},"matches":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Alle Vorschlaege"}},"required":["hasVendor","vendor","best","matches"]},"example":{"hasVendor":true,"vendor":{},"best":{},"matches":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"getApiV1DmsByIdSupplier-suggestion","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Suggest matching supplier","description":"Lieferanten-Vorschlag zum Beleg (Matching aus extrahierten Daten)"}},"/api/v1/dms/{id}/link-supplier":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"contact":{"type":"object","additionalProperties":{},"description":"Verknuepfter Kontakt"},"oneTime":{"type":"boolean","description":"true = Einmal-Adresse statt Stammkontakt"}},"required":["success"]},"example":{"success":true,"contact":{},"oneTime":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdLink-supplier","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Link document to a supplier contact","description":"Beleg mit Kontakt (Typ Lieferant) verknüpfen — oder als Einmallieferant markieren","requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"mode":{"type":"string","const":"existing"},"customer_id":{"type":"string","format":"uuid"}},"required":["mode","customer_id"]},{"type":"object","properties":{"mode":{"type":"string","const":"one_time"}},"required":["mode"]}]},"example":{"mode":"existing","customer_id":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/dms/{id}/customer-suggestion":{"get":{"responses":{"200":{"description":"Vorschlag","content":{"application/json":{"schema":{"type":"object","properties":{"hasCustomer":{"type":"boolean","description":"Ist bereits ein Kunde zugeordnet?"},"customer":{"type":["object","null"],"additionalProperties":{},"description":"Der zugeordnete Kunde"},"best":{"type":["object","null"],"additionalProperties":{},"description":"Bester Vorschlag"},"matches":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Alle Vorschlaege"}},"required":["hasCustomer","customer","best","matches"]},"example":{"hasCustomer":true,"customer":{},"best":{},"matches":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"getApiV1DmsByIdCustomer-suggestion","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Suggest matching customer","description":"Kunden-Vorschlag zum Beleg (Matching aus extrahierten Daten)"}},"/api/v1/dms/{id}/link-customer":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"contact":{"type":"object","additionalProperties":{},"description":"Verknuepfter Kontakt"},"oneTime":{"type":"boolean","description":"true = Einmal-Adresse statt Stammkontakt"}},"required":["success"]},"example":{"success":true,"contact":{},"oneTime":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdLink-customer","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Link document to a customer contact","description":"Beleg mit Kontakt (Typ Kunde) verknüpfen — oder als Einmalkunde markieren","requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"mode":{"type":"string","const":"existing"},"customer_id":{"type":"string","format":"uuid"}},"required":["mode","customer_id"]},{"type":"object","properties":{"mode":{"type":"string","const":"one_time"}},"required":["mode"]}]},"example":{"mode":"existing","customer_id":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/dms/{id}/party-suggestion":{"get":{"responses":{"200":{"description":"Vorschlag","content":{"application/json":{"schema":{"type":"object","properties":{"hasVendor":{"type":"boolean","description":"Ist bereits ein Lieferant zugeordnet?"},"vendor":{"type":["object","null"],"additionalProperties":{},"description":"Der zugeordnete Lieferant"},"best":{"type":["object","null"],"additionalProperties":{},"description":"Bester Vorschlag"},"matches":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Alle Vorschlaege"}},"required":["hasVendor","vendor","best","matches"]},"example":{"hasVendor":true,"vendor":{},"best":{},"matches":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"getApiV1DmsByIdParty-suggestion","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Suggest matching contact","description":"Kontakt-Vorschlag zum Beleg (alle Kontakte, richtungsunabhängig)"}},"/api/v1/dms/{id}/assign":{"patch":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true,"description":"Aktion ausgefuehrt"}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Archiviert"}},"operationId":"patchApiV1DmsByIdAssign","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Assign document to employee or department","description":"Beleg einer Person (Mitarbeiter) und/oder Abteilung zuweisen. Archivierte Belege: 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"assigned_employee":{"type":["string","null"],"format":"uuid"},"assigned_department":{"type":["string","null"],"maxLength":100}}},"example":{"assigned_employee":"00000000-0000-4000-8000-000000000000","assigned_department":"string"}}}}}},"/api/v1/dms/{id}/rerun":{"post":{"responses":{"202":{"description":"Der Lauf wurde angestossen — das Ergebnis kommt spaeter","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Status des angestossenen Laufs"}},"required":["status"]},"example":{"status":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Beleg nicht gefunden"}},"operationId":"postApiV1DmsByIdRerun","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Restart document pipeline","description":"Pipeline neu starten (OCR + Classify). Antwortet 202: der Lauf ist nur ANGESTOSSEN, nicht fertig — das Ergebnis kommt erst beim erneuten Abruf des Belegs. Ohne hinterlegte Datei (storage_key) kommt 400."}},"/api/v1/dms/rerun-needs-review":{"post":{"responses":{"200":{"description":"Kein passender Beleg gefunden — `started` ist 0, `message` nennt das.","content":{"application/json":{"schema":{"type":"object","properties":{"started":{"type":"integer","description":"Zahl der angestossenen Laeufe"},"message":{"type":"string","description":"Nur bei started = 0 gesetzt"}},"required":["started"]},"example":{"started":0,"message":"string"}}}},"202":{"description":"Reruns gestartet. `started` zaehlt die angestossenen Belege; die Laeufe selbst laufen im Hintergrund weiter.","content":{"application/json":{"schema":{"type":"object","properties":{"started":{"type":"integer","description":"Zahl der angestossenen Laeufe"},"message":{"type":"string","description":"Nur bei started = 0 gesetzt"}},"required":["started"]},"example":{"started":0,"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1DmsRerun-needs-review","tags":["dms"],"parameters":[],"summary":"Restart pipeline for unclear documents","description":"Bulk-Rerun für unklare/Sonstiges-Klassifikationen. Nimmt hoechstens 200 Belege und verarbeitet drei gleichzeitig. ACHTUNG bei den Statuscodes: nur wenn wirklich Laeufe starten, kommt 202 mit started=N — findet sich kein passender Beleg, antwortet der Handler 200 mit started=0."}},"/api/v1/dms/{id}/pdf/rotate":{"post":{"responses":{"200":{"description":"Gedreht und gespeichert; `pages` ist die Seitenzahl der neuen Fassung.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"pages":{"type":"integer","description":"Seiten im Ergebnis"}},"required":["success"]},"example":{"success":true,"pages":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfRotate","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rotate PDF pages","description":"PDF-Seite(n) drehen und speichern. Wie alle PDF-Bearbeitungen: das Original bleibt versioniert erhalten (Ruecknahme ueber /pdf/reset). Nicht-PDF oder Beleg ohne Datei: 400, Ablage nicht erreichbar: 502. Archivierte Belege: 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"string","enum":["page","all"],"default":"page"},"page":{"type":"integer","exclusiveMinimum":0},"direction":{"type":"string","enum":["cw","ccw"],"default":"cw"}}},"example":{"scope":"page","page":1,"direction":"cw"}}}}}},"/api/v1/dms/{id}/pdf/crop":{"post":{"responses":{"200":{"description":"Zugeschnitten und gespeichert; `pages` ist die Seitenzahl der neuen Fassung.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"pages":{"type":"integer","description":"Seiten im Ergebnis"}},"required":["success"]},"example":{"success":true,"pages":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfCrop","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Crop a PDF page","description":"Eine PDF-Seite auf das markierte Rechteck zuschneiden (PDF-Punkte)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"integer","exclusiveMinimum":0},"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number","exclusiveMinimum":0},"height":{"type":"number","exclusiveMinimum":0}},"required":["page","x","y","width","height"]},"example":{"page":1,"x":0,"y":0,"width":1,"height":1}}}}}},"/api/v1/dms/{id}/pdf/reorder":{"post":{"responses":{"200":{"description":"Umsortiert und gespeichert; `pages` ist die Seitenzahl der neuen Fassung.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"pages":{"type":"integer","description":"Seiten im Ergebnis"}},"required":["success"]},"example":{"success":true,"pages":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfReorder","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Reorder PDF pages","description":"Seiten in eine neue Reihenfolge bringen (Drag-and-Drop in der Thumbnail-Leiste). order muss eine vollstaendige Permutation der Seiten sein — doppelte Seitenzahlen ergeben 400.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"array","items":{"type":"integer","exclusiveMinimum":0},"minItems":2}},"required":["order"]},"example":{"order":[1,1]}}}}}},"/api/v1/dms/{id}/pdf/swap":{"post":{"responses":{"200":{"description":"Getauscht und gespeichert; `pages` ist die Seitenzahl der neuen Fassung.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"pages":{"type":"integer","description":"Seiten im Ergebnis"}},"required":["success"]},"example":{"success":true,"pages":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfSwap","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Swap two PDF pages","description":"Zwei PDF-Seiten tauschen","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"a":{"type":"integer","exclusiveMinimum":0},"b":{"type":"integer","exclusiveMinimum":0}},"required":["a","b"]},"example":{"a":1,"b":1}}}}}},"/api/v1/dms/{id}/pdf/delete-page":{"post":{"responses":{"200":{"description":"Entfernt und gespeichert; `pages` ist die Seitenzahl der neuen Fassung.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"pages":{"type":"integer","description":"Seiten im Ergebnis"}},"required":["success"]},"example":{"success":true,"pages":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfDelete-page","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Remove a PDF page","description":"Eine PDF-Seite löschen. Entfernt wird sie nur aus der bearbeiteten Fassung — das Original bleibt gesichert und ist ueber /pdf/reset wiederherstellbar.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"integer","exclusiveMinimum":0}},"required":["page"]},"example":{"page":1}}}}}},"/api/v1/dms/{id}/ocr-page":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"integer","description":"Seitenzahl"},"text":{"type":"string","description":"Erkannter Text dieser Seite"}},"required":["page","text"]},"example":{"page":0,"text":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdOcr-page","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Read text from one PDF page","description":"OCR-Text einer einzelnen PDF-Seite extrahieren (nur Anzeige) — der gespeicherte Gesamttext des Belegs wird NICHT ueberschrieben. Die Route haengt an der optionalen Abhaengigkeit pdf-parse: fehlt sie im Container, antwortet sie 503 pdf_parse_unavailable und die Funktion steht nicht zur Verfuegung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"integer","exclusiveMinimum":0}},"required":["page"]},"example":{"page":1}}}}}},"/api/v1/dms/ocr-region":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","description":"Erkannter Text"}},"required":["text"]},"example":{"text":"string"}}}},"400":{"description":"Bad image"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1DmsOcr-region","tags":["dms"],"parameters":[],"summary":"Read text from a marked area","description":"OCR auf einen markierten Bild-Ausschnitt (Rechteck-Auswahl im PDF). Laeuft ueber das Vision-Modell, ohne Datenbankzugriff. Jeder Fehler des Modells — auch ein fehlender KI-Zugang — kommt als 400 ocr_region_failed zurueck, nicht als 5xx.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"image_data_url":{"type":"string","minLength":16}},"required":["image_data_url"]},"example":{"image_data_url":"stringxxxxxxxxxx"}}}}}},"/api/v1/dms/{id}/pdf/annotate":{"post":{"responses":{"200":{"description":"Aufgebracht und gespeichert; `pages` ist die Seitenzahl der neuen Fassung.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"pages":{"type":"integer","description":"Seiten im Ergebnis"}},"required":["success"]},"example":{"success":true,"pages":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfAnnotate","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Flatten annotations onto a PDF page","description":"Markierungen/Anmerkungen (geflachtes Overlay-PNG) auf eine Seite legen. Das Overlay darf hoechstens 4 MB gross sein.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"integer","exclusiveMinimum":0},"image_data_url":{"type":"string","minLength":20}},"required":["page","image_data_url"]},"example":{"page":1,"image_data_url":"stringxxxxxxxxxxxxxx"}}}}}},"/api/v1/dms/{id}/pdf/merge":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"pages":{"type":"integer","description":"Seiten im Ergebnis"}},"required":["success","pages"]},"example":{"success":true,"pages":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfMerge","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Append another PDF","description":"Ein anderes PDF-Dokument an dieses anhängen (Original bleibt via Versionierung)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"source_document_id":{"type":"string","format":"uuid"}},"required":["source_document_id"]},"example":{"source_document_id":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/dms/{id}/pdf/split":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"parts":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die entstandenen Teile"}},"required":["success","parts"]},"example":{"success":true,"parts":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfSplit","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Split PDF into two documents","description":"PDF an gewählter Seite trennen → genau 2 Belege _1/_2 (Original gesichert, via Reset zusammenführbar). Teil 1 bleibt der aufgerufene Beleg, Teil 2 wird neu angelegt; fuer beide startet die Pipeline erneut.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"afterPage":{"type":"integer","exclusiveMinimum":0}},"required":["afterPage"]},"example":{"afterPage":1}}}}}},"/api/v1/dms/{id}/pdf/reset":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"noop":{"type":"boolean","description":"true = es war nichts zurueckzusetzen"},"restoredId":{"type":"string","description":"Id des wiederhergestellten Stands"}},"required":["success"]},"example":{"success":true,"noop":true,"restoredId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfReset","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Discard PDF edits","description":"Alle PDF-Änderungen verwerfen / Trennung rückgängig (Split-Teile _1+_2 wieder zu einem Original). Gab es nichts zurueckzusetzen, kommt 200 mit noop=true — also ein Erfolg, bei dem nichts passiert ist."}},"/api/v1/dms/{id}/original":{"get":{"responses":{"200":{"description":"Die Datei als Anhang (`Content-Disposition: attachment`). Der gesendete Content-Type ist der `mime_type` des Belegs — `application/pdf`, wenn der Beleg keinen traegt; `application/octet-stream` steht hier stellvertretend fuer die Bytes.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"getApiV1DmsByIdOriginal","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Download the original file","description":"Unverändertes Original des Belegs herunterladen. Liefert Datei-Bytes als Anhang, kein JSON. Ohne Bearbeitung ist das die aktuelle Datei."}},"/api/v1/dms/{id}/pdf/sign":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"signature_id":{"type":"string","description":"Id der Signatur"},"doc_sha256":{"type":"string","description":"Pruefsumme des signierten Dokuments"},"pages":{"type":"integer","description":"Signierte Seiten"}},"required":["success","signature_id","doc_sha256","pages"]},"example":{"success":true,"signature_id":"string","doc_sha256":"string","pages":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdPdfSign","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Add signature or stamp to PDF","description":"Signatur oder Stempel auf die PDF einbetten (SES). Legt einen Nachweis ab (Unterzeichner, Zeitpunkt, IP, SHA-256 der signierten Bytes). Bilder ueber 1 MB: 413. /pdf/reset entfernt Signatur UND Nachweisliste wieder.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"integer","exclusiveMinimum":0},"x":{"type":"number"},"y":{"type":"number"},"w":{"type":"number","exclusiveMinimum":0},"h":{"type":"number","exclusiveMinimum":0},"kind":{"type":"string","enum":["draw","stamp_image","stamp_text"]},"payload":{"type":"object","properties":{"image_data_url":{"type":"string"},"text":{"type":"string","maxLength":80},"color":{"type":"string"},"withDate":{"type":"boolean"},"label":{"type":"string","maxLength":120}},"default":{}}},"required":["page","x","y","w","h","kind"]},"example":{"page":1,"x":0,"y":0,"w":1,"h":1,"kind":"draw","payload":{"image_data_url":"string","text":"string","color":"string","withDate":true,"label":"string"}}}}}}},"/api/v1/dms/{id}/lane":{"patch":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"ruleCreated":{"type":"boolean","description":"Wurde eine Routing-Regel angelegt?"},"routing_suggestion":{"type":["object","null"],"additionalProperties":{},"description":"Vorschlag fuer die weitere Zuordnung"}},"required":["success","ruleCreated","routing_suggestion"]},"example":{"success":true,"ruleCreated":true,"routing_suggestion":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not found"}},"operationId":"patchApiV1DmsByIdLane","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Move document to another lane","description":"Beleg in andere Lane verschieben, optional in einen Ordner + Regel lernen. Aus dem Archiv fuehrt dieser Weg NICHT heraus (403) — dafuer gibt es POST /:id/unarchive. Passt der Ordner nicht zur Ziel-Lane: 422.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to_status":{"type":"string","enum":["inbox","processing","classified","done","routed","review","approved","error"]},"folder_id":{"type":["string","null"],"format":"uuid"},"remember_for_vendor":{"type":"boolean"}},"required":["to_status"]},"example":{"to_status":"inbox","folder_id":"00000000-0000-4000-8000-000000000000","remember_for_vendor":true}}}}}},"/api/v1/dms/{id}/unarchive":{"post":{"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true,"description":"Aktion ausgefuehrt"}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}},"operationId":"postApiV1DmsByIdUnarchive","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Unarchive document","description":"Archivierten Beleg zurück nach „Bereit\" holen (entsperrt Bearbeitung). Nur Admin. Ist der Beleg gar nicht archiviert, kommt 404 — derselbe Code wie bei unbekannter Id."}},"/api/v1/signatures/library":{"get":{"responses":{"200":{"description":"Signaturen des angemeldeten Benutzers","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["draw","stamp_image"]},"image_data_url":{"type":"string","description":"Vollständige Data-URL, z. B. `data:image/png;base64,…`"},"is_default":{"type":"boolean"},"created_at":{}},"required":["id","name","type","image_data_url","is_default"]}}},"required":["data"]},"example":{"data":[{"id":"string","name":"string","type":"draw","image_data_url":"string","is_default":true}]}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext"},"503":{"description":"Datenbank nicht verfügbar oder Abfrage fehlgeschlagen"}},"operationId":"getApiV1SignaturesLibrary","tags":["signatures"],"parameters":[],"summary":"Eigene Signaturen-Bibliothek auflisten","description":"Liefert ausschließlich die Signaturen des angemeldeten Benutzers aus der mandanteneigenen Tabelle `user_signatures`; soft-gelöschte Einträge (`deleted_at`) bleiben außen vor. Sortiert wird der als Standard markierte Eintrag zuerst, danach nach Anlagedatum absteigend. Blätterung gibt es nicht, weil das Anlegen auf 10 Signaturen je Benutzer begrenzt ist; jeder Eintrag trägt das vollständige Bild als Data-URL."},"post":{"responses":{"201":{"description":"Signatur gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["draw","stamp_image"]},"image_data_url":{"type":"string","description":"Vollständige Data-URL, z. B. `data:image/png;base64,…`"},"is_default":{"type":"boolean"},"created_at":{}},"required":["id","name","type","image_data_url","is_default"]},"example":{"id":"string","name":"string","type":"draw","image_data_url":"string","is_default":true}}}},"400":{"description":"Keine gültige Base64-Data-URL vom Typ PNG oder JPEG","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_image_data_url"}},"required":["error"]}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext"},"409":{"description":"Obergrenze von 10 Signaturen je Benutzer erreicht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"limit_reached"},"max":{"type":"integer"}},"required":["error","max"]}}}},"413":{"description":"Data-URL länger als erlaubt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"image_too_large"},"maxBytes":{"type":"integer"}},"required":["error","maxBytes"]}}}},"503":{"description":"Datenbank nicht verfügbar oder Speichern fehlgeschlagen"}},"operationId":"postApiV1SignaturesLibrary","tags":["signatures"],"parameters":[],"summary":"Neue Signatur / Stempel in die Bibliothek speichern","description":"Speichert ein gezeichnetes Signaturbild (`draw`) oder einen Bildstempel (`stamp_image`) als Data-URL beim angemeldeten Benutzer. Angenommen werden nur PNG und JPEG in Base64 und höchstens 286.720 Zeichen Länge, was rund 200 KB Bilddaten entspricht; hat der Benutzer bereits 10 nicht gelöschte Signaturen, wird abgelehnt. Mit `is_default: true` verliert die bisherige Standard-Signatur des Benutzers vorab ihre Markierung, und die Antwort ist der angelegte Datensatz selbst, nicht in ein `data`-Feld verpackt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"type":{"type":"string","enum":["draw","stamp_image"]},"image_data_url":{"type":"string","minLength":20},"is_default":{"type":"boolean"}},"required":["name","type","image_data_url"]},"example":{"name":"string","type":"draw","image_data_url":"stringxxxxxxxxxxxxxx","is_default":true}}}}}},"/api/v1/signatures/library/{id}":{"patch":{"responses":{"200":{"description":"Signatur nach der Änderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["draw","stamp_image"]},"image_data_url":{"type":"string","description":"Vollständige Data-URL, z. B. `data:image/png;base64,…`"},"is_default":{"type":"boolean"},"created_at":{}},"required":["id","name","type","image_data_url","is_default"]},"example":{"id":"string","name":"string","type":"draw","image_data_url":"string","is_default":true}}}},"400":{"description":"Rumpf entspricht nicht dem Schema"},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext"},"404":{"description":"Keine eigene, nicht gelöschte Signatur mit dieser ID"},"503":{"description":"Datenbank nicht verfügbar oder Aktualisierung fehlgeschlagen"}},"operationId":"patchApiV1SignaturesLibraryById","tags":["signatures"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Signatur umbenennen oder als Standard markieren","description":"Ändert `name` und/oder `is_default` einer eigenen, nicht gelöschten Signatur; mindestens eines der beiden Felder muss im Rumpf stehen. `is_default: true` nimmt zuerst der bisherigen Standard-Signatur des Benutzers die Markierung — beides läuft in EINER Transaktion: trifft die ID keine Signatur, kommt 404 und der alte Standard bleibt unangetastet. (Bis 01.09.2026 war er in diesem Fall bereits zurückgesetzt, der Benutzer hatte danach gar keine Standard-Signatur mehr.) Signaturen anderer Benutzer sind über die ID nicht erreichbar und liefern ebenfalls 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"is_default":{"type":"boolean"}}},"example":{"name":"string","is_default":true}}}}},"delete":{"responses":{"200":{"description":"Signatur entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]},"example":{"success":true}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext"},"404":{"description":"Keine eigene, nicht gelöschte Signatur mit dieser ID"},"503":{"description":"Datenbank nicht verfügbar oder Löschen fehlgeschlagen"}},"operationId":"deleteApiV1SignaturesLibraryById","tags":["signatures"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Signatur aus der Bibliothek entfernen (Soft-Delete)","description":"Setzt `deleted_at` auf die aktuelle Zeit; Datensatz und Bilddaten bleiben in der Tabelle stehen und tauchen nur nicht mehr in der Bibliothek auf. Ein zweiter Aufruf auf dieselbe ID sowie eine fremde oder unbekannte ID liefern 404. War die Signatur der Standard, wird die Markierung nicht auf eine andere übertragen."}},"/api/v1/documents/{id}/parse-zugferd":{"post":{"responses":{"200":{"description":"Geparst. ACHTUNG: `found: false` kommt ebenfalls mit 200 — der Statuscode sagt nur, dass gelesen wurde, nicht dass etwas gefunden wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"found":{"type":"boolean","description":"Wurde ein eingebetteter Datensatz gefunden? false ist ein gueltiges Ergebnis, kein Fehler"},"format":{"type":"string","enum":["zugferd","xrechnung","unknown"],"description":"Erkanntes Format; fehlt, wenn nichts gefunden wurde"},"profile":{"type":"string","description":"ZUGFeRD-Profil, etwa MINIMUM, BASIC oder EN 16931; als Zeichenkette, damit neue Profile nicht brechen"},"invoice":{"type":"object","properties":{"invoiceNumber":{"type":"string","description":"Rechnungsnummer aus dem Beleg (BT-1)"},"issueDate":{"type":"string","description":"Rechnungsdatum (BT-2)"},"dueDate":{"type":"string","description":"Faelligkeit (BT-9)"},"seller":{"type":"object","properties":{"name":{"type":"string","description":"Name der Partei"},"vatId":{"type":"string","description":"USt-IdNr. (BT-31 bzw. BT-48)"},"address":{"type":"string","description":"Anschrift als eine Zeile"},"email":{"type":"string","description":"E-Mail-Adresse"},"iban":{"type":"string","description":"IBAN der Partei"}},"required":["name"],"additionalProperties":true,"description":"Der Rechnungssteller"},"buyer":{"type":"object","properties":{"name":{"type":"string","description":"Name der Partei"},"vatId":{"type":"string","description":"USt-IdNr. (BT-31 bzw. BT-48)"},"address":{"type":"string","description":"Anschrift als eine Zeile"},"email":{"type":"string","description":"E-Mail-Adresse"},"iban":{"type":"string","description":"IBAN der Partei"}},"required":["name"],"additionalProperties":true,"description":"Der Rechnungsempfaenger"},"currency":{"type":"string","description":"Waehrungscode (ISO-4217)"},"totalNet":{"type":"number","description":"Nettosumme"},"totalGross":{"type":"number","description":"Bruttosumme"},"totalTax":{"type":"number","description":"Steuerbetrag"},"items":{"type":"array","items":{},"description":"Die Rechnungszeilen"}},"required":["invoiceNumber","issueDate","seller","buyer","currency","totalNet","totalGross","totalTax","items"],"additionalProperties":true,"description":"Die gelesenen Rechnungsdaten; fehlt, wenn nichts gefunden wurde"},"errors":{"type":"array","items":{"type":"string"},"description":"Meldungen des Parsers; auch bei found=true moeglich"},"supplierId":{"type":["string","null"],"description":"Zugeordneter Lieferant; null, wenn keiner passte oder nichts gefunden wurde"},"supplierMatchConfidence":{"type":["number","null"],"description":"Guete der Zuordnung: 1.0 ueber die USt-IdNr., 0.6 ueber eine Namensaehnlichkeit; null ohne Treffer"}},"required":["found","supplierId","supplierMatchConfidence"],"additionalProperties":true},"example":{"found":true,"format":"zugferd","profile":"string","invoice":{"invoiceNumber":"string","issueDate":"string","dueDate":"string","seller":{"name":"string","vatId":"string","address":"string","email":"string","iban":"string"},"buyer":{"name":"string","vatId":"string","address":"string","email":"string","iban":"string"},"currency":"string","totalNet":0,"totalGross":0,"totalTax":0,"items":[]},"errors":["string"],"supplierId":"string","supplierMatchConfidence":0}}}},"400":{"description":"Keine Dokument-Id im Pfad","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"missing_id","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Unauthorized"},"404":{"description":"Dokument nicht gefunden oder gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Die Datei ließ sich nicht aus der Ablage laden — die Meldung des Ablagesystems steht im Rumpf","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"storage_download_failed","description":"Fester Fehlerschluessel"},"details":{"type":"string","description":"Meldung des Ablagesystems im Klartext — englisch, nicht fuer die Oberflaeche gedacht"}},"required":["error","details"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsByIdParse-zugferd","tags":["documents","zugferd"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Parse embedded e-invoice data","description":"ZUGFeRD/XRechnung-Daten aus dem Dokument extrahieren, persistieren und Lieferant auto-matchen. Steckt kein Datensatz im Dokument, ist das Ergebnis `found: false` — mit Statuscode 200, und die zuvor gespeicherten Felder werden dabei GELEERT. Der Lieferant wird zuerst über die USt-IdNr. gesucht (Güte 1.0), sonst über eine Namensähnlichkeit (Güte 0.6); gibt es mehrere Treffer, gewinnt ein beliebiger. Der Aufruf schreibt in jedem Fall — er ist keine reine Abfrage."}},"/api/v1/documents/{id}/zugferd":{"get":{"responses":{"200":{"description":"Die gespeicherten Daten. Lauter null bedeutet: noch nie geparst — nicht, dass der Beleg keine Daten enthält.","content":{"application/json":{"schema":{"type":"object","properties":{"zugferd_data":{"description":"Der vollstaendige Parser-Rumpf, wie er beim Parsen gespeichert wurde; null, solange nie geparst wurde"},"zugferd_format":{"type":["string","null"],"description":"Erkanntes Format: zugferd, xrechnung oder unknown"},"supplier_id":{"type":["string","null"],"description":"Zugeordneter Lieferant"},"supplier_match_confidence":{"type":["number","null"],"description":"Guete der Zuordnung als Zahl — die Spalte ist NUMERIC, der Wert laeuft durch Number()"},"extracted_total_gross":{"type":["number","null"],"description":"Herausgelesene Bruttosumme als Zahl"},"extracted_invoice_number":{"type":["string","null"],"description":"Herausgelesene Rechnungsnummer"},"extracted_issue_date":{"type":["string","null"],"description":"Herausgelesenes Rechnungsdatum"}},"required":["zugferd_format","supplier_id","supplier_match_confidence","extracted_total_gross","extracted_invoice_number","extracted_issue_date"],"additionalProperties":false},"example":{"zugferd_format":"string","supplier_id":"string","supplier_match_confidence":0,"extracted_total_gross":0,"extracted_invoice_number":"string","extracted_issue_date":"string"}}}},"400":{"description":"Keine Dokument-Id im Pfad","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"missing_id","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Unauthorized"},"404":{"description":"Dokument nicht gefunden oder gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"document_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1DocumentsByIdZugferd","tags":["documents","zugferd"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Read stored e-invoice data","description":"Liest die zuvor gespeicherten ZUGFeRD/XRechnung-Daten eines Dokuments. Der Aufruf parst NICHT — wurde das Dokument nie geparst, kommt 200 mit lauter null-Werten, nicht 404. Die Felder heißen hier snake_case und anders als beim Parsen; der vollständige Parser-Rumpf steckt unter `zugferd_data`."}},"/api/v1/documents/{id}/book":{"post":{"responses":{"200":{"description":"Beleg war bereits gebucht — es wurde nichts geschrieben","content":{"application/json":{"schema":{"type":"object","properties":{"journalEntryId":{"type":"string","format":"uuid","description":"Kennung der BESTEHENDEN Buchung — nicht neu erzeugt"},"buchungsnummer":{"type":"string","minLength":1,"description":"Buchungsnummer aus dem frueheren Lauf"},"alreadyBooked":{"type":"boolean","const":true,"description":"Es wurde NICHTS gebucht; der Beleg trug bereits eine Buchung"}},"required":["journalEntryId","buchungsnummer","alreadyBooked"],"additionalProperties":false},"example":{"journalEntryId":"00000000-0000-4000-8000-000000000000","buchungsnummer":"string","alreadyBooked":true}}}},"201":{"description":"Beleg gebucht — diese Antwort trägt KEIN `alreadyBooked`","content":{"application/json":{"schema":{"type":"object","properties":{"journalEntryId":{"type":"string","format":"uuid","description":"Kennung der erzeugten Buchung im Journal"},"buchungsnummer":{"type":"string","minLength":1,"description":"Vergebene Buchungsnummer"}},"required":["journalEntryId","buchungsnummer"],"additionalProperties":false},"example":{"journalEntryId":"00000000-0000-4000-8000-000000000000","buchungsnummer":"string"}}}},"400":{"description":"Validierungsfehler oder fachlicher Fehler aus dem Buchungsdienst","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_input","posting_failed"],"description":"Code der Ausnahme aus dem Buchungsdienst"},"message":{"type":"string","minLength":1,"description":"Text der Ausnahme, englisch oder deutsch — nicht fuer die Maske gedacht"}},"required":["error","message"],"additionalProperties":false}}}},"401":{"description":"Kein Mandant im Kontext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"unauthorized","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"423":{"description":"Buchungsperiode gesperrt — ein späteres Datum wählen oder die Periode öffnen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"period_closed","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Begruendung im Klartext"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"Unerwarteter Serverfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"booking_failed","description":"Fester Fehlerschluessel"},"message":{"type":"string","description":"Meldung des zugrundeliegenden Fehlers, unveraendert durchgereicht"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar; der Fehlerschlüssel lautet hier `db_unavailable`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"db_unavailable","description":"Fester Fehlerschluessel — nicht database_unavailable wie anderswo"},"message":{"type":"string","minLength":1,"description":"Text der Ausnahme"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden; steht auch im Kopf Retry-After"}},"required":["error","message","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1DocumentsByIdBook","tags":["documents","booking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Book document as incoming invoice","description":"1-Klick-Buchung eines Belegs als Eingangsrechnung. Idempotent über die Beleg-Id: ein zweiter Aufruf bucht NICHT erneut, sondern gibt die bestehende Buchung zurück — daran zu erkennen, dass der Statuscode 200 statt 201 lautet und `alreadyBooked` gesetzt ist. Fehlen Soll- und Habenkonto, werden sie nach SKR03 aus dem Steuersatz abgeleitet; ausdrücklich angegebene Konten haben Vorrang. `betrag` ist der BRUTTOBETRAG — die Umsatzsteuer wird daraus herausgerechnet, nicht aufgeschlagen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sollkonto":{"type":"string","minLength":1,"maxLength":20},"habenkonto":{"type":"string","minLength":1,"maxLength":20},"betrag":{"type":"number","exclusiveMinimum":0},"steuersatz":{"type":"number","minimum":0,"maximum":100},"buchungstext":{"type":"string","minLength":1,"maxLength":500},"kostenstelle":{"type":"string","format":"uuid"},"datum":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"belegnummer":{"type":"string","maxLength":100}},"required":["betrag","steuersatz","buchungstext","datum"]},"example":{"sollkonto":"string","habenkonto":"string","betrag":1,"steuersatz":0,"buchungstext":"string","kostenstelle":"00000000-0000-4000-8000-000000000000","datum":"2026-01-01","belegnummer":"string"}}}}}},"/api/v1/documents/{id}/move-to-folder":{"post":{"responses":{"200":{"description":"Beleg umgehaengt. `document.folder_id` traegt den neuen Ordner oder null.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"document":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"folder_id":{"type":["string","null"],"format":"uuid"}},"required":["id","folder_id"]}},"required":["ok","document"],"additionalProperties":false},"example":{"ok":true,"document":{"id":"00000000-0000-4000-8000-000000000000","folder_id":"00000000-0000-4000-8000-000000000000"}}}}},"400":{"description":"Der Rumpf haelt das Schema nicht ein — `folderId` muss UUID oder null sein.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"403":{"description":"`document_archived` (freigegebene Belege sind unveraenderlich) oder `INSUFFICIENT_MODULE_PERMISSION` aus der Modul-Sperre `documents`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"404":{"description":"`folder_not_found` oder `document_not_found` (unbekannt oder im Papierkorb).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"422":{"description":"`folder_lane_mismatch` — Ordner und Beleg stehen in verschiedenen Boxen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"folder_lane_mismatch"},"expected":{"type":"string","description":"Box des Belegs."},"got":{"type":"string","description":"Box des Zielordners."}},"required":["error","expected","got"]}}}},"500":{"description":"`move_failed`; `message` traegt den Grund der Datenbank.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}}},"operationId":"postApiV1DocumentsByIdMove-to-folder","tags":["documents","folders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Beleg in einen Ordner legen","description":"Setzt die Ordner-Zuordnung eines Belegs (`folder_id`). `folderId: null`\nnimmt ihn aus dem Ordner heraus, ohne ihn zu loeschen — er liegt danach\nwieder ohne Ablageort in seiner Box.\n\nES WIRD NICHTS GELOESCHT UND NICHTS KOPIERT. Der Beleg selbst, seine\nDatei, seine Fassungen und sein Bearbeitungsstand bleiben unberuehrt;\nnur der Ablageort wechselt. Der Vorgang ist umkehrbar — derselbe Aufruf\nmit dem alten Ordner legt ihn zurueck.\n\nFreigegebene Belege sind gesperrt: steht `pipeline_status` auf\n`approved` (Archiv), antwortet die Route 403 `document_archived` und\naendert nichts. Das ist die einzige Rollen-unabhaengige Sperre hier.\n\nOrdner gehoeren je zu einer Box. Der Zielordner muss in derselben Box\nliegen wie der Beleg gerade steht, sonst 422 `folder_lane_mismatch` —\ndie Antwort nennt beide Boxen (`expected` die des Belegs, `got` die des\nOrdners). Ein Ordner ohne Box nimmt jeden Beleg an.\n\nBelege im Papierkorb (`deleted_at` gesetzt) sind nicht erreichbar und\nergeben 404 wie ein unbekannter Beleg.\n\nRECHTE: diese Route haengt unter `/documents/*` und damit an der\nModul-Sperre `documents`. Wer im Bereich Dokumente nicht schreiben darf,\nbekommt 403 `INSUFFICIENT_MODULE_PERMISSION`, bevor der Handler laeuft.\nDie Ordner-Routen unter `/document-folders` haben diese Sperre NICHT.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"folderId":{"type":["string","null"],"format":"uuid"}},"required":["folderId"]},"example":{"folderId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/documents/{id}/chat":{"post":{"responses":{"200":{"description":"Antwort und Belegstellen. Auch dann, wenn das Sprachmodell scheiterte.","content":{"application/json":{"schema":{"type":"object","properties":{"answer":{"type":"string","description":"Die Antwort in Klartext. Bei einem Fehler des Sprachmodells steht hier „Fehler: …\", bei unlesbarer Antwort „Keine Antwort verfuegbar\" — beides mit Status 200."},"citations":{"type":"array","items":{"type":"object","properties":{"snippet":{"type":"string"},"position":{"type":"number"}},"required":["snippet"]},"description":"Belegstellen, wie das Sprachmodell sie genannt hat. Nicht gegen den Text geprueft."}},"required":["answer","citations"]},"example":{"answer":"string","citations":[{"snippet":"string","position":0}]}}}},"400":{"description":"Der Rumpf haelt das Schema nicht ein — `question` fehlt oder ist zu lang."},"401":{"description":"`unauthorized` — kein Mandanten-Kontext."},"403":{"description":"`INSUFFICIENT_MODULE_PERMISSION` aus der Modul-Sperre `documents`."},"404":{"description":"`document_not_found` — unbekannt oder fremder Mandant."},"500":{"description":"Fehler beim LESEN des Belegs (etwa eine fehlende Tabelle). Fehler des Sprachmodells landen NICHT hier, sondern als Text in der 200-Antwort."},"503":{"description":"`database_unavailable`; die Antwort traegt `Retry-After: 5`."}},"operationId":"postApiV1DocumentsByIdChat","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Frage zu einem Beleg stellen","description":"Beantwortet eine Frage zu EINEM Beleg. Grundlage ist der erkannte Text\ndes Belegs; ein Sprachmodell formuliert daraus die Antwort und nennt\nBelegstellen.\n\nNur lesend. Am Beleg wird nichts geaendert, und die Frage wird nicht\ngespeichert — es gibt keinen Gespraechsverlauf, jeder Aufruf steht fuer\nsich. Der Verbrauch wird fuer die Kostenabrechnung mitgeschrieben.\n\nIst die Wissensdatenbank (RAG) verdrahtet, werden die passenden\nTextstellen gesucht; sonst gehen die ersten 12.000 Zeichen des erkannten\nTextes in die Anfrage. Fehlt der erkannte Text ganz, antwortet die Route\ntrotzdem — auf der Grundlage „[Kein OCR-Text]\".\n\nEIN FEHLER DES SPRACHMODELLS KOMMT ALS 200. Faellt der Aufruf des Modells\naus oder ist seine Antwort nicht lesbar, steht der Fehlertext in `answer`\n(„Fehler: …\" bzw. „Keine Antwort verfuegbar\") und `citations` ist leer.\nEin Aufrufer, der nur den Statuscode prueft, haelt das fuer eine Antwort.\nDie 500 unten deckt nur Fehler VOR dem Modell ab.\n\nDer Beleg wird in `public.documents` gesucht und dabei ueber die\nMandanten-Kennung eingegrenzt. Fremde oder unbekannte Belege ergeben 404.\n\nRECHTE: die Route haengt unter `/documents/*` und damit an der\nModul-Sperre `documents` — wer dort nicht schreiben darf, bekommt 403,\nobwohl die Route nichts schreibt. Die KI-Kontingente greifen NICHT: sie\nliegen auf `/ai/*` und `/ai-actions/*`, dieser Pfad gehoert nicht dazu.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"question":{"type":"string","minLength":1,"maxLength":2000}},"required":["question"]},"example":{"question":"string"}}}}}},"/api/v1/documents/{id}/annotations":{"get":{"responses":{"200":{"description":"Liste der Annotationen, im `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"documentId":{},"page":{"type":"number"},"type":{},"selectedText":{"type":["string","null"]},"comment":{"type":["string","null"]},"color":{},"rect":{},"createdBy":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["page","selectedText","comment","createdBy"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"page":0,"selectedText":"string","comment":"string","createdBy":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1DocumentsByIdAnnotations","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Listet alle Annotationen für ein Dokument — Markierungen und Kommentare gemeinsam, unterschieden durch `type`. Sortiert nach Seite, innerhalb der Seite nach Anlagezeit; weder Blätterung noch Obergrenze noch Filter. Die Tabelle wird beim ersten Zugriff angelegt, ein unbekanntes Dokument ergibt daher eine leere Liste statt 404. Scheitert die Abfrage, kommt bewusst ein Fehler und keine leere Liste — sonst wäre ein Datenbankproblem von „keine Annotationen\" nicht zu unterscheiden.","summary":"Listet alle Annotationen für ein Dokument","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Annotation erstellt — der Datensatz flach, ohne `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"documentId":{},"page":{"type":"number"},"type":{},"selectedText":{"type":["string","null"]},"comment":{"type":["string","null"]},"color":{},"rect":{},"createdBy":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["page","selectedText","comment","createdBy"],"additionalProperties":false},"example":{"page":0,"selectedText":"string","comment":"string","createdBy":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1DocumentsByIdAnnotations","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Erstellt eine neue Annotation (Highlight oder Kommentar). Pflicht ist nur `page` (ab 1); ohne Angabe entsteht ein `highlight` in Gelb. `rect` beschreibt den Kasten in Prozent der Seite (0–100), nicht in Pixeln. Ersteller ist der angemeldete Nutzer. Ob das Dokument überhaupt existiert, prüft der Aufruf NICHT — eine Annotation kann ins Leere zeigen. Mindestens Rolle `user`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"integer","minimum":1},"type":{"type":"string","enum":["highlight","comment"],"default":"highlight"},"selectedText":{"type":"string","maxLength":2000},"comment":{"type":"string","maxLength":4000},"color":{"type":"string","enum":["yellow","green","red","blue"],"default":"yellow"},"rect":{"type":"object","properties":{"x":{"type":"number","minimum":0,"maximum":100},"y":{"type":"number","minimum":0,"maximum":100},"width":{"type":"number","minimum":0,"maximum":100},"height":{"type":"number","minimum":0,"maximum":100}},"required":["x","y","width","height"]}},"required":["page"]},"example":{"page":1,"type":"highlight","selectedText":"string","comment":"string","color":"yellow","rect":{"x":0,"y":0,"width":0,"height":0}}}}},"summary":"Erstellt eine neue Annotation (Highlight oder Kommentar)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/documents/{id}/annotations/{annotId}":{"patch":{"responses":{"200":{"description":"Annotation aktualisiert — der vollständige Datensatz.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"documentId":{},"page":{"type":"number"},"type":{},"selectedText":{"type":["string","null"]},"comment":{"type":["string","null"]},"color":{},"rect":{},"createdBy":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["page","selectedText","comment","createdBy"],"additionalProperties":false},"example":{"page":0,"selectedText":"string","comment":"string","createdBy":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"patchApiV1DocumentsByIdAnnotationsByAnnotId","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"annotId","required":true}],"description":"Aktualisiert Kommentartext oder Farbe einer Annotation. Mehr lässt sich hier nicht ändern — Seite, Typ, markierter Text und Kasten bleiben, wie sie angelegt wurden. `updated_at` zieht auch dann mit, wenn der Rumpf leer ist. Die Annotation muss zu DIESEM Dokument gehören; sonst 404. Mindestens Rolle `user`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","maxLength":4000},"color":{"type":"string","enum":["yellow","green","red","blue"]}}},"example":{"comment":"string","color":"yellow"}}}},"summary":"Aktualisiert Kommentartext oder Farbe einer Annotation","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Quittung mit der entfernten Kennung, kein Datensatz.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deletedId":{"type":"string"}},"required":["ok","deletedId"],"additionalProperties":false},"example":{"ok":true,"deletedId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"deleteApiV1DocumentsByIdAnnotationsByAnnotId","tags":["documents"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"annotId","required":true}],"description":"Löscht eine Annotation endgültig — kein `deleted_at`, kein Zurückholen. Die Annotation muss zu DIESEM Dokument gehören; ein zweiter Löschversuch oder eine fremde Kennung ergibt 404. Wer sie angelegt hat, spielt keine Rolle: mindestens Rolle `user` genügt, jeder im Mandanten darf fremde Annotationen entfernen. Das Dokument selbst bleibt unberührt.","summary":"Löscht eine Annotation endgültig — kein `deleted_at`, kein Zurückholen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/projects/{projectId}/dossier":{"get":{"responses":{"200":{"description":"Die Akte und ihre Faecher.","content":{"application/json":{"schema":{"type":"object","properties":{"dossier":{"type":"object","additionalProperties":{},"description":"Die Akte, wie sie in der Datenbank steht (Spaltennamen, nicht umbenannt): id, tenant_id, project_id, template_layer, name, status, created_at, updated_at."},"slots":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Faecher der Akte, nach Position sortiert: id, position, slot_type, required, document_id, filled_at, notes."}},"required":["dossier","slots"]},"example":{"dossier":{},"slots":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Dossier zu diesem Projekt in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1ProjectsByProjectIdDossier","tags":["Projekte"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"summary":"Projektakte eines Projekts lesen","description":"Liefert die Akte eines Projekts samt ihrer Faecher. Gibt es mehrere\nAkten zum selben Projekt, kommt die ZULETZT ANGELEGTE — die Route\nentscheidet ueber `ORDER BY created_at DESC LIMIT 1`, nicht ueber eine\nKennzeichnung. Aeltere Akten sind hierueber nicht erreichbar.\n\nEine ungueltige Projekt-Kennung ergibt 400 (`invalid_projectId`), eine\ngueltige ohne Akte 404 (`dossier_not_found`). Beide kommen als\n`text/plain` bzw. als schlankes JSON — siehe die Codes unten.\n\nFeldnamen sind die SPALTENNAMEN der Datenbank, nicht die sonst uebliche\nSchreibweise mit Binnengrossbuchstaben: diese Route serialisiert nicht\num, sie reicht die Zeilen durch.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."},"post":{"responses":{"201":{"description":"Die angelegte Akte und ihre leeren Faecher.","content":{"application/json":{"schema":{"type":"object","properties":{"dossier":{"type":"object","additionalProperties":{},"description":"Die Akte, wie sie in der Datenbank steht (Spaltennamen, nicht umbenannt): id, tenant_id, project_id, template_layer, name, status, created_at, updated_at."},"slots":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Faecher der Akte, nach Position sortiert: id, position, slot_type, required, document_id, filled_at, notes."}},"required":["dossier","slots"]},"example":{"dossier":{},"slots":[{}]}}}},"400":{"description":"Die Projekt-Kennung hat nicht die Form einer Kennung — `invalid_projectId`, als `text/plain`."},"401":{"description":"Kein Mandantenkontext."},"403":{"description":"Kein Schreibrecht im Modul `projects`."},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1ProjectsByProjectIdDossier","tags":["Projekte"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"summary":"Projektakte zu einem Projekt anlegen","description":"Legt eine Akte an und fuellt sie sofort mit den leeren Faechern einer\nVorlage. Es gibt zwei: `bauprojekt` (Angebot, Vertrag, Aufmass,\nSchlussrechnung — alle vier Pflichtfaecher) und `default` (Angebot,\nAuftrag, Lieferschein als einziges freiwilliges Fach, Rechnung).\n\nOhne `templateType` gilt `bauprojekt`. EIN UNBEKANNTER WERT IST KEIN\nFEHLER: er faellt still auf `default` zurueck. Wer sich vertippt, bekommt\ndie andere Akte und keinen Hinweis darauf.\n\nWELCHE VORLAGE GENOMMEN WURDE, WIRD NICHT FESTGEHALTEN. Das Feld\n`template_layer` in der Antwort steht bei jeder Akte auf dem\nDatenbank-Vorgabewert `hersteller` — es wird beim Anlegen nicht\nbeschrieben. Woran die Akte gebaut ist, erkennt man nur an den Faechern.\n\nDAS PROJEKT WIRD NICHT GEPRUEFT, und es gibt keine Eindeutigkeit: jeder\nAufruf legt eine WEITERE Akte zum selben Projekt an. `GET\n/projects/{projectId}/dossier` liefert nur die ZULETZT angelegte — aeltere\nsind darueber nicht mehr erreichbar, bleiben aber in der Tabelle stehen.\n\nKEINE TRANSAKTION: Akte und Faecher werden nacheinander geschrieben.\nBricht ein Fach ab, bleibt die Akte mit unvollstaendigen Faechern zurueck.\n\nOhne `name` heisst die Akte woertlich „Projektakte\", ihr Status ist `open`.\n\nSchreibend, deshalb greift die Modul-Wache `projects`: eine Rolle unter\n`manager` ohne ausdrueckliches Schreibrecht bekommt 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"templateType":{"type":"string","minLength":1,"maxLength":60},"name":{"type":"string","minLength":1,"maxLength":200}}},"example":{"templateType":"string","name":"string"}}}}}},"/api/v1/contact-categories":{"get":{"responses":{"200":{"description":"Alle Kategorien des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"abbreviation":{"type":["string","null"]},"color":{"type":"string"},"category_type":{"type":"string","enum":["Verkauf","Einkauf","Allgemein","Service","Sonstige"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","abbreviation","color","category_type","created_at","updated_at"]}}},"required":["data"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","name":"string","abbreviation":"string","color":"string","category_type":"Verkauf","created_at":"string","updated_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Contact-categories","tags":["Contact Categories"],"parameters":[],"summary":"List all contact categories","description":"Liefert alle Kontaktkategorien des Mandanten. Gelesen wird `contact_categories` im Mandantenschema, sortiert nach `category_type` und `name`. Es gibt weder Soft-Delete noch Blaettern: die Antwort enthaelt immer alle Zeilen der Tabelle. Mindestrolle `user`."},"post":{"responses":{"201":{"description":"Kategorie angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"abbreviation":{"type":["string","null"]},"color":{"type":"string"},"category_type":{"type":"string","enum":["Verkauf","Einkauf","Allgemein","Service","Sonstige"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","abbreviation","color","category_type","created_at","updated_at"]},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","abbreviation":"string","color":"string","category_type":"Verkauf","created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Contact-categories","tags":["Contact Categories"],"parameters":[],"summary":"Create new contact category","description":"Legt eine neue Kontaktkategorie an. Geschrieben wird nach `contact_categories` im Mandantenschema; die Antwort ist 201 mit der angelegten Zeile. Ohne Angabe faellt `color` auf `#000000` und `category_type` auf `Allgemein`. Ein schon vergebener Name verletzt den eindeutigen Index auf LOWER(name) und ergibt 409. Mindestrolle `user`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"abbreviation":{"type":"string","maxLength":10},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"category_type":{"type":"string","enum":["Verkauf","Einkauf","Allgemein","Service","Sonstige"],"default":"Allgemein"}},"required":["name"]},"example":{"name":"string","abbreviation":"string","category_type":"Verkauf"}}}}}},"/api/v1/contact-categories/{id}":{"put":{"responses":{"200":{"description":"Kategorie aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"abbreviation":{"type":["string","null"]},"color":{"type":"string"},"category_type":{"type":"string","enum":["Verkauf","Einkauf","Allgemein","Service","Sonstige"]},"updated_at":{"type":"string"}},"required":["id","name","abbreviation","color","category_type","updated_at"]},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","abbreviation":"string","color":"string","category_type":"Verkauf","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putApiV1Contact-categoriesById","tags":["Contact Categories"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update contact category","description":"Aktualisiert eine Kontaktkategorie vollstaendig. Der Rumpf ersetzt alle vier Felder; was er auslaesst, faellt auf den Standard zurueck (`abbreviation` auf NULL, `color` auf `#000000`, `category_type` auf `Allgemein`). Die Antwort enthaelt `created_at` nicht, nur `updated_at`. Eine unbekannte Kennung ergibt 404. Mindestrolle `user`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"abbreviation":{"type":"string","maxLength":10},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"category_type":{"type":"string","enum":["Verkauf","Einkauf","Allgemein","Service","Sonstige"],"default":"Allgemein"}},"required":["name"]},"example":{"name":"string","abbreviation":"string","category_type":"Verkauf"}}}}},"delete":{"responses":{"200":{"description":"Kategorie geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}},"required":["deleted","id"]},"example":{"deleted":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1Contact-categoriesById","tags":["Contact Categories"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete contact category","description":"Loescht eine Kontaktkategorie endgueltig. Zuvor wird `category_id` in `customers` und `contacts` auf NULL gesetzt, die zugeordneten Datensaetze bleiben also erhalten. Die Kategorie selbst verschwindet per SQL-DELETE; es gibt kein Soft-Delete und kein Rueckgaengig. Eine unbekannte Kennung ergibt 404. Mindestrolle `admin`."}},"/api/v1/contact-types":{"get":{"responses":{"200":{"description":"Alle Typen, System-Typen zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"is_system":{"type":"boolean","description":"true bei den drei gesperrten Typen — die lassen sich weder umbenennen noch loeschen"},"sort_order":{"type":"integer","description":"Kleiner heisst weiter oben; Vorgabe 100"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","is_system","sort_order","created_at","updated_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","name":"string","is_system":true,"sort_order":0,"created_at":"string","updated_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — die Meldung nennt den Grund"}},"operationId":"getApiV1Contact-types","tags":["Contact Types"],"parameters":[],"summary":"List all contact types","description":"Listet die Kontakt-Typen des Mandanten — erst die drei gesperrten System-Typen (Interessent, Kunde, Lieferant), dann die eigenen, jeweils nach `sort_order` und Name. Fehlt die Tabelle im Mandanten, wird sie beim ersten Aufruf angelegt und mit den drei System-Typen befuellt: eine leere Liste kann es also nicht geben. Ohne Filter und ohne Blaetterung."},"post":{"responses":{"201":{"description":"Der angelegte Typ.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"is_system":{"type":"boolean","description":"true bei den drei gesperrten Typen — die lassen sich weder umbenennen noch loeschen"},"sort_order":{"type":"integer","description":"Kleiner heisst weiter oben; Vorgabe 100"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","is_system","sort_order","created_at","updated_at"]},"example":{"id":"string","name":"string","is_system":true,"sort_order":0,"created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Ein Typ dieses Namens existiert bereits"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1Contact-types","tags":["Contact Types"],"parameters":[],"summary":"Create a custom contact type","description":"Legt einen eigenen Kontakt-Typ an. `is_system` ist dabei immer `false` — gesperrte System-Typen entstehen nur beim Befuellen der Tabelle, nicht ueber diesen Aufruf. `sort_order` steuert die Reihenfolge in der Liste (Vorgabe 100). Der Name muss frei sein, ohne Ruecksicht auf Gross- und Kleinschreibung: ein vergebener Name ergibt 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"sort_order":{"type":"integer","minimum":0,"maximum":100000}},"required":["name"]},"example":{"name":"string","sort_order":0}}}}}},"/api/v1/contact-types/{id}":{"put":{"responses":{"200":{"description":"Der umbenannte Typ — ohne `created_at`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"is_system":{"type":"boolean","description":"Hier immer false — System-Typen kommen bis hierher nicht"},"sort_order":{"type":"integer"},"updated_at":{"type":"string"}},"required":["id","name","is_system","sort_order","updated_at"]},"example":{"id":"string","name":"string","is_system":true,"sort_order":0,"updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"System-Typen lassen sich nicht aendern"},"404":{"description":"Kein Typ mit dieser Id"},"409":{"description":"Ein Typ dieses Namens existiert bereits"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1Contact-typesById","tags":["Contact Types"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rename a custom contact type","description":"Benennt einen eigenen Kontakt-Typ um und setzt bei Bedarf seine Reihenfolge neu; ohne `sort_order` bleibt die bisherige stehen. Die drei System-Typen sind gesperrt und ergeben 403. Kunden, die auf dem alten Namen stehen, werden NICHT nachgezogen — der Bezug laeuft ueber den Namen, nicht ueber die Id, und ein umbenannter Typ laesst sie auf einem Namen zurueck, den es nicht mehr gibt. Die Antwort traegt `created_at` NICHT.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"sort_order":{"type":"integer","minimum":0,"maximum":100000}},"required":["name"]},"example":{"name":"string","sort_order":0}}}}},"delete":{"responses":{"200":{"description":"Geloescht; betroffene Kunden stehen jetzt auf `Interessent`.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true},"id":{"type":"string","description":"Die Id aus dem Pfad, unveraendert zurueckgespiegelt"}},"required":["deleted","id"]},"example":{"deleted":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"System-Typen lassen sich nicht loeschen, oder Rolle unter `admin`"},"404":{"description":"Kein Typ mit dieser Id"}},"operationId":"deleteApiV1Contact-typesById","tags":["Contact Types"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete a custom contact type","description":"Loescht einen eigenen Kontakt-Typ endgueltig — kein Soft-Delete, keine Ruecknahme. VORHER werden alle Kunden dieses Typs auf `Interessent` umgestellt, damit kein Datensatz auf einen Typ zeigt, den es nicht mehr gibt; welche das waren, sagt die Antwort nicht, und rueckgaengig macht es niemand. Die drei System-Typen sind gesperrt und ergeben 403. Ab Rolle `admin` — anders als die uebrigen drei Aufrufe dieses Bereichs."}},"/api/v1/address-types":{"get":{"responses":{"200":{"description":"Alle Adresstypen des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"is_system":{"type":"boolean","description":"TRUE bei den beiden festen Systemtypen"},"sort_order":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","is_system","sort_order","created_at","updated_at"]}}},"required":["data"]},"example":{"data":[{"id":"string","name":"string","is_system":true,"sort_order":0,"created_at":"string","updated_at":"string"}]}}}},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"Abfrage fehlgeschlagen"}},"operationId":"getApiV1Address-types","tags":["Address Types"],"parameters":[],"summary":"List all address types","description":"Liest `<mandant>.address_types` und gibt Systemtypen zuerst zurück, danach die eigenen — sortiert nach `sort_order`, bei Gleichstand nach Name. Beim ersten Aufruf je Prozesslauf legt die Route die Tabelle an und ergänzt die beiden Systemtypen „Rechnungsadresse\" und „Lieferadresse\", falls sie fehlen. Es gibt weder Blätterung noch Soft-Delete."},"post":{"responses":{"201":{"description":"Adresstyp angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"is_system":{"type":"boolean","description":"TRUE bei den beiden festen Systemtypen"},"sort_order":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","is_system","sort_order","created_at","updated_at"]},"example":{"id":"string","name":"string","is_system":true,"sort_order":0,"created_at":"string","updated_at":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Kein Mandantenkontext"},"409":{"description":"Name bereits vergeben"},"503":{"description":"Nicht angelegt"}},"operationId":"postApiV1Address-types","tags":["Address Types"],"parameters":[],"summary":"Create a custom address type","description":"Legt einen eigenen Adresstyp an — `is_system` ist dabei immer FALSE, ein Systemtyp lässt sich über diesen Weg nicht erzeugen. Ohne `sort_order` steht der neue Typ auf 100 und landet damit hinter den beiden Systemtypen. Der Name ist ohne Rücksicht auf Groß- und Kleinschreibung eindeutig; ein bereits vergebener Name endet mit 409. Die Antwort ist der angelegte Datensatz ohne Umschlag.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"sort_order":{"type":"integer","minimum":0,"maximum":100000}},"required":["name"]},"example":{"name":"string","sort_order":0}}}}}},"/api/v1/address-types/{id}":{"put":{"responses":{"200":{"description":"Geänderter Adresstyp — ohne `created_at`","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"is_system":{"type":"boolean","description":"TRUE bei den beiden festen Systemtypen"},"sort_order":{"type":"integer"},"updated_at":{"type":"string"}},"required":["id","name","is_system","sort_order","updated_at"]},"example":{"id":"string","name":"string","is_system":true,"sort_order":0,"updated_at":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Systemtyp — nicht änderbar"},"404":{"description":"Adresstyp nicht gefunden"},"409":{"description":"Name bereits vergeben"},"503":{"description":"Nicht gespeichert"}},"operationId":"putApiV1Address-typesById","tags":["Address Types"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rename a custom address type","description":"Benennt einen eigenen Adresstyp um und setzt auf Wunsch die Sortierung neu; ohne `sort_order` bleibt die bisherige stehen. Die beiden Systemtypen sind gesperrt und werden mit 403 abgewiesen, eine unbekannte Kennung mit 404, ein schon vergebener Name mit 409. Die Antwort enthält `created_at` nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"sort_order":{"type":"integer","minimum":0,"maximum":100000}},"required":["name"]},"example":{"name":"string","sort_order":0}}}}},"delete":{"responses":{"200":{"description":"Adresstyp gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["deleted","id"]},"example":{"deleted":true,"id":"string"}}}},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Systemtyp oder fehlende Admin-Rolle"},"404":{"description":"Adresstyp nicht gefunden"}},"operationId":"deleteApiV1Address-typesById","tags":["Address Types"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete a custom address type","description":"Entfernt einen eigenen Adresstyp endgültig aus der Tabelle — kein Soft-Delete, kein Wiederherstellen. Die beiden Systemtypen sind gesperrt (403), eine unbekannte Kennung ergibt 404. Anders als Lesen und Anlegen verlangt dieser Aufruf mindestens die Rolle „admin\". Die Route prüft nicht, ob der Typ noch an Adressen hängt."}},"/api/v1/customers/{customerId}/addresses":{"get":{"responses":{"200":{"description":"Adressen — zentrale zuerst, dann die Zusatz-Adressen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"street":{"type":"string"},"zip":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"isCentralBilling":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","label","type","street","zip","city","country","email","phone","isCentralBilling","createdAt","updatedAt"]}}},"required":["data"]},"example":{"data":[{"id":"string","label":"string","type":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","isCentralBilling":true,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kontakt nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1CustomersByCustomerIdAddresses","tags":["Customer Addresses"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true}],"summary":"List addresses of a customer","description":"Gibt beide Quellen in EINER Liste zurueck: zuerst die zentrale Rechnungsadresse aus den Stammdaten (`customers.address`), danach die Zusatz-Adressen aus dem Array. Die zentrale traegt die feste Kennung \"central\" und ist ueber diesen Endpunkt nicht aenderbar; E-Mail und Telefon stammen bei ihr aus dem Kontakt selbst, nicht aus der Adresse. Sie fehlt in der Liste, wenn Strasze, PLZ und Ort alle leer sind. Ohne Blaetterung und ohne Filter. 404, wenn es den Kontakt nicht gibt oder er geloescht ist."},"post":{"responses":{"201":{"description":"Zusatz-Adresse angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"street":{"type":"string"},"zip":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"isCentralBilling":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","label","type","street","zip","city","country","email","phone","isCentralBilling","createdAt","updatedAt"]},"example":{"id":"string","label":"string","type":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","isCentralBilling":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kontakt nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1CustomersByCustomerIdAddresses","tags":["Customer Addresses"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true}],"summary":"Add an extra address to a customer","description":"Haengt eine ZUSATZ-Adresse an das Array `customers.addresses` an; die zentrale Rechnungsadresse bleibt unberuehrt. Die Kennung vergibt der Server. Nur `type` ist Pflicht (freier Text, keine feste Liste) — alle uebrigen Felder fallen auf einen leeren Wert zurueck, `country` auf \"DE\". Ein `isCentralBilling` im Rumpf wird ANGENOMMEN, aber verworfen: eine Zusatz-Adresse ist nie zentral; dafuer gibt es POST /:addressId/make-central. Es wird nicht auf Dubletten geprueft. 404, wenn es den Kontakt nicht gibt oder er geloescht ist.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","maxLength":120,"default":""},"type":{"type":"string","minLength":1,"maxLength":120},"street":{"type":"string","maxLength":255,"default":""},"zip":{"type":"string","maxLength":20,"default":""},"city":{"type":"string","maxLength":120,"default":""},"country":{"type":"string","maxLength":60,"default":"DE"},"email":{"type":"string","maxLength":200,"default":""},"phone":{"type":"string","maxLength":60,"default":""},"isCentralBilling":{"type":"boolean"}},"required":["type"]},"example":{"label":"string","type":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","isCentralBilling":true}}}}}},"/api/v1/customers/{customerId}/addresses/{addressId}":{"put":{"responses":{"200":{"description":"Adresse nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"street":{"type":"string"},"zip":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"isCentralBilling":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","label","type","street","zip","city","country","email","phone","isCentralBilling","createdAt","updatedAt"]},"example":{"id":"string","label":"string","type":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","isCentralBilling":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Die zentrale Adresse wird ueber die Kontaktuebersicht geaendert"},"404":{"description":"Kontakt oder Adresse nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1CustomersByCustomerIdAddressesByAddressId","tags":["Customer Addresses"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true},{"schema":{"type":"string"},"in":"path","name":"addressId","required":true}],"summary":"Update an extra address","description":"VOLL-Ersatz, kein Teil-Update: nicht gesendete Felder werden auf ihren Vorgabewert gesetzt (leer, `country` auf \"DE\"), nicht beibehalten. Erhalten bleiben nur Kennung und Anlagezeitpunkt; `updatedAt` wird neu gesetzt. Die zentrale Adresse laesst sich hier NICHT aendern — die Kennung \"central\" ergibt 403, sie wird ueber die Kontaktuebersicht gepflegt. 404, wenn es den Kontakt oder die Adresse nicht gibt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","maxLength":120,"default":""},"type":{"type":"string","minLength":1,"maxLength":120},"street":{"type":"string","maxLength":255,"default":""},"zip":{"type":"string","maxLength":20,"default":""},"city":{"type":"string","maxLength":120,"default":""},"country":{"type":"string","maxLength":60,"default":"DE"},"email":{"type":"string","maxLength":200,"default":""},"phone":{"type":"string","maxLength":60,"default":""},"isCentralBilling":{"type":"boolean"}},"required":["type"]},"example":{"label":"string","type":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","isCentralBilling":true}}}}},"delete":{"responses":{"200":{"description":"Adresse geloescht — beim Loeschen der Zentrale mit der Nachrueckerin","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true},"id":{"type":"string"},"promoted":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"street":{"type":"string"},"zip":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"isCentralBilling":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","label","type","street","zip","city","country","email","phone","isCentralBilling","createdAt","updatedAt"]},"extras":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"street":{"type":"string"},"zip":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"isCentralBilling":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","label","type","street","zip","city","country","email","phone","isCentralBilling","createdAt","updatedAt"]}}},"required":["deleted","id"]},"example":{"deleted":true,"id":"string","promoted":{"id":"string","label":"string","type":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","isCentralBilling":true,"createdAt":"string","updatedAt":"string"},"extras":[{"id":"string","label":"string","type":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","isCentralBilling":true,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kontakt oder Adresse nicht gefunden"},"409":{"description":"Keine Nachfolgerin fuer die zentrale Anschrift vorhanden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1CustomersByCustomerIdAddressesByAddressId","tags":["Customer Addresses"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true},{"schema":{"type":"string"},"in":"path","name":"addressId","required":true}],"summary":"Delete an extra address","description":"Entfernt den Eintrag ENDGUELTIG aus dem Adress-Array — kein Soft-Delete, kein Rueckgaengig. Die Kennung \"central\" ist erlaubt, aber nur unter einer Bedingung: es muss eine weitere Adresse vom Typ \"Rechnungsadresse\" geben, die nachruecken kann. Sie wird dann zur neuen Zentrale (Bezeichnung \"Zentrale\") und verschwindet aus den Zusatz-Adressen; die Antwort nennt sie unter `promoted` samt der verbliebenen Liste. Gibt es keine Nachfolgerin, antwortet der Endpunkt 409 und aendert nichts. 404, wenn es den Kontakt oder die Adresse nicht gibt."}},"/api/v1/customers/{customerId}/addresses/{addressId}/make-central":{"post":{"responses":{"200":{"description":"Zentrale umgehaengt — oder nichts zu tun (alreadyCentral)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"central":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"street":{"type":"string"},"zip":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"isCentralBilling":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","label","type","street","zip","city","country","email","phone","isCentralBilling","createdAt","updatedAt"]},"extras":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"street":{"type":"string"},"zip":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"isCentralBilling":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","label","type","street","zip","city","country","email","phone","isCentralBilling","createdAt","updatedAt"]}},"alreadyCentral":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true,"central":{"id":"string","label":"string","type":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","isCentralBilling":true,"createdAt":"string","updatedAt":"string"},"extras":[{"id":"string","label":"string","type":"string","street":"string","zip":"string","city":"string","country":"string","email":"string","phone":"string","isCentralBilling":true,"createdAt":"string","updatedAt":"string"}],"alreadyCentral":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kontakt oder Adresse nicht gefunden"},"409":{"description":"Nur eine Rechnungsadresse kann zentral werden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1CustomersByCustomerIdAddressesByAddressIdMake-central","tags":["Customer Addresses"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true},{"schema":{"type":"string"},"in":"path","name":"addressId","required":true}],"summary":"Promote an extra billing address to central","description":"Taucht die gewaehlte Zusatz-Adresse und die bisherige Zentrale: die gewaehlte wird zur zentralen Rechnungsadresse (Bezeichnung \"Zentrale\") und verlaesst das Zusatz-Array, die bisherige Zentrale wandert als Zusatz-Adresse mit NEUER Kennung an dessen Anfang und verliert die Bezeichnung \"Zentrale\". Enthielt sie weder Strasze noch PLZ noch Ort, geht sie ersatzlos verloren. Nur Adressen vom Typ \"Rechnungsadresse\" duerfen zentral werden (sonst 409). Die Kennung \"central\" ist kein Fehler: sie aendert nichts und meldet `alreadyCentral`. 404, wenn es den Kontakt oder die Adresse nicht gibt."}},"/api/v1/customers/{customerId}/bank-accounts":{"get":{"responses":{"200":{"description":"Liste, Standardkonto zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Bankverbindung"},"customerId":{"type":"string","format":"uuid","description":"Kontakt, zu dem die Verbindung gehoert"},"label":{"type":["string","null"],"maxLength":120,"description":"Freie Bezeichnung, etwa „Geschaeftskonto\""},"iban":{"type":"string","minLength":5,"maxLength":40,"description":"IBAN, normalisiert: ohne Leerzeichen und in Grossbuchstaben; NICHT auf Pruefziffern geprueft"},"bic":{"type":["string","null"],"maxLength":20,"description":"BIC der Bank"},"bankName":{"type":["string","null"],"maxLength":120,"description":"Name der Bank"},"accountHolder":{"type":["string","null"],"maxLength":255,"description":"Kontoinhaber, falls abweichend"},"isDefault":{"type":"boolean","description":"Genau eine Verbindung je Kontakt traegt true; sie steht auf den Belegen"},"note":{"type":["string","null"],"maxLength":2000,"description":"Interne Notiz"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","customerId","label","iban","bic","bankName","accountHolder","isDefault","note","createdAt","updatedAt"],"additionalProperties":false},"description":"Die Bankverbindungen des Kontakts, Standardkonto zuerst, danach die aeltesten"},"total":{"type":"integer","minimum":0,"description":"Laenge der gelieferten Liste"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","customerId":"00000000-0000-4000-8000-000000000000","label":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","isDefault":true,"note":"string","createdAt":"string","updatedAt":"string"}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"404":{"description":"Kontakt nicht gefunden — als text/plain, nicht als JSON"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1CustomersByCustomerIdBank-accounts","tags":["Customer Bank Accounts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true}],"summary":"Listet die Bankverbindungen eines Kontakts","description":"Listet die Bankverbindungen eines Kontakts. Standardkonto zuerst, danach nach Alter aufsteigend. Weich gelöschte Verbindungen fehlen. Es wird nicht geblättert — `total` ist die Länge der gelieferten Liste."},"post":{"responses":{"201":{"description":"Angelegt — die neue Verbindung. `null`, falls die Datenbank keine Zeile zurückgab; der Statuscode bleibt trotzdem 201.","content":{"application/json":{"schema":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Bankverbindung"},"customerId":{"type":"string","format":"uuid","description":"Kontakt, zu dem die Verbindung gehoert"},"label":{"type":["string","null"],"maxLength":120,"description":"Freie Bezeichnung, etwa „Geschaeftskonto\""},"iban":{"type":"string","minLength":5,"maxLength":40,"description":"IBAN, normalisiert: ohne Leerzeichen und in Grossbuchstaben; NICHT auf Pruefziffern geprueft"},"bic":{"type":["string","null"],"maxLength":20,"description":"BIC der Bank"},"bankName":{"type":["string","null"],"maxLength":120,"description":"Name der Bank"},"accountHolder":{"type":["string","null"],"maxLength":255,"description":"Kontoinhaber, falls abweichend"},"isDefault":{"type":"boolean","description":"Genau eine Verbindung je Kontakt traegt true; sie steht auf den Belegen"},"note":{"type":["string","null"],"maxLength":2000,"description":"Interne Notiz"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","customerId","label","iban","bic","bankName","accountHolder","isDefault","note","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","customerId":"00000000-0000-4000-8000-000000000000","label":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","isDefault":true,"note":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"404":{"description":"Kontakt nicht gefunden — als text/plain, nicht als JSON"},"409":{"description":"IBAN bereits bei diesem Kontakt hinterlegt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"iban_exists","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1CustomersByCustomerIdBank-accounts","tags":["Customer Bank Accounts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true}],"summary":"Legt eine weitere Bankverbindung an","description":"Legt eine Bankverbindung an. Die IBAN wird normalisiert gespeichert (ohne Leerzeichen, Großbuchstaben), aber NICHT auf Prüfziffern geprüft — ausländische und Testkonten sollen nicht scheitern. Die erste Verbindung eines Kontakts wird immer zum Standard, auch ohne `isDefault`. Ein neuer Standard nimmt dem bisherigen die Markierung und wird in die Kundenstammdaten zurückgeschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"iban":{"type":"string","minLength":5,"maxLength":40},"label":{"type":["string","null"],"maxLength":120},"bic":{"type":["string","null"],"maxLength":20},"bankName":{"type":["string","null"],"maxLength":120},"accountHolder":{"type":["string","null"],"maxLength":255},"note":{"type":["string","null"],"maxLength":2000},"isDefault":{"type":"boolean"}},"required":["iban"]},"example":{"iban":"string","label":"string","bic":"string","bankName":"string","accountHolder":"string","note":"string","isDefault":true}}}}}},"/api/v1/customers/{customerId}/bank-accounts/{accountId}":{"put":{"responses":{"200":{"description":"Geändert — die Verbindung nach der Änderung. `null`, falls die Datenbank keine Zeile zurückgab; der Statuscode bleibt trotzdem 200.","content":{"application/json":{"schema":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Bankverbindung"},"customerId":{"type":"string","format":"uuid","description":"Kontakt, zu dem die Verbindung gehoert"},"label":{"type":["string","null"],"maxLength":120,"description":"Freie Bezeichnung, etwa „Geschaeftskonto\""},"iban":{"type":"string","minLength":5,"maxLength":40,"description":"IBAN, normalisiert: ohne Leerzeichen und in Grossbuchstaben; NICHT auf Pruefziffern geprueft"},"bic":{"type":["string","null"],"maxLength":20,"description":"BIC der Bank"},"bankName":{"type":["string","null"],"maxLength":120,"description":"Name der Bank"},"accountHolder":{"type":["string","null"],"maxLength":255,"description":"Kontoinhaber, falls abweichend"},"isDefault":{"type":"boolean","description":"Genau eine Verbindung je Kontakt traegt true; sie steht auf den Belegen"},"note":{"type":["string","null"],"maxLength":2000,"description":"Interne Notiz"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","customerId","label","iban","bic","bankName","accountHolder","isDefault","note","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","customerId":"00000000-0000-4000-8000-000000000000","label":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","isDefault":true,"note":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"404":{"description":"Kontakt oder Bankverbindung nicht gefunden, oder die Id hat kein UUID-Format — als text/plain, nicht als JSON"},"409":{"description":"Die neue IBAN liegt bereits auf einer anderen Verbindung desselben Kontakts","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"iban_exists","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1CustomersByCustomerIdBank-accountsByAccountId","tags":["Customer Bank Accounts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true},{"schema":{"type":"string"},"in":"path","name":"accountId","required":true}],"summary":"Ändert eine Bankverbindung","description":"Ändert eine Bankverbindung. Nicht mitgeschickte Felder bleiben, wie sie sind. Ein Standardkonto lässt sich hier NICHT abwählen: `isDefault: false` auf dem aktuellen Standard bleibt wirkungslos, damit der Kontakt nie ohne Bankverbindung auf dem Beleg steht. Um zu wechseln, das andere Konto zum Standard machen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"iban":{"type":"string","minLength":5,"maxLength":40},"label":{"type":["string","null"],"maxLength":120},"bic":{"type":["string","null"],"maxLength":20},"bankName":{"type":["string","null"],"maxLength":120},"accountHolder":{"type":["string","null"],"maxLength":255},"note":{"type":["string","null"],"maxLength":2000},"isDefault":{"type":"boolean"}}},"example":{"iban":"string","label":"string","bic":"string","bankName":"string","accountHolder":"string","note":"string","isDefault":true}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Quittung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Verbindung wurde weich geloescht"}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"404":{"description":"Kontakt oder Bankverbindung nicht gefunden, oder die Id hat kein UUID-Format — als text/plain, nicht als JSON"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1CustomersByCustomerIdBank-accountsByAccountId","tags":["Customer Bank Accounts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true},{"schema":{"type":"string"},"in":"path","name":"accountId","required":true}],"summary":"Löscht eine Bankverbindung (weich)","description":"Löscht eine Bankverbindung weich — die Zeile bleibt mit Löschzeitpunkt stehen, taucht aber in keiner Liste mehr auf. War es der Standard, rückt die älteste verbleibende Verbindung nach und wird in die Kundenstammdaten geschrieben. War es die letzte, werden die Bankfelder des Kontakts geleert."}},"/api/v1/customers/{customerId}/bank-accounts/{accountId}/default":{"post":{"responses":{"200":{"description":"Gesetzt — alle Verbindungen des Kontakts, Standardkonto zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Bankverbindung"},"customerId":{"type":"string","format":"uuid","description":"Kontakt, zu dem die Verbindung gehoert"},"label":{"type":["string","null"],"maxLength":120,"description":"Freie Bezeichnung, etwa „Geschaeftskonto\""},"iban":{"type":"string","minLength":5,"maxLength":40,"description":"IBAN, normalisiert: ohne Leerzeichen und in Grossbuchstaben; NICHT auf Pruefziffern geprueft"},"bic":{"type":["string","null"],"maxLength":20,"description":"BIC der Bank"},"bankName":{"type":["string","null"],"maxLength":120,"description":"Name der Bank"},"accountHolder":{"type":["string","null"],"maxLength":255,"description":"Kontoinhaber, falls abweichend"},"isDefault":{"type":"boolean","description":"Genau eine Verbindung je Kontakt traegt true; sie steht auf den Belegen"},"note":{"type":["string","null"],"maxLength":2000,"description":"Interne Notiz"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","customerId","label","iban","bic","bankName","accountHolder","isDefault","note","createdAt","updatedAt"],"additionalProperties":false},"description":"Alle Bankverbindungen des Kontakts nach der Umstellung, Standardkonto zuerst"}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","customerId":"00000000-0000-4000-8000-000000000000","label":"string","iban":"string","bic":"string","bankName":"string","accountHolder":"string","isDefault":true,"note":"string","createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"404":{"description":"Kontakt oder Bankverbindung nicht gefunden, oder die Id hat kein UUID-Format — als text/plain, nicht als JSON"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1CustomersByCustomerIdBank-accountsByAccountIdDefault","tags":["Customer Bank Accounts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"customerId","required":true},{"schema":{"type":"string"},"in":"path","name":"accountId","required":true}],"summary":"Markiert eine Bankverbindung als Standard","description":"Macht eine Bankverbindung zum Standard. Der bisherige Standard verliert die Markierung, und die neue Verbindung wird in die Kundenstammdaten zurückgeschrieben. Die Antwort ist die GANZE Liste des Kontakts, nicht die eine geänderte Verbindung."}},"/api/v1/tags":{"get":{"responses":{"200":{"description":"Alle Tags des Mandanten, im `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"created_at":{},"updated_at":{}},"required":["id","name"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Tags","tags":["Tags"],"parameters":[],"summary":"List all tags","description":"Liest die Tabelle `tags` des Mandanten, alphabetisch nach Namen. Weder Filter noch Blaetterung noch Obergrenze. Beim ERSTEN Zugriff je Mandant legt die Route die Tabelle an und uebernimmt dabei alle Schlagworte, die bereits in `customers.tags` stehen — die Liste kann also Eintraege enthalten, die nie ueber diese API angelegt wurden. Die Antwort sagt nicht, wie oft ein Tag verwendet wird."},"post":{"responses":{"200":{"description":"Den Namen gab es bereits — zurueck kommt der vorhandene Tag, unveraendert.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"created_at":{},"updated_at":{}},"required":["id","name"],"additionalProperties":false},"example":{"id":"string","name":"string"}}}},"201":{"description":"Der neu angelegte Tag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"created_at":{},"updated_at":{}},"required":["id","name"],"additionalProperties":false},"example":{"id":"string","name":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Tags","tags":["Tags"],"parameters":[],"summary":"Create a tag","description":"Legt ein Schlagwort an — idempotent. Gibt es den Namen schon (Vergleich ohne Gross-/Kleinschreibung), antwortet die Route 200 mit dem VORHANDENEN Tag statt 201 und legt nichts Neues an; der Statuscode ist damit der einzige Unterschied. Der Name wird getrimmt und darf hoechstens 120 Zeichen lang sein. Ein 409 gibt es hier trotz des Unique-Index nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120}},"required":["name"]},"example":{"name":"string"}}}}}},"/api/v1/tags/{id}":{"put":{"responses":{"200":{"description":"Der umbenannte Tag. ACHTUNG: diese Antwort traegt KEIN `created_at` — anders als Liste und Anlage.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"updated_at":{}},"required":["id","name"],"additionalProperties":false},"example":{"id":"string","name":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putApiV1TagsById","tags":["Tags"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rename a tag (updates all customers.tags too)","description":"Benennt das Schlagwort um UND zieht den neuen Namen durch alle `customers.tags`-Listen des Mandanten nach (Vergleich ohne Gross-/Kleinschreibung). Beides laeuft nacheinander, nicht in einer Transaktion: scheitert das Nachziehen bei den Kunden, bleibt die Umbenennung trotzdem stehen und der Aufruf meldet 200. Ein bereits vergebener Name ergibt 409, ein unbekannter Tag 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120}},"required":["name"]},"example":{"name":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung mit der geloeschten Kennung, kein Datensatz.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["deleted","id"],"additionalProperties":false},"example":{"deleted":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1TagsById","tags":["Tags"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete a tag (and remove from all customers.tags)","description":"Entfernt das Schlagwort zuerst aus allen `customers.tags`-Listen des Mandanten (Vergleich ohne Gross-/Kleinschreibung) und loescht dann die Zeile in `tags` endgueltig — kein `deleted_at`, kein Zurueckholen. Die Reihenfolge ist Absicht, aber es ist keine Transaktion: scheitert das Entfernen bei den Kunden, wird der Tag trotzdem geloescht. Ein unbekannter Tag ergibt 404."}},"/api/v1/number-ranges":{"get":{"responses":{"200":{"description":"Alle Nummernkreise, mit gerechneter Vorschau","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Nummernkreises"},"code":{"type":"string","minLength":1,"maxLength":60,"description":"Technischer Schluessel, ueber den andere Routen den Kreis anziehen, etwa invoice_number"},"label":{"type":"string","minLength":1,"maxLength":120,"description":"Anzeigename in den Einstellungen"},"kuerzel":{"type":"string","maxLength":20,"description":"Festes Praefix, etwa RE; LEER, nicht null, wenn keines gesetzt ist"},"freifeld":{"type":"string","maxLength":60,"description":"Mittelteil mit den Platzhaltern {JJJJ} und {MM}; LEER, nicht null, wenn keiner gesetzt ist"},"padding":{"type":"integer","minimum":1,"maximum":10,"description":"Mindestlaenge der Zaehlerziffern"},"next_value":{"type":"integer","minimum":1,"description":"Die naechste freie Nummer in der laufenden Periode"},"reset_yearly":{"type":"boolean","description":"Zaehler springt zum Jahreswechsel auf 1"},"reset_monthly":{"type":"boolean","description":"Zaehler springt zum Monatswechsel auf 1"},"last_reset_year":{"type":["integer","null"],"description":"Jahr des letzten Zaehler-Rueckstellens; null, solange nie zurueckgestellt wurde"},"last_reset_month":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat des letzten Zaehler-Rueckstellens; null, solange nie zurueckgestellt wurde"},"is_system":{"type":"boolean","description":"System-Kreise lassen sich nicht loeschen"},"preview":{"type":"string","description":"Wie die NAECHSTE Nummer aussehen wuerde — bei jeder Antwort neu gerechnet, mit Jahr und Monat von heute"},"last_used_preview":{"type":["string","null"],"description":"Wie die zuletzt vergebene Nummer aussah; null, solange keine vergeben wurde"},"created_at":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updated_at":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","code","label","kuerzel","freifeld","padding","next_value","reset_yearly","reset_monthly","last_reset_year","last_reset_month","is_system","preview","last_used_preview","created_at","updated_at"],"additionalProperties":false},"description":"Alle Nummernkreise des Mandanten, System-Kreise zuerst, dann nach Namen"}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","code":"string","label":"string","kuerzel":"string","freifeld":"string","padding":1,"next_value":1,"reset_yearly":true,"reset_monthly":true,"last_reset_year":0,"last_reset_month":1,"is_system":true,"preview":"string","last_used_preview":"string","created_at":"string","updated_at":"string"}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"503":{"description":"Datenbank nicht erreichbar — als text/plain, mit dem Grund im Text"}},"operationId":"getApiV1Number-ranges","tags":["Number Ranges"],"parameters":[],"summary":"List all number ranges","description":"Listet alle Nummernkreise des Mandanten, System-Kreise zuerst. Beim ersten Zugriff werden Tabelle und System-Kreise angelegt. `preview` und `last_used_preview` sind gerechnete Felder und stehen nicht in der Datenbank — sie enthalten Jahr und Monat von heute und lauten deshalb morgen anders."},"post":{"responses":{"201":{"description":"Angelegt — der neue Nummernkreis, samt gerechneter Vorschau","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Nummernkreises"},"code":{"type":"string","minLength":1,"maxLength":60,"description":"Technischer Schluessel, ueber den andere Routen den Kreis anziehen, etwa invoice_number"},"label":{"type":"string","minLength":1,"maxLength":120,"description":"Anzeigename in den Einstellungen"},"kuerzel":{"type":"string","maxLength":20,"description":"Festes Praefix, etwa RE; LEER, nicht null, wenn keines gesetzt ist"},"freifeld":{"type":"string","maxLength":60,"description":"Mittelteil mit den Platzhaltern {JJJJ} und {MM}; LEER, nicht null, wenn keiner gesetzt ist"},"padding":{"type":"integer","minimum":1,"maximum":10,"description":"Mindestlaenge der Zaehlerziffern"},"next_value":{"type":"integer","minimum":1,"description":"Die naechste freie Nummer in der laufenden Periode"},"reset_yearly":{"type":"boolean","description":"Zaehler springt zum Jahreswechsel auf 1"},"reset_monthly":{"type":"boolean","description":"Zaehler springt zum Monatswechsel auf 1"},"last_reset_year":{"type":["integer","null"],"description":"Jahr des letzten Zaehler-Rueckstellens; null, solange nie zurueckgestellt wurde"},"last_reset_month":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat des letzten Zaehler-Rueckstellens; null, solange nie zurueckgestellt wurde"},"is_system":{"type":"boolean","description":"System-Kreise lassen sich nicht loeschen"},"preview":{"type":"string","description":"Wie die NAECHSTE Nummer aussehen wuerde — bei jeder Antwort neu gerechnet, mit Jahr und Monat von heute"},"last_used_preview":{"type":["string","null"],"description":"Wie die zuletzt vergebene Nummer aussah; null, solange keine vergeben wurde"},"created_at":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updated_at":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","code","label","kuerzel","freifeld","padding","next_value","reset_yearly","reset_monthly","last_reset_year","last_reset_month","is_system","preview","last_used_preview","created_at","updated_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","code":"string","label":"string","kuerzel":"string","freifeld":"string","padding":1,"next_value":1,"reset_yearly":true,"reset_monthly":true,"last_reset_year":0,"last_reset_month":1,"is_system":true,"preview":"string","last_used_preview":"string","created_at":"string","updated_at":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"409":{"description":"Ein Nummernkreis mit diesem Code gibt es bereits — als text/plain"},"503":{"description":"Datenbank nicht erreichbar — als text/plain, mit dem Grund im Text"}},"operationId":"postApiV1Number-ranges","tags":["Number Ranges"],"parameters":[],"summary":"Create a custom number range","description":"Legt einen eigenen Nummernkreis an. Der Code ist mandantenweit eindeutig und lässt sich später nicht mehr ändern — über ihn ziehen andere Routen den Kreis an. Ein so angelegter Kreis ist nie ein System-Kreis und damit löschbar. Ohne Angaben gelten Mindestlänge 4 und nächster Wert 1.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","pattern":"^[a-z0-9_]+$","minLength":1,"maxLength":60},"label":{"type":"string","minLength":1,"maxLength":120},"kuerzel":{"type":"string","maxLength":20},"freifeld":{"type":"string","maxLength":60},"padding":{"type":"integer","minimum":1,"maximum":10},"next_value":{"type":"integer","minimum":1},"reset_yearly":{"type":"boolean"},"reset_monthly":{"type":"boolean"}},"required":["code","label"]}}}}}},"/api/v1/number-ranges/{id}":{"put":{"responses":{"200":{"description":"Geändert — der Nummernkreis nach der Änderung, samt neuer Vorschau","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Nummernkreises"},"code":{"type":"string","minLength":1,"maxLength":60,"description":"Technischer Schluessel, ueber den andere Routen den Kreis anziehen, etwa invoice_number"},"label":{"type":"string","minLength":1,"maxLength":120,"description":"Anzeigename in den Einstellungen"},"kuerzel":{"type":"string","maxLength":20,"description":"Festes Praefix, etwa RE; LEER, nicht null, wenn keines gesetzt ist"},"freifeld":{"type":"string","maxLength":60,"description":"Mittelteil mit den Platzhaltern {JJJJ} und {MM}; LEER, nicht null, wenn keiner gesetzt ist"},"padding":{"type":"integer","minimum":1,"maximum":10,"description":"Mindestlaenge der Zaehlerziffern"},"next_value":{"type":"integer","minimum":1,"description":"Die naechste freie Nummer in der laufenden Periode"},"reset_yearly":{"type":"boolean","description":"Zaehler springt zum Jahreswechsel auf 1"},"reset_monthly":{"type":"boolean","description":"Zaehler springt zum Monatswechsel auf 1"},"last_reset_year":{"type":["integer","null"],"description":"Jahr des letzten Zaehler-Rueckstellens; null, solange nie zurueckgestellt wurde"},"last_reset_month":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat des letzten Zaehler-Rueckstellens; null, solange nie zurueckgestellt wurde"},"is_system":{"type":"boolean","description":"System-Kreise lassen sich nicht loeschen"},"preview":{"type":"string","description":"Wie die NAECHSTE Nummer aussehen wuerde — bei jeder Antwort neu gerechnet, mit Jahr und Monat von heute"},"last_used_preview":{"type":["string","null"],"description":"Wie die zuletzt vergebene Nummer aussah; null, solange keine vergeben wurde"},"created_at":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updated_at":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","code","label","kuerzel","freifeld","padding","next_value","reset_yearly","reset_monthly","last_reset_year","last_reset_month","is_system","preview","last_used_preview","created_at","updated_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","code":"string","label":"string","kuerzel":"string","freifeld":"string","padding":1,"next_value":1,"reset_yearly":true,"reset_monthly":true,"last_reset_year":0,"last_reset_month":1,"is_system":true,"preview":"string","last_used_preview":"string","created_at":"string","updated_at":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"404":{"description":"Nummernkreis nicht gefunden — als text/plain, nicht als JSON"},"503":{"description":"Datenbank nicht erreichbar — als text/plain, mit dem Grund im Text"}},"operationId":"putApiV1Number-rangesById","tags":["Number Ranges"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update a number range","description":"Ändert einen Nummernkreis. Nicht mitgeschickte Felder bleiben, wie sie sind. Der Code lässt sich nicht ändern. System-Kreise sind hier NICHT gesperrt — anders als beim Löschen. ACHTUNG: `next_value` lässt sich frei setzen, auch zurück. Eine bereits vergebene Nummer wird dann ein zweites Mal vergeben; das ist buchhalterisch heikel und wird nicht geprüft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":120},"kuerzel":{"type":"string","maxLength":20},"freifeld":{"type":"string","maxLength":60},"padding":{"type":"integer","minimum":1,"maximum":10},"next_value":{"type":"integer","minimum":1},"reset_yearly":{"type":"boolean"},"reset_monthly":{"type":"boolean"}}},"example":{"label":"string","kuerzel":"string","freifeld":"string","padding":1,"next_value":1,"reset_yearly":true,"reset_monthly":true}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — Quittung mit der Id, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true,"description":"Der Nummernkreis wurde endgueltig geloescht"},"id":{"type":"string","description":"Die Id, die geloescht wurde — wie im Pfad uebergeben"}},"required":["deleted","id"],"additionalProperties":false},"example":{"deleted":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`, oder ein System-Nummernkreis — als text/plain"},"404":{"description":"Nummernkreis nicht gefunden — als text/plain, nicht als JSON"},"503":{"description":"Datenbank nicht erreichbar — als text/plain, mit dem Grund im Text"}},"operationId":"deleteApiV1Number-rangesById","tags":["Number Ranges"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete a custom number range (system locked)","description":"Löscht einen eigenen Nummernkreis endgültig. System-Kreise sind gesperrt (403). Bereits vergebene Nummern bleiben auf ihren Belegen; verloren geht nur der Zählerstand. Zieht eine andere Route den Kreis über seinen Code an, findet sie ihn danach nicht mehr — das wird beim Löschen nicht geprüft."}},"/api/v1/compliance/audit-log":{"get":{"responses":{"200":{"description":"Protokolleinträge — oder leere Liste mit Hinweis, wenn die Tabelle noch fehlt","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Fortlaufende Kennung des Eintrags; BIGSERIAL, kommt als Zeichenkette"},"tenant_id":{"type":["string","null"],"description":"Mandant des Aufrufs; null bei mandantenfreien Aufrufen"},"user_id":{"type":["string","null"],"description":"Benutzer des Aufrufs; null bei Systemaufrufen"},"method":{"type":"string","maxLength":10,"description":"HTTP-Verb des protokollierten Aufrufs"},"path":{"type":"string","minLength":1,"description":"Aufgerufener Pfad"},"body_hash":{"type":"string","minLength":64,"maxLength":64,"description":"Hash des Rumpfs — SHA-256 als 64-stellige Hexadezimalzahl"},"prev_hash":{"type":"string","description":"Signatur des Vorgaengers, verkettet die Eintraege — SHA-256 als 64-stellige Hexadezimalzahl; leer beim ersten"},"signature":{"type":"string","minLength":64,"maxLength":64,"description":"Signatur dieses Eintrags — SHA-256 als 64-stellige Hexadezimalzahl"},"status":{"type":["integer","null"],"description":"HTTP-Status der protokollierten Antwort; null wenn nicht erfasst"},"created_at":{"type":"string","format":"date-time","description":"Zeitpunkt der Protokollierung"}},"required":["id","tenant_id","user_id","method","path","body_hash","prev_hash","signature","status","created_at"],"description":"Ein Eintrag des hashverketteten GoBD-Protokolls"},"description":"Die Eintraege der aktuellen Seite, neueste zuerst"},"meta":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":500,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"}},"required":["limit","offset"],"description":"Seitenangaben"}},"required":["data","meta"]},{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Fortlaufende Kennung des Eintrags; BIGSERIAL, kommt als Zeichenkette"},"tenant_id":{"type":["string","null"],"description":"Mandant des Aufrufs; null bei mandantenfreien Aufrufen"},"user_id":{"type":["string","null"],"description":"Benutzer des Aufrufs; null bei Systemaufrufen"},"method":{"type":"string","maxLength":10,"description":"HTTP-Verb des protokollierten Aufrufs"},"path":{"type":"string","minLength":1,"description":"Aufgerufener Pfad"},"body_hash":{"type":"string","minLength":64,"maxLength":64,"description":"Hash des Rumpfs — SHA-256 als 64-stellige Hexadezimalzahl"},"prev_hash":{"type":"string","description":"Signatur des Vorgaengers, verkettet die Eintraege — SHA-256 als 64-stellige Hexadezimalzahl; leer beim ersten"},"signature":{"type":"string","minLength":64,"maxLength":64,"description":"Signatur dieses Eintrags — SHA-256 als 64-stellige Hexadezimalzahl"},"status":{"type":["integer","null"],"description":"HTTP-Status der protokollierten Antwort; null wenn nicht erfasst"},"created_at":{"type":"string","format":"date-time","description":"Zeitpunkt der Protokollierung"}},"required":["id","tenant_id","user_id","method","path","body_hash","prev_hash","signature","status","created_at"],"description":"Ein Eintrag des hashverketteten GoBD-Protokolls"},"maxItems":0,"description":"Immer leer — es gibt noch keine Tabelle"},"meta":{"type":"object","properties":{"note":{"type":"string","minLength":1,"description":"Klartext: die Tabelle entsteht beim ersten schreibenden Aufruf"}},"required":["note"],"description":"Hinweis statt Seitenangaben"}},"required":["data","meta"]}]},"example":{"data":[{"id":"string","tenant_id":"string","user_id":"string","method":"string","path":"string","body_hash":"stringxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","prev_hash":"string","signature":"stringxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","status":0,"created_at":"2026-01-01T12:00:00.000Z"}],"meta":{"limit":1,"offset":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Admin-Rolle"},"503":{"description":"Datenbank nicht verfügbar (Klartext)"}},"operationId":"getApiV1ComplianceAudit-log","tags":["compliance"],"parameters":[{"in":"query","name":"from","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"in":"query","name":"to","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"in":"query","name":"method","schema":{"type":"string"}},{"in":"query","name":"pathPrefix","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"summary":"List GoBD audit-log entries","description":"Listet das hashverkettete GoBD-Protokoll. Die Zeilen kommen roh aus der Tabelle, daher snake_case. Existiert die Tabelle noch nicht, kommt 200 mit leerer Liste und einem Hinweis in `meta.note` statt der Seitenangaben — dann fehlen `meta.limit` und `meta.offset`."}},"/api/v1/compliance/audit-log/verify":{"get":{"responses":{"200":{"description":"Prüfergebnis — oder der Sonderfall ohne Tabelle","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"totalRecords":{"type":"integer","minimum":0,"description":"Anzahl gepruefter Eintraege"},"chainValid":{"type":"boolean","description":"true, wenn Verkettung UND Signaturen stimmen"},"brokenRecordIds":{"type":"array","items":{"type":"string","minLength":1},"description":"Kennungen der beanstandeten Eintraege, je Eintrag einmal; leer wenn die Kette haelt"},"legacyUnverifiable":{"type":"integer","minimum":0,"description":"Alt-Eintraege ohne gespeicherten Signatur-Zeitstempel. Bei ihnen wurde NUR die Verkettung und die Laenge geprueft, nicht der Inhalt."},"note":{"type":"string","minLength":1,"description":"Ergebnis im Klartext"}},"required":["totalRecords","chainValid","brokenRecordIds","legacyUnverifiable","note"]},{"type":"object","properties":{"totalRecords":{"type":"number","const":0,"description":"Es gibt keine Eintraege"},"chainValid":{"type":"boolean","const":true,"description":"true, weil nichts zu pruefen war — nicht, weil etwas geprueft wurde"},"note":{"type":"string","minLength":1,"description":"Klartext: keine Eintraege"}},"required":["totalRecords","chainValid","note"]}]},"example":{"totalRecords":0,"chainValid":true,"brokenRecordIds":["string"],"legacyUnverifiable":0,"note":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Admin-Rolle"},"503":{"description":"Datenbank nicht verfügbar (Klartext)"}},"operationId":"getApiV1ComplianceAudit-logVerify","tags":["compliance"],"parameters":[],"summary":"Verify GoBD hash-chain integrity","description":"Rechnet die Hash-Kette des GoBD-Protokolls nach. Geprüft werden Verkettung, Signaturlänge und — sofern der Signatur-Zeitstempel gespeichert ist — die Inhalts-Signatur. Existiert die Tabelle noch nicht, kommt 200 mit `chainValid: true` und `totalRecords: 0`; die Felder `brokenRecordIds` und `legacyUnverifiable` fehlen dann. Das heißt „nichts geprüft\", nicht „Kette in Ordnung\"."}},"/api/v1/dashboard/daily-briefing":{"get":{"responses":{"200":{"description":"Kennzahlen, Zusammenfassung und Tagesumsatz","content":{"application/json":{"schema":{"type":"object","properties":{"generatedAt":{"type":"string","description":"Zeitpunkt der Antwort, ISO 8601"},"summary":{"type":"string","description":"Ein Satz auf Deutsch — vom Sprachmodell oder aus der Vorlage"},"counts":{"type":"object","properties":{"overdueInvoices":{"type":"integer"},"openInvoices":{"type":"integer"},"newLeads24h":{"type":"integer"},"activeProjects":{"type":"integer"},"pendingApprovals":{"type":"integer"}},"required":["overdueInvoices","openInvoices","newLeads24h","activeProjects","pendingApprovals"]},"money":{"type":"object","properties":{"todayInvoiceTotal":{"type":"number","description":"Summe der heute erstellten Rechnungen ohne Entwurf/storniert"},"currency":{"type":"string","const":"EUR"}},"required":["todayInvoiceTotal","currency"]}},"required":["generatedAt","summary","counts","money"]},"example":{"generatedAt":"string","summary":"string","counts":{"overdueInvoices":0,"openInvoices":0,"newLeads24h":0,"activeProjects":0,"pendingApprovals":0},"money":{"todayInvoiceTotal":0,"currency":"EUR"}}}}},"401":{"description":"Kein Mandantenkontext"},"503":{"description":"Eine tragende Rechnungszahl war nicht ermittelbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1DashboardDaily-briefing","tags":["dashboard"],"parameters":[],"summary":"Daily briefing aggregate for the dashboard widget","description":"Zählt die Kennzahlen für die Dashboard-Kachel in einem Durchgang zusammen: überfällige und offene Rechnungen, neue Leads der letzten 24 Stunden, laufende Projekte, offene Freigaben und den heutigen Rechnungsumsatz. Dazu kommt ein einzelner Satz als Zusammenfassung — er stammt vom Sprachmodell, liegt je Mandant eine Stunde im Zwischenspeicher, und bei fehlendem Anbieter oder Fehler tritt ein aus denselben Zahlen gebauter Satz an seine Stelle. Scheitert eine der beiden Rechnungszahlen, antwortet der Endpunkt mit 503, statt eine falsche Entwarnung zu melden; nur eine noch gar nicht angelegte Tabelle zählt als echte 0, ebenso fehlende Kunden-, Projekt- und Freigabetabellen."}},"/api/v1/customers/health":{"get":{"responses":{"200":{"description":"Die berechneten Kennzahlen, schlechtester Punktwert zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid","description":"Kennung des Kunden (UUID)"},"customerName":{"type":"string","description":"Name des Kunden, wie er in der Kundentabelle steht"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Punktwert von 0 bis 100. Start sind 100 Punkte, Abzuege stehen in `reasons`"},"rating":{"type":"string","enum":["excellent","good","warning","critical"],"description":"Einstufung des Punktwerts: ab 80 `excellent`, ab 60 `good`, ab 40 `warning`, darunter `critical`"},"daysSinceLastActivity":{"type":["integer","null"],"description":"Tage seit der letzten Aenderung an Kunde, Rechnung oder Auftrag. Achtung: gab es NIE eine, rechnet der Server ab dem 1.1.1970 — dann steht hier eine sehr grosse Zahl, nicht `null`"},"overdueInvoicesCount":{"type":"integer","minimum":0,"description":"Anzahl offener Rechnungen mit ueberschrittenem Faelligkeitsdatum"},"openInvoiceTotal":{"type":"number","description":"Summe der offenen Rechnungsbetraege abzueglich Teilzahlungen, in Euro. Nur Anzeige — ohne Einfluss auf den Punktwert"},"revenueLast90d":{"type":"number","description":"Rechnungssumme der letzten 90 Tage in Euro"},"revenuePrev90d":{"type":"number","description":"Rechnungssumme der 90 Tage davor in Euro — die Vergleichsgroesse"},"trend":{"type":"string","enum":["up","down","flat"],"description":"`up` ab 10 Prozent ueber dem Vorzeitraum, `down` bei weniger als der Haelfte (und nur, wenn im Vorzeitraum ueberhaupt Umsatz war), sonst `flat`"},"reasons":{"type":"array","items":{"type":"string"},"description":"Deutsche Klartext-Gruende fuer die Abzuege. Leere Liste heisst: keine Auffaelligkeit — nicht jeder Abzug erzeugt einen Grund, kleine Abzuege bleiben stumm"}},"required":["customerId","customerName","score","rating","daysSinceLastActivity","overdueInvoicesCount","openInvoiceTotal","revenueLast90d","revenuePrev90d","trend","reasons"],"additionalProperties":false},"maxItems":200,"description":"Die Kunden, schlechtester Punktwert zuerst. Hoechstens 200, ohne Moeglichkeit zu blaettern"},"meta":{"type":"object","properties":{"totalCustomers":{"type":"integer","minimum":0,"maximum":200,"description":"Laenge von `data` — NICHT die Zahl der Kunden im Mandanten. Kunden, deren Berechnung scheiterte, fehlen hier stillschweigend"}},"required":["totalCustomers"],"additionalProperties":false,"description":"Angaben zur Abfrage"}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"customerId":"00000000-0000-4000-8000-000000000000","customerName":"string","score":0,"rating":"excellent","daysSinceLastActivity":0,"overdueInvoicesCount":0,"openInvoiceTotal":0,"revenueLast90d":0,"revenuePrev90d":0,"trend":"up","reasons":["string"]}],"meta":{"totalCustomers":0}}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext `tenant context missing`, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client vorhanden — Klartext, kein JSON"}},"operationId":"getApiV1CustomersHealth","tags":["CRM · Health"],"parameters":[],"summary":"List health scores for all customers","description":"Berechnet fuer bis zu 200 Kunden (Art `customer` oder `partner`) einen Punktwert von 0 bis 100 und liefert sie schlechtester zuerst. Die Grenze von 200 ist fest und NICHT verschiebbar — es gibt keine Blaetterung. Scheitert die Berechnung fuer einen Kunden, faellt er stillschweigend heraus; `meta.totalCustomers` zaehlt deshalb nur die gelieferten Zeilen."}},"/api/v1/customers/{id}/health":{"get":{"responses":{"200":{"description":"Die Kennzahlen des Kunden","content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid","description":"Kennung des Kunden (UUID)"},"customerName":{"type":"string","description":"Name des Kunden, wie er in der Kundentabelle steht"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Punktwert von 0 bis 100. Start sind 100 Punkte, Abzuege stehen in `reasons`"},"rating":{"type":"string","enum":["excellent","good","warning","critical"],"description":"Einstufung des Punktwerts: ab 80 `excellent`, ab 60 `good`, ab 40 `warning`, darunter `critical`"},"daysSinceLastActivity":{"type":["integer","null"],"description":"Tage seit der letzten Aenderung an Kunde, Rechnung oder Auftrag. Achtung: gab es NIE eine, rechnet der Server ab dem 1.1.1970 — dann steht hier eine sehr grosse Zahl, nicht `null`"},"overdueInvoicesCount":{"type":"integer","minimum":0,"description":"Anzahl offener Rechnungen mit ueberschrittenem Faelligkeitsdatum"},"openInvoiceTotal":{"type":"number","description":"Summe der offenen Rechnungsbetraege abzueglich Teilzahlungen, in Euro. Nur Anzeige — ohne Einfluss auf den Punktwert"},"revenueLast90d":{"type":"number","description":"Rechnungssumme der letzten 90 Tage in Euro"},"revenuePrev90d":{"type":"number","description":"Rechnungssumme der 90 Tage davor in Euro — die Vergleichsgroesse"},"trend":{"type":"string","enum":["up","down","flat"],"description":"`up` ab 10 Prozent ueber dem Vorzeitraum, `down` bei weniger als der Haelfte (und nur, wenn im Vorzeitraum ueberhaupt Umsatz war), sonst `flat`"},"reasons":{"type":"array","items":{"type":"string"},"description":"Deutsche Klartext-Gruende fuer die Abzuege. Leere Liste heisst: keine Auffaelligkeit — nicht jeder Abzug erzeugt einen Grund, kleine Abzuege bleiben stumm"}},"required":["customerId","customerName","score","rating","daysSinceLastActivity","overdueInvoicesCount","openInvoiceTotal","revenueLast90d","revenuePrev90d","trend","reasons"],"additionalProperties":false},"example":{"customerId":"00000000-0000-4000-8000-000000000000","customerName":"string","score":0,"rating":"excellent","daysSinceLastActivity":0,"overdueInvoicesCount":0,"openInvoiceTotal":0,"revenueLast90d":0,"revenuePrev90d":0,"trend":"up","reasons":["string"]}}}},"400":{"description":"Mandanten-Slug passt nicht zum erlaubten Muster — Klartext, kein JSON"},"401":{"description":"Kein Mandantenkontext — Klartext `tenant context missing`, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `user`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Kunde mit dieser Kennung — Klartext `customer not found`, kein JSON"},"503":{"description":"Kein Datenbank-Client vorhanden — Klartext, kein JSON"}},"operationId":"getApiV1CustomersByIdHealth","tags":["CRM · Health"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Health score for one customer","description":"Berechnet den Punktwert eines einzelnen Kunden und liefert die Kennzahlen OHNE Umschlag — die Felder stehen direkt im Wurzelobjekt, anders als in der Listenfassung. Der Wert wird bei JEDEM Aufruf frisch gerechnet und nirgends gespeichert; zwei Aufrufe an verschiedenen Tagen koennen also abweichen, ohne dass sich am Kunden etwas geaendert hat. Geloeschte Kunden gelten als nicht vorhanden."}},"/api/v1/bulk/{entity}/update":{"post":{"responses":{"200":{"description":"Geaenderte Zeilen und die ignorierten Spalten","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer","minimum":0,"description":"Anzahl tatsaechlich geaenderter Zeilen"},"ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"IDs der geaenderten Zeilen — kann kuerzer sein als die Anfrage"},"rejectedColumns":{"type":"array","items":{"type":"string"},"description":"Schluessel aus `set`, die nicht auf der Freigabeliste der Entitaet stehen und deshalb nicht geschrieben wurden"}},"required":["updated","ids","rejectedColumns"]},"example":{"updated":0,"ids":["00000000-0000-4000-8000-000000000000"],"rejectedColumns":["string"]}}}},"400":{"description":"Unbekannte Entitaet oder keine erlaubte Spalte in `set` (Klartext)"},"401":{"description":"Kein Mandantenkontext (Klartext)"},"403":{"description":"Rolle unter `manager`"},"409":{"description":"Festgeschriebene Belege — GoBD, mit den betroffenen IDs im Klartext"}},"operationId":"postApiV1BulkByEntityUpdate","tags":["bulk"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true}],"summary":"Mass-update fields on multiple records","description":"Schreibt die Schluessel aus `set` in EINER UPDATE-Anweisung auf alle `ids` und setzt `updated_at` mit. Geschrieben wird nur, was auf der Freigabeliste der Entitaet steht; alles uebrige kommt als `rejectedColumns` zurueck, und enthaelt `set` keine einzige erlaubte Spalte, endet der Aufruf mit 400. Bei `invoices` prueft ein GoBD-Riegel einen Statuswechsel vorher gegen dieselbe Uebergangsmatrix wie der Einzelweg und lehnt festgeschriebene Belege mit 409 ab. Hoechstens 500 IDs je Aufruf, Rolle mindestens `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"set":{"type":"object","additionalProperties":{}}},"required":["ids","set"]},"example":{"ids":["00000000-0000-4000-8000-000000000000"],"set":{}}}}}}},"/api/v1/bulk/{entity}/delete":{"post":{"responses":{"200":{"description":"Geloeschte Zeilen — `hardDeleted` oder `softDeleted`, je nach Weg","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"softDeleted":{"type":"integer","minimum":0,"description":"Anzahl weich geloeschter Zeilen"},"ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"IDs der weich geloeschten Zeilen"}},"required":["softDeleted","ids"]},{"type":"object","properties":{"hardDeleted":{"type":"integer","minimum":0,"description":"Anzahl physisch geloeschter Zeilen"},"ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"IDs der physisch geloeschten Zeilen"}},"required":["hardDeleted","ids"]}],"description":"Der Schluessel sagt, welcher Weg gegangen wurde: `hard=true` liefert `hardDeleted`, sonst `softDeleted`"},"example":{"softDeleted":0,"ids":["00000000-0000-4000-8000-000000000000"]}}}},"400":{"description":"Unbekannte Entitaet oder kein Soft-Delete moeglich (Klartext)"},"401":{"description":"Kein Mandantenkontext (Klartext)"},"403":{"description":"Rolle unter `manager`, oder `hard=true` ohne Rolle `admin`"},"409":{"description":"Festgeschriebene Belege — GoBD, mit den betroffenen IDs im Klartext"}},"operationId":"postApiV1BulkByEntityDelete","tags":["bulk"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true}],"summary":"Mass soft-delete records (admin can hard-delete)","description":"Setzt `deleted_at` und `updated_at` auf allen `ids`. Bereits weich geloeschte Zeilen bleiben unberuehrt und zaehlen nicht mit — der Aufruf ist damit wiederholbar. Mit `hard: true` wird stattdessen physisch geloescht; das verlangt die Rolle `admin`, sonst 403. Belege sind auf BEIDEN Wegen geschuetzt: bei `invoices` kommt alles ausser einem nicht festgeschriebenen Entwurf mit 409 zurueck, weil auch das weiche Loeschen den Beleg aus den Buechern nimmt. Fehlt der Entitaet eine Soft-Delete-Spalte, antwortet die Route mit 400. Hoechstens 500 IDs je Aufruf, Rolle mindestens `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"hard":{"type":"boolean","default":false}},"required":["ids"]},"example":{"ids":["00000000-0000-4000-8000-000000000000"],"hard":true}}}}}},"/api/v1/bulk/{entity}/tag":{"post":{"responses":{"200":{"description":"Geaenderte Zeilen samt angewandtem Modus","content":{"application/json":{"schema":{"type":"object","properties":{"tagged":{"type":"integer","minimum":0,"description":"Anzahl geaenderter Zeilen"},"ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"IDs der geaenderten Zeilen"},"tags":{"type":"array","items":{"type":"string"},"description":"Die uebergebenen Marken, unveraendert zurueckgespiegelt"},"mode":{"type":"string","enum":["append","replace"],"description":"Der angewandte Modus — bei fehlender Angabe `append`"}},"required":["tagged","ids","tags","mode"]},"example":{"tagged":0,"ids":["00000000-0000-4000-8000-000000000000"],"tags":["string"],"mode":"append"}}}},"400":{"description":"Unbekannte Entitaet oder Entitaet ohne Marken-Spalte (Klartext)"},"401":{"description":"Kein Mandantenkontext (Klartext)"},"403":{"description":"Rolle unter `manager`"}},"operationId":"postApiV1BulkByEntityTag","tags":["bulk"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entity","required":true}],"summary":"Add or replace tags on multiple records","description":"Schreibt die Marken-Spalte der Entitaet und setzt `updated_at` mit. `mode: \"append\"` (Vorgabe) vereinigt die neuen Marken mit den vorhandenen und entfernt dabei Dubletten; `mode: \"replace\"` ersetzt die Liste vollstaendig. Entitaeten ohne Marken-Spalte — heute alles ausser `customers` — antworten mit 400, bevor geschrieben wird. Je Aufruf hoechstens 500 IDs und 20 Marken zu je 40 Zeichen, Rolle mindestens `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":40},"minItems":1,"maxItems":20},"mode":{"type":"string","enum":["append","replace"],"default":"append"}},"required":["ids","tags"]},"example":{"ids":["00000000-0000-4000-8000-000000000000"],"tags":["string"],"mode":"append"}}}}}},"/api/v1/recurring-invoices":{"get":{"responses":{"200":{"description":"Alle Serienrechnungen; ohne `notes`","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Vorlage"},"customer_id":{"type":"string","format":"uuid","description":"Kunde, fuer den fakturiert wird"},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Bezeichnung der Serie"},"positions":{"type":"array","items":{},"description":"Die Positionen, die jede erzeugte Rechnung erhaelt"},"interval_type":{"type":"string","description":"Rhythmus: weekly, monthly, quarterly oder yearly"},"interval_count":{"type":"integer","minimum":1,"maximum":12,"description":"Vielfaches des Rhythmus, etwa 2 bei zweimonatlich"},"next_run_at":{"type":"string","description":"Naechster Faelligkeitstag; Datum, je nach Treiber als YYYY-MM-DD oder voller ISO-Zeitstempel"},"last_run_at":{"type":["string","null"],"description":"Letzter Lauf; null, solange die Serie nie gelaufen ist"},"run_count":{"type":"integer","minimum":0,"description":"Wie oft die Serie bereits Rechnungen erzeugt hat"},"max_runs":{"type":["integer","null"],"minimum":1,"description":"Obergrenze der Laeufe; null = unbegrenzt"},"status":{"type":"string","description":"active, paused, completed oder cancelled"},"due_days":{"type":"integer","minimum":1,"maximum":180,"description":"Zahlungsziel der erzeugten Rechnungen in Tagen"},"created_at":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updated_at":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","customer_id","name","positions","interval_type","interval_count","next_run_at","last_run_at","run_count","max_runs","status","due_days","created_at","updated_at"],"additionalProperties":false},"description":"Alle Serienrechnungen des Mandanten, nach Status und naechstem Termin sortiert"}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","customer_id":"00000000-0000-4000-8000-000000000000","name":"string","positions":[],"interval_type":"string","interval_count":1,"next_run_at":"string","last_run_at":"string","run_count":0,"max_runs":1,"status":"string","due_days":1,"created_at":"string","updated_at":"string"}]}}}},"400":{"description":"Ungültiger Mandanten-Slug — als text/plain"},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext — als text/plain"},"403":{"description":"Rolle unterhalb `user`"},"503":{"description":"Kein Datenbank-Client vorhanden — als text/plain"}},"operationId":"getApiV1Recurring-invoices","tags":["Recurring Invoices"],"parameters":[],"summary":"List recurring invoice templates","description":"Listet alle Serienrechnungen des Mandanten, sortiert nach Status und nächstem Termin. Es wird nicht geblättert und nicht gefiltert. Die Felder kommen in snake_case zurück, anders als sie beim Anlegen heißen — und `notes` ist NICHT dabei, obwohl es sich schreiben lässt."},"post":{"responses":{"201":{"description":"Angelegt — nur die Id, nicht der Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der angelegten bzw. geaenderten Vorlage"}},"required":["id"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext — als text/plain"},"403":{"description":"Rolle unterhalb `user`"},"503":{"description":"Kein Datenbank-Client vorhanden — als text/plain"}},"operationId":"postApiV1Recurring-invoices","tags":["Recurring Invoices"],"parameters":[],"summary":"Create recurring invoice template","description":"Legt eine Serienrechnung an. Die Antwort enthält NUR die neue Id — für den ganzen Datensatz die Liste nachladen. Es wird nicht geprüft, ob es den Kunden gibt; ein erfundener Kunde fällt erst beim Serienlauf auf. Der erste Termin darf in der Vergangenheit liegen — der nächste Lauf holt ihn dann nach.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":200},"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string","maxLength":500,"default":""},"quantity":{"type":"number","minimum":0},"unitPrice":{"type":"number","minimum":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19}},"required":["quantity","unitPrice"]},"minItems":1,"maxItems":1000},"intervalType":{"type":"string","enum":["weekly","monthly","quarterly","yearly"]},"intervalCount":{"type":"integer","minimum":1,"maximum":12,"default":1},"nextRunAt":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"maxRuns":{"type":"integer","minimum":1},"dueDays":{"type":"integer","minimum":1,"maximum":180,"default":14},"status":{"type":"string","enum":["active","paused","completed","cancelled"],"default":"active"},"notes":{"type":"string","maxLength":2000}},"required":["customerId","name","positions","intervalType","nextRunAt"]},"example":{"customerId":"00000000-0000-4000-8000-000000000000","name":"string","positions":[{"title":"string","description":"string","quantity":0,"unitPrice":0,"taxRate":0}],"intervalType":"weekly","intervalCount":1,"nextRunAt":"2026-01-01","maxRuns":1,"dueDays":1,"status":"active","notes":"string"}}}}}},"/api/v1/recurring-invoices/{id}":{"put":{"responses":{"200":{"description":"Geändert — nur die Id, nicht der Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der angelegten bzw. geaenderten Vorlage"}},"required":["id"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext — als text/plain"},"403":{"description":"Rolle unterhalb `user`"},"404":{"description":"Serienrechnung nicht gefunden — als text/plain, nicht als JSON"},"503":{"description":"Kein Datenbank-Client vorhanden — als text/plain"}},"operationId":"putApiV1Recurring-invoicesById","tags":["Recurring Invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update recurring invoice template","description":"Ersetzt eine Serienrechnung vollständig — trotz PUT werden ALLE Felder geschrieben, nicht mitgeschickte fallen auf ihren Vorgabewert zurück. Der Lauf-Zähler und der letzte Lauf bleiben davon unberührt. Die Antwort enthält nur die Id. Bereits erzeugte Rechnungen ändern sich nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":200},"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string","maxLength":500,"default":""},"quantity":{"type":"number","minimum":0},"unitPrice":{"type":"number","minimum":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19}},"required":["quantity","unitPrice"]},"minItems":1,"maxItems":1000},"intervalType":{"type":"string","enum":["weekly","monthly","quarterly","yearly"]},"intervalCount":{"type":"integer","minimum":1,"maximum":12,"default":1},"nextRunAt":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"maxRuns":{"type":"integer","minimum":1},"dueDays":{"type":"integer","minimum":1,"maximum":180,"default":14},"status":{"type":"string","enum":["active","paused","completed","cancelled"],"default":"active"},"notes":{"type":"string","maxLength":2000}},"required":["customerId","name","positions","intervalType","nextRunAt"]},"example":{"customerId":"00000000-0000-4000-8000-000000000000","name":"string","positions":[{"title":"string","description":"string","quantity":0,"unitPrice":0,"taxRate":0}],"intervalType":"weekly","intervalCount":1,"nextRunAt":"2026-01-01","maxRuns":1,"dueDays":1,"status":"active","notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Quittung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true,"description":"Die Vorlage wurde endgueltig geloescht"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":true}}}},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext — als text/plain"},"403":{"description":"Rolle unterhalb `admin`"},"404":{"description":"Serienrechnung nicht gefunden — als text/plain, nicht als JSON"},"503":{"description":"Kein Datenbank-Client vorhanden — als text/plain"}},"operationId":"deleteApiV1Recurring-invoicesById","tags":["Recurring Invoices"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete recurring invoice template","description":"Löscht eine Serienrechnung endgültig — kein Soft-Delete, kein Wiederherstellen. Bereits erzeugte Rechnungen bleiben bestehen. Soll die Serie nur ruhen, ist der Status `paused` der richtige Weg. Nur ab Rolle `admin`."}},"/api/v1/recurring-invoices/run":{"post":{"responses":{"200":{"description":"Ergebnis des Laufs. `processed` und `created` gehen auseinander, sobald eine Serie scheitert — dann steht der Grund in `errors`.","content":{"application/json":{"schema":{"type":"object","properties":{"processed":{"type":"integer","minimum":0,"maximum":200,"description":"Faellige Vorlagen, die angefasst wurden; hoechstens 200 je Lauf"},"created":{"type":"integer","minimum":0,"description":"Davon wirklich erzeugte Rechnungen"},"errors":{"type":"array","items":{"type":"string"},"description":"Je gescheiterter Vorlage eine Zeile aus Id und Grund; leer, wenn alles durchlief"}},"required":["processed","created","errors"],"additionalProperties":false},"example":{"processed":0,"created":0,"errors":["string"]}}}},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext — als text/plain"},"403":{"description":"Rolle unterhalb `admin`"},"503":{"description":"Kein Datenbank-Client vorhanden — als text/plain"}},"operationId":"postApiV1Recurring-invoicesRun","tags":["Recurring Invoices"],"parameters":[],"summary":"Process all due recurring invoices","description":"Erzeugt für jede fällige Serie eine Rechnung im Status `open` und setzt den nächsten Termin weiter. Höchstens 200 Serien je Lauf. Der Aufruf ist NICHT idempotent: zweimal gerufen erzeugt er zweimal Rechnungen, sofern der Termin wieder erreicht ist. Eine gescheiterte Serie bricht den Lauf nicht ab, sie landet in `errors` — die Antwort ist auch dann 200, wenn KEINE einzige Rechnung entstanden ist. Die Rechnungsnummern kommen aus dem atomaren Nummernkreis; scheitert das Einfügen nach der Nummernvergabe, entsteht eine dokumentierte Lücke (Protokoll), die nicht zurückgenommen wird. Nur ab Rolle `admin`."}},"/api/v1/customers/{id}/statement":{"get":{"responses":{"200":{"description":"Der Kontoauszug. Welcher der beiden Medientypen kommt, entscheidet der Parameter `format`.","content":{"application/json":{"schema":{"type":"object","properties":{"customer":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Kunden"},"name":{"type":"string","description":"Name des Kunden"},"address":{"type":"object","additionalProperties":{},"description":"Anschrift als Objekt; leeres Objekt, wenn keine hinterlegt ist"}},"required":["id","name","address"],"additionalProperties":false,"description":"Der Kunde — nur diese drei Felder, nicht der volle Kundendatensatz"},"period":{"type":"object","properties":{"from":{"type":"string","description":"Beginn des Zeitraums (ISO-Datum), wie angefragt"},"to":{"type":"string","description":"Ende des Zeitraums (ISO-Datum), wie angefragt"}},"required":["from","to"],"additionalProperties":false,"description":"Der ausgewertete Zeitraum"},"openingBalance":{"type":"number","description":"Saldo VOR dem Zeitraum: offene Betraege aller Rechnungen, die davor angelegt wurden"},"closingBalance":{"type":"number","description":"Saldo nach der letzten Zeile"},"totalInvoiced":{"type":"number","description":"Summe der Rechnungen im Zeitraum"},"totalPaid":{"type":"number","description":"Summe der Zahlungen im Zeitraum"},"lines":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","description":"Datum der Zeile (ISO-Datum, ohne Uhrzeit)"},"type":{"type":"string","enum":["invoice","payment"],"description":"Rechnung erhoeht den Saldo, Zahlung senkt ihn"},"reference":{"type":"string","description":"Belegnummer; bei einer Zahlung der Text „Zahlung zu\" samt Rechnungsnummer"},"amount":{"type":"number","description":"Betrag der Zeile, immer positiv — die Richtung sagt `type`"},"balance":{"type":"number","description":"Laufender Saldo NACH dieser Zeile. ACHTUNG: er wird je Rechnung samt zugehoeriger Zahlung gerechnet und die Liste ERST DANACH nach Datum sortiert — bei mehreren Rechnungen im Zeitraum steigt er in der gelieferten Reihenfolge nicht zwingend monoton"}},"required":["date","type","reference","amount","balance"],"additionalProperties":false},"description":"Die Bewegungen, nach Datum sortiert"}},"required":["customer","period","openingBalance","closingBalance","totalInvoiced","totalPaid","lines"],"additionalProperties":false},"example":{"customer":{"id":"00000000-0000-4000-8000-000000000000","name":"string","address":{}},"period":{"from":"string","to":"string"},"openingBalance":0,"closingBalance":0,"totalInvoiced":0,"totalPaid":0,"lines":[{"date":"string","type":"invoice","reference":"string","amount":0,"balance":0}]}},"text/html":{}}},"400":{"description":"Ungültige Query-Parameter oder ungültiger Mandanten-Slug"},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext — als text/plain"},"403":{"description":"Rolle unterhalb `user`"},"404":{"description":"Kunde nicht gefunden — als text/plain, nicht als JSON"},"503":{"description":"Kein Datenbank-Client vorhanden — als text/plain"}},"operationId":"getApiV1CustomersByIdStatement","tags":["CRM · Statement"],"parameters":[{"in":"query","name":"from","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"required":true},{"in":"query","name":"to","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"required":true},{"in":"query","name":"format","schema":{"type":"string","enum":["json","html"],"default":"json"},"required":false},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Customer statement (Konto-Auszug) for a date range","description":"Kontoauszug eines Kunden für einen Zeitraum. `format=html` liefert eine fertige Druckseite als text/html, sonst kommt JSON — beides mit Statuscode 200, der Medientyp ist der Unterschied. Zahlungen sind keine eigenen Datensätze, sie werden aus Zahlbetrag und Zahldatum der Rechnung abgeleitet; je Rechnung erscheint deshalb höchstens EINE Zahlungszeile. Die Rechnungen werden nach ihrem ANLAGEDATUM in den Zeitraum einsortiert, nicht nach Rechnungs- oder Fälligkeitsdatum."}},"/api/v1/customers/{id}/portal/share-document":{"post":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1CustomersByIdPortalShare-document","tags":["Customer-Portal","admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Dokument für Portal-Kunde freigeben","description":"Macht ein Dokument im Kundenportal dieses Kunden sichtbar. Der Eintrag ist je Dokument und Kunde eindeutig; eine wiederholte Freigabe erneuert nur Zeitpunkt, Freigeber und Notiz und legt keinen zweiten Eintrag an. Antwortet 201 mit `shared: true`; eine Kunden-Kennung, die keine UUID ist, ergibt 400 `invalid_customer_id`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"documentId":{"type":"string","format":"uuid"},"note":{"type":"string","maxLength":500}},"required":["documentId"]},"example":{"documentId":"00000000-0000-4000-8000-000000000000","note":"string"}}}}}},"/api/v1/customers/{id}/portal/share-document/{documentId}":{"delete":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1CustomersByIdPortalShare-documentByDocumentId","tags":["Customer-Portal","admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"documentId","required":true}],"summary":"Dokument für Portal-Kunde wieder verbergen","description":"Zieht die Portal-Freigabe eines Dokuments für diesen Kunden zurück. Das Dokument selbst bleibt im DMS unberührt, nur der Sichtbarkeits-Eintrag verschwindet. Antwortet auch dann 200 `revoked: true`, wenn gar keine Freigabe bestand; Kennungen, die keine UUID sind, ergeben 400 `invalid_id`."}},"/api/v1/customers/{id}/portal/uploads":{"get":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1CustomersByIdPortalUploads","tags":["Customer-Portal","admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Portal-Inbox: was hat der Kunde hochgeladen?","description":"Liefert die letzten 200 Dateien, die dieser Kunde über das Portal hochgeladen hat, neueste zuerst, mit Status (`pending`, `imported`, `rejected`), Dateiname, Größe und der Kennung des ggf. daraus importierten Dokuments. Ein Datenbankfehler wird als 500 `query_failed` gemeldet und nie als leere Liste kaschiert."}},"/api/v1/customers/{id}/portal/uploads/{uploadId}":{"patch":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"patchApiV1CustomersByIdPortalUploadsByUploadId","tags":["Customer-Portal","admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"uploadId","required":true}],"summary":"Portal-Upload als imported/rejected markieren","description":"Ändert Status, importierte Dokument-Kennung oder Notiz eines Kunden-Uploads; nur mitgeschickte Felder werden geschrieben. Ein leerer Rumpf ist 400 `no_fields_to_update`. Die Datei wird dabei weder verschoben noch ins DMS übernommen, die Route vermerkt nur das Ergebnis. Antwortet 200 `ok: true` auch dann, wenn keine Zeile zu Upload und Kunde passt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["pending","imported","rejected"]},"importedDocumentId":{"type":"string","format":"uuid"},"note":{"type":"string","maxLength":500}}},"example":{"status":"pending","importedDocumentId":"00000000-0000-4000-8000-000000000000","note":"string"}}}}}},"/api/v1/customers/{id}/portal/audit-log":{"get":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1CustomersByIdPortalAudit-log","tags":["Customer-Portal","admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Portal-Audit-Log für einen Kunden","description":"Liefert die letzten 500 Portal-Ereignisse dieses Kunden (Aktion, Zielobjekt, IP, User-Agent, Zusatzdaten), neueste zuerst. Nur lesend, es wird nichts vermerkt. Ein Datenbankfehler wird als 500 `query_failed` gemeldet, eine Kunden-Kennung ohne UUID-Form als 400 `invalid_customer_id`."}},"/api/v1/settings/branding":{"get":{"responses":{"200":{"description":"Branding-Einstellungen des Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"brand_logo_url":{"type":["string","null"]},"brand_color":{"type":["string","null"]},"imprint_vat_id":{"type":["string","null"]},"imprint_trade_register":{"type":["string","null"]},"imprint_ceo_name":{"type":["string","null"]},"website":{"type":["string","null"]},"phone":{"type":["string","null"]},"tax_number":{"type":["string","null"]},"pdf_design_id":{"type":["string","null"]},"pdf_design_quote":{"type":["string","null"]},"pdf_design_order":{"type":["string","null"]},"pdf_design_delivery":{"type":["string","null"]},"pdf_design_invoice":{"type":["string","null"]},"has_custom_template":{"type":"boolean"},"bank_iban":{"type":["string","null"]},"bank_bic":{"type":["string","null"]},"bank_name":{"type":["string","null"]},"bank_account_holder":{"type":["string","null"]}},"required":["id","name","brand_logo_url","brand_color","imprint_vat_id","imprint_trade_register","imprint_ceo_name","website","phone","tax_number","pdf_design_id","pdf_design_quote","pdf_design_order","pdf_design_delivery","pdf_design_invoice","has_custom_template","bank_iban","bank_bic","bank_name","bank_account_holder"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","name":"string","brand_logo_url":"string","brand_color":"string","imprint_vat_id":"string","imprint_trade_register":"string","imprint_ceo_name":"string","website":"string","phone":"string","tax_number":"string","pdf_design_id":"string","pdf_design_quote":"string","pdf_design_order":"string","pdf_design_delivery":"string","pdf_design_invoice":"string","has_custom_template":true,"bank_iban":"string","bank_bic":"string","bank_name":"string","bank_account_holder":"string"}}}},"401":{"description":"Kein Mandantenkontext — als text/plain, ohne JSON-Körper"},"404":{"description":"Mandant nicht gefunden — als text/plain, ohne JSON-Körper"},"503":{"description":"Kein Datenbank-Client — als text/plain, ohne JSON-Körper"}},"operationId":"getApiV1SettingsBranding","tags":["settings"],"parameters":[],"summary":"Load tenant branding settings","description":"Liest die Marken- und Impressumsangaben des Mandanten. Die Schlüssel sind snake_case (DB-Spaltennamen, kein Serialisierer). Das eigene HTML-Template kommt NICHT mit — `has_custom_template` sagt nur, ob eines hinterlegt ist."},"put":{"responses":{"200":{"description":"Entweder der aktualisierte Datensatz (18 Felder) ODER `{ updated: false }`, wenn kein bekanntes Feld im Rumpf stand","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"updated":{"type":"boolean","const":false},"message":{"type":"string"}},"required":["updated","message"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"brand_logo_url":{"type":["string","null"]},"brand_color":{"type":["string","null"]},"imprint_vat_id":{"type":["string","null"]},"imprint_trade_register":{"type":["string","null"]},"imprint_ceo_name":{"type":["string","null"]},"website":{"type":["string","null"]},"phone":{"type":["string","null"]},"tax_number":{"type":["string","null"]},"pdf_design_id":{"type":["string","null"]},"pdf_design_quote":{"type":["string","null"]},"pdf_design_order":{"type":["string","null"]},"pdf_design_delivery":{"type":["string","null"]},"pdf_design_invoice":{"type":["string","null"]},"bank_iban":{"type":["string","null"]},"bank_bic":{"type":["string","null"]},"bank_name":{"type":["string","null"]},"bank_account_holder":{"type":["string","null"]}},"required":["id","brand_logo_url","brand_color","imprint_vat_id","imprint_trade_register","imprint_ceo_name","website","phone","tax_number","pdf_design_id","pdf_design_quote","pdf_design_order","pdf_design_delivery","pdf_design_invoice","bank_iban","bank_bic","bank_name","bank_account_holder"],"additionalProperties":false}]},"example":{"updated":false,"message":"string"}}}},"400":{"description":"Validierungsfehler — hier ausnahmsweise der ROHE Auswurf des Validators (`{ success: false, error: <ZodError> }`), nicht das sonst übliche `{ error: \"validation_failed\", fields }`. Diese Route reicht `zValidatorHook` nicht durch.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{}},"required":["success"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext — als text/plain, ohne JSON-Körper"},"403":{"description":"Keine Admin-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Mandant nicht gefunden — als text/plain, ohne JSON-Körper"},"503":{"description":"Kein Datenbank-Client — als text/plain, ohne JSON-Körper"}},"operationId":"putApiV1SettingsBranding","tags":["settings"],"parameters":[],"summary":"Update tenant branding (admin)","description":"Aktualisiert die Marken- und Impressumsangaben. Teilweise Änderungen sind erlaubt, weggelassene Felder bleiben stehen; die vier Bank-Felder werden in `settings.bank` HINEINGEMISCHT statt überschrieben. ZWEI Dinge, die man der Antwort nicht ansieht: (1) Unbekannte Feldnamen — etwa ein Tippfehler — werden vom Validator stillschweigend gestrichen; bleibt danach nichts übrig, antwortet der Aufruf 200 mit `{ updated: false }`, obwohl NICHTS gespeichert wurde. (2) Die Antwort ist NICHT die Form von GET /branding: `name` und `has_custom_template` fehlen in der RETURNING-Liste.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"brand_logo_url":{"type":["string","null"],"format":"uri"},"brand_color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"imprint_vat_id":{"type":["string","null"],"maxLength":40},"imprint_trade_register":{"type":["string","null"],"maxLength":120},"imprint_ceo_name":{"type":["string","null"],"maxLength":120},"website":{"type":["string","null"],"format":"uri"},"phone":{"type":["string","null"],"maxLength":40},"tax_number":{"type":["string","null"],"maxLength":40},"bank_iban":{"type":["string","null"],"maxLength":34},"bank_bic":{"type":["string","null"],"maxLength":11},"bank_name":{"type":["string","null"],"maxLength":120},"bank_account_holder":{"type":["string","null"],"maxLength":120},"pdf_design_id":{"type":"string","enum":["classic","modern","bold","elegant","mono","fresh","stripe-like","dark-mode","gradient","executive","custom"]},"pdf_design_quote":{"type":["string","null"],"pattern":"^(custom:[0-9a-fA-F-]{1,40}|[a-z][a-z-]{1,30})$","maxLength":80},"pdf_design_order":{"type":["string","null"],"pattern":"^(custom:[0-9a-fA-F-]{1,40}|[a-z][a-z-]{1,30})$","maxLength":80},"pdf_design_delivery":{"type":["string","null"],"pattern":"^(custom:[0-9a-fA-F-]{1,40}|[a-z][a-z-]{1,30})$","maxLength":80},"pdf_design_invoice":{"type":["string","null"],"pattern":"^(custom:[0-9a-fA-F-]{1,40}|[a-z][a-z-]{1,30})$","maxLength":80}}},"example":{"brand_logo_url":"https://example.com","imprint_vat_id":"string","imprint_trade_register":"string","imprint_ceo_name":"string","website":"https://example.com","phone":"string","tax_number":"string","bank_iban":"string","bank_bic":"string","bank_name":"string","bank_account_holder":"string","pdf_design_id":"classic","pdf_design_quote":null,"pdf_design_order":null,"pdf_design_delivery":null,"pdf_design_invoice":null}}}}}},"/api/v1/settings/branding/designs":{"get":{"responses":{"200":{"description":"Design-Liste, virtueller `custom`-Eintrag und Platzhalter-Liste","content":{"application/json":{"schema":{"type":"object","properties":{"designs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}},"required":["id","name","description"],"additionalProperties":false}},"custom":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}},"required":["id","name","description"],"additionalProperties":false},"variables":{"type":"array","items":{"type":"string"}}},"required":["designs","custom","variables"],"additionalProperties":false},"example":{"designs":[{"id":"string","name":"string","description":"string"}],"custom":{"id":"string","name":"string","description":"string"},"variables":["string"]}}}},"401":{"description":"Nicht angemeldet — als text/plain, ohne JSON-Körper"}},"operationId":"getApiV1SettingsBrandingDesigns","tags":["settings"],"parameters":[],"summary":"List available PDF designs","description":"Listet die eingebauten PDF-Designs plus den virtuellen Eintrag `custom` und den Platzhalter-Spickzettel für eigene Handlebars-Vorlagen. Reine Programm-Konstanten — die Antwort hängt weder am Mandanten noch an der Datenbank, es wird keine Abfrage ausgeführt."}},"/api/v1/settings/branding/custom-templates":{"get":{"responses":{"200":{"description":"Vorlagen-Liste (ohne HTML-Inhalt)","content":{"application/json":{"schema":{"type":"object","properties":{"templates":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"bytes":{"type":"integer"},"created_at":{"type":"string"}},"required":["id","name","bytes","created_at"],"additionalProperties":false}}},"required":["templates"],"additionalProperties":false},"example":{"templates":[{"id":"string","name":"string","bytes":0,"created_at":"string"}]}}}},"401":{"description":"Kein Mandantenkontext — als text/plain, ohne JSON-Körper"},"503":{"description":"Kein Datenbank-Client — als text/plain, ohne JSON-Körper"}},"operationId":"getApiV1SettingsBrandingCustom-templates","tags":["settings"],"parameters":[],"summary":"List custom HTML templates","description":"Listet die benannten HTML-Vorlagen des Mandanten, neueste zuerst. Ungeblättert. Das HTML selbst kommt nicht mit — `bytes` ist nur seine Länge."},"post":{"responses":{"201":{"description":"Vorlage angelegt. Fällt die RETURNING-Zeile aus, geht ein LEERES Objekt mit demselben Status 201 raus — dann fehlt die id.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"bytes":{"type":"integer"},"created_at":{"type":"string"}},"required":["id","name","bytes","created_at"],"additionalProperties":false},{"type":"object","additionalProperties":false}]},"example":{"id":"string","name":"string","bytes":0,"created_at":"string"}}}},"400":{"description":"Kein HTML im Rumpf — als text/plain, ohne JSON-Körper"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Admin-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"413":{"description":"Vorlage größer als 500 KB — als text/plain, ohne JSON-Körper"},"503":{"description":"Kein Datenbank-Client — als text/plain, ohne JSON-Körper"}},"operationId":"postApiV1SettingsBrandingCustom-templates","tags":["settings"],"parameters":[],"summary":"Create custom HTML template (admin)","description":"Legt eine benannte HTML-Vorlage an. Nimmt entweder JSON `{ name, html }` oder einen Multipart-Upload (`file`, optional `name`; sonst der Dateiname, sonst „Eigene Vorlage\"). Der Rumpf läuft NICHT durch einen Validator — deshalb steht zu dieser Operation kein Eingabe-Schema in der Spezifikation. Das Anlegen schaltet die Vorlage NICHT scharf; dafür muss `pdf_design_*` auf `custom:<id>` gesetzt werden."}},"/api/v1/settings/branding/custom-templates/{id}":{"delete":{"responses":{"200":{"description":"Quittung — auch dann, wenn nichts gelöscht wurde","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":true}}}},"400":{"description":"Fehlende id bzw. fehlender Mandant. Über diesen Pfad NICHT erreichbar: ohne `:id` greift die Route gar nicht, und ein fehlender Mandant endet vorher in einer 401.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"id_required"}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"tenant_required"}},"required":["error"],"additionalProperties":false}]}}}},"401":{"description":"Kein Mandantenkontext — als text/plain, ohne JSON-Körper"},"403":{"description":"Keine Admin-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client — als text/plain, ohne JSON-Körper"}},"operationId":"deleteApiV1SettingsBrandingCustom-templatesById","tags":["settings"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete custom HTML template (admin)","description":"Löscht eine benannte HTML-Vorlage und setzt alle Belegtyp-Spalten zurück, die auf `custom:<id>` zeigten. ENDGÜLTIG — es gibt kein `deleted_at` und keinen Rückweg. `{ deleted: true }` ist eine Konstante, keine Messung: der Handler prüft nicht, ob überhaupt eine Zeile getroffen wurde. Eine unbekannte id liefert dieselbe Antwort wie ein echter Löschvorgang."}},"/api/v1/settings/branding/custom-template":{"post":{"responses":{"200":{"description":"Vorlage gespeichert, globales Design steht jetzt auf `custom`","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"pdf_design_id":{"type":["string","null"]},"bytes":{"type":"integer"}},"required":["id","pdf_design_id","bytes"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","pdf_design_id":"string","bytes":0}}}},"400":{"description":"Kein HTML im Rumpf — als text/plain, ohne JSON-Körper"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Admin-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Mandant nicht gefunden — als text/plain, ohne JSON-Körper"},"413":{"description":"Vorlage größer als 500 KB — als text/plain, ohne JSON-Körper"},"503":{"description":"Kein Datenbank-Client — als text/plain, ohne JSON-Körper"}},"operationId":"postApiV1SettingsBrandingCustom-template","tags":["settings"],"parameters":[],"summary":"Upload custom HTML template (admin)","description":"Alt-Weg „eine Vorlage pro Mandant\": speichert das HTML in `tenants.custom_pdf_template_html` und schaltet dabei UNGEFRAGT das globale Design auf `custom` um — ab diesem Aufruf werden alle Belege mit der eigenen Vorlage gerendert, nicht erst nach einer weiteren Bestätigung. Antwortet 200, nicht 201, obwohl Inhalt gespeichert wird. Für mehrere benannte Vorlagen stattdessen POST /branding/custom-templates verwenden."},"get":{"responses":{"200":{"description":"Das gespeicherte HTML oder `null`","content":{"application/json":{"schema":{"type":"object","properties":{"html":{"type":["string","null"]}},"required":["html"],"additionalProperties":false},"example":{"html":"string"}}}},"401":{"description":"Kein Mandantenkontext — als text/plain, ohne JSON-Körper"},"404":{"description":"Mandant nicht gefunden — als text/plain, ohne JSON-Körper"},"503":{"description":"Kein Datenbank-Client — als text/plain, ohne JSON-Körper"}},"operationId":"getApiV1SettingsBrandingCustom-template","tags":["settings"],"parameters":[],"summary":"Fetch current custom HTML template","description":"Liefert das HTML der Alt-Vorlage („eine pro Mandant\"). `null` bedeutet: keine hinterlegt. Die benannten Vorlagen aus /branding/custom-templates erscheinen hier NICHT."},"delete":{"responses":{"200":{"description":"Quittung — auch dann, wenn gar keine Vorlage hinterlegt war","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":true}}}},"401":{"description":"Kein Mandantenkontext — als text/plain, ohne JSON-Körper"},"403":{"description":"Keine Admin-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Kein Datenbank-Client — als text/plain, ohne JSON-Körper"}},"operationId":"deleteApiV1SettingsBrandingCustom-template","tags":["settings"],"parameters":[],"summary":"Clear custom HTML template (admin)","description":"Löscht die Alt-Vorlage ENDGÜLTIG (das HTML wird auf NULL gesetzt, es gibt keine Kopie) und schaltet ein globales Design `custom` zurück auf `classic`. `{ deleted: true }` ist eine Konstante, keine Messung: der Handler prüft nicht, ob überhaupt eine Vorlage vorhanden war."}},"/api/v1/settings/branding/preview-pdf":{"get":{"responses":{"200":{"description":"Der Beispielbeleg als PDF. `Cache-Control: no-store`, kein ETag — jeder Abruf rendert neu.","content":{"application/pdf":{}}},"400":{"description":"Unbekannter `type` — als text/plain, ohne JSON-Körper"},"401":{"description":"Kein Mandantenkontext — als text/plain, ohne JSON-Körper"}},"operationId":"getApiV1SettingsBrandingPreview-pdf","tags":["settings"],"parameters":[],"summary":"Live preview PDF with selectable design","description":"Rendert einen Beispielbeleg mit den Daten der „Muster Kunden GmbH\" und liefert ihn als PDF-Bytes zurück. Rein lesend: weder der Beleg noch die Design-Auswahl aus `?design=` werden gespeichert — der Parameter überschreibt die Mandanteneinstellung nur für diesen einen Aufruf. `?type=` wählt die Belegart (quote|order|delivery|invoice, Standard invoice); beim Lieferschein werden alle Preise auf 0 gesetzt. Das tatsächlich verwendete Design steht im Kopf `X-Design-Id`."}},"/api/v1/admin/branding-preview.pdf":{"get":{"responses":{"200":{"description":"Das PDF selbst, zur Anzeige im Browser (`inline`) und mit `Cache-Control: no-store`. Der Kopf `X-Branding-Warnings` traegt die fehlenden Branding-Felder.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Unbekannte Belegart in `type`"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AdminBranding-preview.pdf","tags":["admin"],"parameters":[],"summary":"Branding Preview PDF (Sample data, real tenant branding)","description":"Rendert ein Beleg-PDF mit dem ECHTEN Branding des Mandanten, aber mit erfundenen Kunden-, Positions- und Nummerndaten (Belegnummern in der Form `…-VORSCHAU-0001`). Der Aufruf liest nur die Branding-Einstellungen — er legt nichts an, speichert nichts und verschickt nichts. `type` waehlt die Belegart: quote, order, delivery oder invoice (Vorgabe invoice); alles andere ergibt 400. Beim Lieferschein bleiben die Preise ausgeblendet. Fehlende Branding-Felder werden nicht als Fehler gemeldet, sondern im Kopf `X-Branding-Warnings` aufgezaehlt (`none`, wenn nichts fehlt). Nur Rolle `admin` oder hoeher."}},"/api/v1/vendors/scorecard":{"get":{"responses":{"200":{"description":"ZWEI FORMEN. Im Normalfall die Lieferanten mit `meta.count` und `meta.periodDays`. Hat der Mandant noch keine `eingangsrechnungen`-Tabelle, kommt ebenfalls 200 — dann aber mit leerer Liste und `meta.note`. Wer nur `data.length` prueft, haelt eine fehlende Tabelle faelschlich fuer „keine Lieferanten\".","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"vendorKey":{"type":["string","null"],"description":"Gruppierungsschluessel: die Lieferantenkennung, oder `name:<Name>`, wenn die Rechnung keinen Stammsatz hat. `null`, wenn weder Kennung noch Name gepflegt sind."},"vendorName":{"type":["string","null"],"description":"Name des Lieferanten. `null`, wenn keiner erfasst ist."},"invoiceCount":{"type":"integer","minimum":0,"description":"Anzahl der Eingangsrechnungen im Zeitraum."},"totalSpend":{"type":"number","description":"Summe der Bruttobetraege im Zeitraum, in Euro."},"avgAmount":{"type":"number","description":"Durchschnittlicher Bruttobetrag je Rechnung, in Euro."},"paidCount":{"type":"integer","minimum":0,"description":"Wie viele dieser Rechnungen auf `paid` stehen."},"paymentRate":{"type":"integer","minimum":0,"maximum":100,"description":"Anteil bezahlter Rechnungen in PROZENT, ganzzahlig gerundet."},"firstInvoice":{"type":["string","null"],"description":"Aelteste Rechnung im Zeitraum, in Postgres-Schreibweise (kein `T`)."},"lastInvoice":{"type":["string","null"],"description":"Juengste Rechnung im Zeitraum, in Postgres-Schreibweise (kein `T`)."},"daysSinceLast":{"type":["integer","null"],"description":"Ganze Tage seit der juengsten Rechnung. `null`, wenn kein Datum vorliegt."},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Zusammenfassende Note von 0 bis 100 aus Volumen (bis 40), Aktualitaet (bis 30) und Zahlungsquote (bis 30). Eine Hausformel, kein Branchenmass."}},"required":["vendorKey","vendorName","invoiceCount","totalSpend","avgAmount","paidCount","paymentRate","firstInvoice","lastInvoice","daysSinceLast","score"],"additionalProperties":false},"maxItems":100,"description":"Die Lieferanten, groesstes Volumen zuerst. Hoechstens 100 Eintraege."},"meta":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":100,"description":"Anzahl der zurueckgegebenen Lieferanten."},"periodDays":{"type":"number","const":365,"description":"Laenge des ausgewerteten Zeitraums in Tagen — fest."}},"required":["count","periodDays"],"additionalProperties":false,"description":"Angaben zur Auswertung."}},"required":["data","meta"],"additionalProperties":false},{"type":"object","properties":{"data":{"type":"array","items":{},"maxItems":0,"description":"Immer leer. Es konnte nichts ausgewertet werden."},"meta":{"type":"object","properties":{"note":{"type":"string","const":"eingangsrechnungen table not yet available for this tenant","description":"Fester Text. Er sagt: die Tabelle fehlt noch, nicht „keine Lieferanten\"."}},"required":["note"],"additionalProperties":false,"description":"Hinweis statt Kennzahlen — `count` und `periodDays` fehlen hier."}},"required":["data","meta"],"additionalProperties":false}]},"example":{"data":[{"vendorKey":"string","vendorName":"string","invoiceCount":0,"totalSpend":0,"avgAmount":0,"paidCount":0,"paymentRate":0,"firstInvoice":"string","lastInvoice":"string","daysSinceLast":0,"score":0}],"meta":{"count":0,"periodDays":365}}}}},"400":{"description":"Mandantenkennung unbrauchbar (`invalid tenant slug`), als Text."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database unavailable`), als Text."}},"operationId":"getApiV1VendorsScorecard","tags":["Vendors"],"parameters":[],"summary":"Vendor performance scorecard","description":"Kennzahlen je Lieferant aus den Eingangsrechnungen der letzten 365 Tage — Volumen, Rechnungszahl, Zahlungsquote, Aktualitaet und eine daraus gerechnete Note. Hoechstens 100 Lieferanten, groesstes Volumen zuerst."}},"/api/v1/inventory/low-stock":{"get":{"responses":{"200":{"description":"ZWEI FORMEN. Im Normalfall die betroffenen Artikel mit `meta.count`. Hat der Mandant noch keine `products`-Tabelle, kommt ebenfalls 200 — dann aber mit leerer Liste und `meta.note` statt `meta.count`. Wer nur auf `data.length` schaut, haelt einen fehlenden Bestand faelschlich fuer einen gesunden.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Artikels (UUID)"},"sku":{"type":"string","minLength":1,"maxLength":100,"description":"Artikelnummer, mandantenweit eindeutig."},"name":{"type":"string","minLength":1,"maxLength":255,"description":"Bezeichnung des Artikels."},"category":{"type":["string","null"],"maxLength":100,"description":"Warengruppe. `null`, wenn keine gepflegt ist."},"stockQuantity":{"type":"number","description":"Aktueller Bestand in der Einheit des Artikels."},"minStock":{"type":"number","exclusiveMinimum":0,"description":"Gepflegter Mindestbestand. Artikel ohne Mindestbestand kommen gar nicht vor."},"shortfall":{"type":"number","exclusiveMinimum":0,"description":"Fehlmenge, also Mindestbestand minus Bestand. Immer groesser als null."},"unit":{"type":["string","null"],"maxLength":20,"description":"Mengeneinheit, z. B. `Stk`. `null`, wenn keine gepflegt ist."},"unitPrice":{"type":"number","description":"Verkaufspreis je Einheit in Euro. Ein leeres Feld kommt als `0` an, nicht als `null`."},"urgency":{"type":"string","enum":["critical","warning","low"],"description":"Dringlichkeit, aus der Fehlmenge gerechnet: `critical` ab der halben, `warning` ab einem Viertel des Mindestbestands, sonst `low`."}},"required":["id","sku","name","category","stockQuantity","minStock","shortfall","unit","unitPrice","urgency"],"additionalProperties":false},"maxItems":500,"description":"Die betroffenen Artikel, groesste Fehlmenge zuerst. Hoechstens 500 Eintraege."},"meta":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"description":"Anzahl der zurueckgegebenen Artikel."}},"required":["count"],"additionalProperties":false,"description":"Angaben zur Abfrage."}},"required":["data","meta"],"additionalProperties":false},{"type":"object","properties":{"data":{"type":"array","items":{},"maxItems":0,"description":"Immer leer. Es konnte nichts gelesen werden."},"meta":{"type":"object","properties":{"note":{"type":"string","const":"products table not yet available for this tenant","description":"Fester Text. Er sagt: die Tabelle fehlt noch, nicht „es gibt keine Unterdeckung\"."}},"required":["note"],"additionalProperties":false,"description":"Hinweis statt Kennzahlen — `count` fehlt hier."}},"required":["data","meta"],"additionalProperties":false}]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","sku":"string","name":"string","category":"string","stockQuantity":0,"minStock":1,"shortfall":1,"unit":"string","unitPrice":0,"urgency":"critical"}],"meta":{"count":0}}}}},"400":{"description":"Mandantenkennung unbrauchbar (`invalid tenant slug`), als Text."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"503":{"description":"Datenbank nicht erreichbar (`database unavailable`), als Text."}},"operationId":"getApiV1InventoryLow-stock","tags":["Inventory"],"parameters":[{"in":"query","name":"category","schema":{"type":"string","maxLength":100}}],"summary":"Products below minimum stock level","description":"Artikel, deren Bestand unter den gepflegten Mindestbestand gefallen ist — groesste Fehlmenge zuerst, hoechstens 500. Artikel ohne Mindestbestand und geloeschte Artikel bleiben aussen vor. Ueber `category` auf eine Warengruppe eingrenzbar."}},"/api/v1/dossiers/{id}/slots/{slotId}/link":{"post":{"responses":{"200":{"description":"Das nun gefuellte Fach.","content":{"application/json":{"schema":{"type":"object","properties":{"slot":{"type":"object","additionalProperties":{},"description":"Spaltennamen der Datenbank: id, position, slot_type, required, document_id, filled_at, notes."}},"required":["slot"]},"example":{"slot":{}}}}},"400":{"description":"`invalid_dossier_id` oder `invalid_slot_id` als `text/plain`; oder der Rumpf haelt das Schema nicht ein (`documentId` fehlt oder ist keine UUID)."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Akte fremd/unbekannt (`dossier_not_found`) oder Fach unbekannt (`slot_not_found`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"503":{"description":"`database_unavailable`."}},"operationId":"postApiV1DossiersByIdSlotsBySlotIdLink","tags":["Projekte"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"slotId","required":true}],"summary":"Beleg in ein Fach der Projektakte legen","description":"Legt einen Beleg in ein Fach der Akte: `document_id` wird gesetzt und\n`filled_at` auf den Zeitpunkt des Aufrufs.\n\nES WIRD NICHTS GELOESCHT. Der Beleg selbst wird nicht angefasst, er\nbekommt nur einen Platz in der Akte. Umkehrbar ueber die Gegenroute\n`DELETE /dossiers/{id}/slots/{slotId}/unlink`.\n\nWar das Fach schon belegt, wird es OHNE RUECKFRAGE ueberschrieben. Der\nvorherige Beleg verliert seinen Platz; welcher es war, steht danach\nnirgends mehr. Die Antwort zeigt nur den neuen Stand — sie sagt nicht, ob\ndas Fach vorher leer war.\n\nDER BELEG WIRD NICHT GEPRUEFT. `documentId` muss eine UUID sein, mehr\nnicht: weder die Existenz des Belegs noch seine Zugehoerigkeit zum\nMandanten wird nachgesehen. Eine Kennung ins Leere wird gespeichert und\nfaellt erst beim Lesen der Akte auf.\n\nDie Akte wird dagegen VORHER geprueft: gehoert sie einem anderen\nMandanten oder gibt es sie nicht, kommt 404 `dossier_not_found` und es\nwird nichts geaendert. Das Fach muss zu dieser Akte gehoeren, sonst 404\n`slot_not_found`.\n\nZur Art des Fachs (`slot_type`, etwa `angebot` oder `schlussrechnung`)\npasst der Beleg nicht zwingend — die Route vergleicht das nicht.\n\nUngueltige Kennungen ergeben 400 (`invalid_dossier_id`, `invalid_slot_id`)\nals `text/plain`.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten. Die\nModul-Sperre `projects` liegt auf `/projects/*` und damit NICHT auf\ndiesem Pfad.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"documentId":{"type":"string","format":"uuid"}},"required":["documentId"]},"example":{"documentId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/dossiers/{id}/slots/{slotId}/unlink":{"delete":{"responses":{"200":{"description":"Das nun leere Fach.","content":{"application/json":{"schema":{"type":"object","properties":{"slot":{"type":"object","additionalProperties":{},"description":"Spaltennamen der Datenbank: id, position, slot_type, required, document_id, filled_at, notes."}},"required":["slot"]},"example":{"slot":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Akte fremd/unbekannt (`dossier_not_found`) oder Fach unbekannt (`slot_not_found`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}},"operationId":"deleteApiV1DossiersByIdSlotsBySlotIdUnlink","tags":["Projekte"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"slotId","required":true}],"summary":"Beleg aus einem Fach der Projektakte loesen","description":"Leert ein Fach: `document_id` und `filled_at` werden auf `null` gesetzt.\nDas FACH BLEIBT bestehen — es ist danach wieder unbefuellt, nicht\ngeloescht. Der Beleg selbst wird nicht angeruehrt; er verliert nur seinen\nPlatz in der Akte.\n\nUmkehrbar ueber die Gegenroute `POST /dossiers/{id}/slots/{slotId}/link`.\n\nDie Zugehoerigkeit wird VORHER geprueft: gehoert die Akte einem anderen\nMandanten oder gibt es sie nicht, kommt 404 `dossier_not_found`, und es\nwird nichts geaendert. Trifft die Fach-Kennung nicht, kommt 404\n`slot_not_found` — ein Aufruf ins Leere ist also erkennbar und wird\nnicht als Erfolg gemeldet.\n\nUngueltige Kennungen ergeben 400 (`invalid_dossier_id`,\n`invalid_slot_id`) als `text/plain`.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."}},"/api/v1/voice/beleg":{"post":{"responses":{"201":{"description":"Entwurf angelegt. `extracted` kann der Ersatz aus dem Transkript sein.","content":{"application/json":{"schema":{"type":"object","properties":{"draftId":{"type":"string","description":"Kennung der angelegten Zeile in `public.documents`."},"extracted":{"type":"object","properties":{"vendor":{"type":"string"},"betrag":{"type":["number","null"]},"kategorie":{"type":["string","null"]},"expected_at":{"type":["string","null"]}},"required":["vendor","betrag","kategorie","expected_at"],"additionalProperties":false}},"required":["draftId","extracted"],"additionalProperties":false},"example":{"draftId":"string","extracted":{"vendor":"string","betrag":0,"kategorie":"string","expected_at":"string"}}}}},"400":{"description":"Kein oder ein zu langes Transkript (1 bis 10000 Zeichen)."},"401":{"description":"Keine Sitzung oder kein Mandantenkontext."},"500":{"description":"Der INSERT lief, lieferte aber keine Kennung zurueck (`draft_insert_failed`)."},"503":{"description":"Keine Datenbankverbindung — `error: \"database_unavailable\"`, dazu `Retry-After: 5`."}},"operationId":"postApiV1VoiceBeleg","tags":["Belege"],"parameters":[],"summary":"Beleg-Entwurf aus einem Sprach-Transkript anlegen","description":"Nimmt ein gesprochenes und bereits verschriftetes Transkript entgegen,\nlaesst ein Sprachmodell daraus Lieferant, Betrag, Kategorie und erwartetes\nDatum ziehen und legt damit einen Beleg-Entwurf an.\n\nUNTERSCHIED ZU `POST /voice/beleg-direct`: dort werden dieselben vier\nFelder fertig mitgeschickt und der Modellaufruf entfaellt. Diese Route\nhier ist die einzige der beiden, die ueberhaupt ein Modell befragt.\n\nEINE FEHLGESCHLAGENE EXTRAKTION SIEHT AUS WIE EINE GELUNGENE. Antwortet\ndas Modell nicht oder liefert es kein lesbares JSON, faellt der Dienst\nstill auf einen Ersatz zurueck: die ersten 80 Zeichen des Transkripts\nwerden als `vendor` eingetragen, `betrag`, `kategorie` und `expected_at`\nbleiben `null`. Der Entwurf wird trotzdem geschrieben und mit 201\ngemeldet. Erkennbar ist der Fall nur am `vendor`, der dann ein\nSatzanfang ist.\n\nDas vollstaendige Transkript wird in `metadata_json.transcript`\nmitgespeichert.\n\nDer Modellaufruf wird als KI-Kosten des Mandanten verbucht\n(`toolId: voice_beleg`). Welches Modell laeuft, entscheidet\n`NEMIX_AI_PROVIDER` — nicht diese Route.\n\nAngelegt wird eine Zeile in `public.documents` mit `source = 'voice'` und\n`status = 'awaiting_attachment'` — ein Entwurf, der auf den echten Beleg\nWARTET. Trifft spaeter eine Rechnung per E-Mail ein, sucht der Importeur\nueber den Lieferantennamen den passenden offenen Entwurf\n(`matchPendingDraftByVendor`).\n\nEs entsteht KEIN fertiger Beleg und keine Buchung — nur der Entwurf.\n\nDer Mandant kommt aus der Sitzung, nie aus dem Rumpf.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"transcript":{"type":"string","minLength":1,"maxLength":10000}},"required":["transcript"]},"example":{"transcript":"string"}}}}}},"/api/v1/voice/beleg-direct":{"post":{"responses":{"201":{"description":"Entwurf angelegt. `extracted` spiegelt die mitgeschickten Werte.","content":{"application/json":{"schema":{"type":"object","properties":{"draftId":{"type":"string","description":"Kennung der angelegten Zeile in `public.documents`."},"extracted":{"type":"object","properties":{"vendor":{"type":"string"},"betrag":{"type":["number","null"]},"kategorie":{"type":["string","null"]},"expected_at":{"type":["string","null"]}},"required":["vendor","betrag","kategorie","expected_at"],"additionalProperties":false}},"required":["draftId","extracted"],"additionalProperties":false},"example":{"draftId":"string","extracted":{"vendor":"string","betrag":0,"kategorie":"string","expected_at":"string"}}}}},"400":{"description":"Kein `vendor`, oder ein Feld ueber der Laengengrenze (vendor 200, kategorie 120, expected_at 50)."},"401":{"description":"Keine Sitzung oder kein Mandantenkontext."},"500":{"description":"Der INSERT lief, lieferte aber keine Kennung zurueck (`draft_insert_failed`)."},"503":{"description":"Keine Datenbankverbindung — `error: \"database_unavailable\"`, dazu `Retry-After: 5`."}},"operationId":"postApiV1VoiceBeleg-direct","tags":["Belege"],"parameters":[],"summary":"Beleg-Entwurf ohne Sprachmodell anlegen","description":"Legt denselben Beleg-Entwurf an wie `POST /voice/beleg`, nur aus bereits\nstrukturierten Feldern.\n\nUEBERSPRUNGEN WIRD GENAU EIN SCHRITT: der Modellaufruf. Kein Transkript,\nkeine Extraktion, keine KI-Kosten. Die vier mitgeschickten Werte landen\nungeprueft in `metadata_json.voice_extracted` und kommen unveraendert als\n`extracted` zurueck — die Antwort bestaetigt also die Eingabe, sie\nergaenzt sie nicht. `metadata_json.transcript` bleibt `null`.\n\nGedacht fuer den Fall, dass die Oberflaeche die Felder schon hat: eine\nnachbearbeitete Vorschau, oder getippt statt gesprochen.\n\nBeide Wege sind in `public.documents` am Namen zu unterscheiden:\n`Beleg-Draft: <vendor>` hier, `Voice-Draft: <vendor>` bei der\nTranskript-Route. `source` ist bei beiden `'voice'`.\n\nNur `vendor` ist Pflicht. `betrag`, `kategorie` und `expected_at` duerfen\nfehlen oder `null` sein und werden dann als `null` abgelegt.\n`expected_at` wird als Zeichenkette durchgereicht — es findet KEINE\nDatumspruefung statt.\n\nAngelegt wird eine Zeile in `public.documents` mit `source = 'voice'` und\n`status = 'awaiting_attachment'` — ein Entwurf, der auf den echten Beleg\nWARTET. Trifft spaeter eine Rechnung per E-Mail ein, sucht der Importeur\nueber den Lieferantennamen den passenden offenen Entwurf\n(`matchPendingDraftByVendor`).\n\nEs entsteht KEIN fertiger Beleg und keine Buchung — nur der Entwurf.\n\nDer Mandant kommt aus der Sitzung, nie aus dem Rumpf.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"vendor":{"type":"string","minLength":1,"maxLength":200},"betrag":{"type":["number","null"]},"kategorie":{"type":["string","null"],"maxLength":120},"expected_at":{"type":["string","null"],"maxLength":50}},"required":["vendor"]},"example":{"vendor":"string","betrag":0,"kategorie":"string","expected_at":"string"}}}}}},"/api/v1/document-folders":{"get":{"responses":{"200":{"description":"Ordner mit Dokumentzahl, nach Box und Pfad sortiert","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Ordners"},"tenant_id":{"type":"string","description":"Mandant, dem der Ordner gehoert"},"name":{"type":"string","description":"Angezeigter Name"},"path":{"type":"string","description":"ltree-Pfad als Text, aus den Namen der Kette gebildet"},"parent_id":{"type":["string","null"],"format":"uuid","description":"Uebergeordneter Ordner; null bei einem Wurzelordner"},"lane_status":{"type":["string","null"],"description":"Box, in der der Ordner haengt (inbox, processing, done, …)"},"is_smart":{"type":"boolean","description":"true bei einem Suchordner, dessen Inhalt aus smart_query_json kommt"},"smart_query_json":{"description":"Abfrage des Suchordners; bei gewoehnlichen Ordnern leer"},"icon":{"type":["string","null"],"description":"Symbolname; null wenn keiner gesetzt ist"},"color":{"type":["string","null"],"description":"Farbkennung; null wenn keine gesetzt ist"},"created_at":{"type":"string","description":"Anlagezeitpunkt"},"updated_at":{"type":"string","description":"Letzte Aenderung"},"doc_count":{"type":"integer","minimum":0,"description":"Anzahl nicht geloeschter Dokumente im Ordner; 0 auch dann, wenn die Zaehlung fehlschlug"}},"required":["id","tenant_id","name","path","parent_id","lane_status","is_smart","icon","color","created_at","updated_at","doc_count"]},"description":"Die Ordner des Mandanten"}},"required":["data"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"string","name":"string","path":"string","parent_id":"00000000-0000-4000-8000-000000000000","lane_status":"string","is_smart":true,"icon":"string","color":"string","created_at":"string","updated_at":"string","doc_count":0}]}}}},"401":{"description":"Kein Mandantenkontext"},"500":{"description":"Abfrage fehlgeschlagen"},"503":{"description":"Keine Datenbankverbindung"}},"operationId":"getApiV1Document-folders","tags":["documents","folders"],"parameters":[],"description":"Liefert die Ordner aus `public.document_folders`, nach Box und Pfad sortiert. Mit `?lane=<Box>` kommt nur ein Teilbaum; `approved` liest denselben Baum wie `done`.  Die Abfrage raeumt nebenbei auf: alte `approved`-Ordner werden in den `done`-Baum ueberfuehrt, und hat der Mandant ueberhaupt keinen Ordner, legt sie „Eingangsbelege\" und „Ausgangsbelege\" an. Wer seine Ordner geloescht hat, bekommt sie nur zurueck, wenn KEIN einziger mehr da ist.  Zu jedem Ordner steht in `doc_count` die Zahl der nicht geloeschten Dokumente. Scheitert diese Zaehlung, bleibt sie 0 und die Ordnerliste kommt trotzdem.","summary":"Liefert die Ordner aus `public.document_folders`, nach Box und Pfad sortiert","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Ordner angelegt.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"path":{"type":"string","description":"ltree-Pfad als Text, Punkte trennen die Ebenen."},"parent_id":{"type":["string","null"],"format":"uuid"},"is_smart":{"type":"boolean","description":"true: der Ordner sammelt per Suchanfrage statt eigener Belege."},"smart_query_json":{"type":"null","description":"Gespeicherte Suchanfrage; Form nicht zugesagt."},"icon":{"type":["string","null"]},"color":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"lane_status":{"type":["string","null"],"description":"Box des Ordners. `approved` wird beim Anlegen auf `done` normalisiert."}},"required":["id","tenant_id","name","path","parent_id","is_smart","icon","color","created_at","updated_at","lane_status"]},"example":{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"00000000-0000-4000-8000-000000000000","name":"string","path":"string","parent_id":"00000000-0000-4000-8000-000000000000","is_smart":true,"smart_query_json":null,"icon":"string","color":"string","created_at":"string","updated_at":"string","lane_status":"string"}}}},"400":{"description":"`folder_rule` (Unterordner im Eingang), oder der Rumpf haelt das Schema nicht ein (unbekannte Box, `parentId` keine UUID).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"404":{"description":"`parent_not_found` — Elternordner unbekannt oder in einer anderen Box.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"409":{"description":"`duplicate_path` — in dieser Box gibt es den Pfad schon.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"500":{"description":"`create_failed`; `message` traegt den Grund der Datenbank.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}}},"operationId":"postApiV1Document-folders","tags":["documents","folders"],"parameters":[],"summary":"Ordner anlegen","description":"Legt einen Ordner in einer Box (`laneStatus`) an. Ohne `parentId` wird\ner ein Wurzelordner, sonst ein Unterordner.\n\nDer Pfad wird aus dem NAMEN gebildet, nicht aus der Kennung: Umlaute\nwerden zerlegt, alles ausser Buchstaben, Ziffern und Unterstrich wird zu\n`_`. „Rechnungen 2026\" ergibt `Rechnungen_2026`. Zwei Ordner, deren\nNamen sich nur in solchen Zeichen unterscheiden, ergeben denselben Pfad\nund kollidieren mit 409. Der ANZEIGENAME bleibt davon unberuehrt und\nwird unveraendert gespeichert.\n\n`approved` wird beim Anlegen auf `done` umgeschrieben: Pruefung und\nArchiv teilen sich einen Baum. Wer `approved` schickt, bekommt einen\nOrdner mit `lane_status: \"done\"` zurueck.\n\nDer Elternordner muss in DERSELBEN Box liegen; sonst 404\n`parent_not_found`. Im Eingang (`inbox`) sind ueberhaupt keine\nUnterordner erlaubt (400 `folder_rule`) — ein Eingangsordner traegt je\neine Empfangsadresse.\n\n`isSmart` mit `smartQuery` legt einen Suchordner an, der keine eigenen\nBelege haelt. Die Form der Suchanfrage wird NICHT geprueft.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"laneStatus":{"type":"string","enum":["inbox","processing","classified","done","routed","approved","error"]},"parentId":{"type":["string","null"],"format":"uuid"},"icon":{"type":"string","maxLength":64},"color":{"type":"string","maxLength":32},"isSmart":{"type":"boolean"},"smartQuery":{"type":"object","additionalProperties":{}}},"required":["name","laneStatus"]},"example":{"name":"string","laneStatus":"inbox","parentId":"00000000-0000-4000-8000-000000000000","icon":"string","color":"string","isSmart":true,"smartQuery":{}}}}}}},"/api/v1/document-folders/{id}":{"put":{"responses":{"200":{"description":"Der Ordner nach der Aenderung — OHNE `lane_status`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"path":{"type":"string","description":"ltree-Pfad als Text, Punkte trennen die Ebenen."},"parent_id":{"type":["string","null"],"format":"uuid"},"is_smart":{"type":"boolean","description":"true: der Ordner sammelt per Suchanfrage statt eigener Belege."},"smart_query_json":{"type":"null","description":"Gespeicherte Suchanfrage; Form nicht zugesagt."},"icon":{"type":["string","null"]},"color":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","name","path","parent_id","is_smart","icon","color","created_at","updated_at"]},"example":{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"00000000-0000-4000-8000-000000000000","name":"string","path":"string","parent_id":"00000000-0000-4000-8000-000000000000","is_smart":true,"smart_query_json":null,"icon":"string","color":"string","created_at":"string","updated_at":"string"}}}},"400":{"description":"`empty_update` — keines der drei Felder war im Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"404":{"description":"Ordner unbekannt oder gehoert einem anderen Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"500":{"description":"`update_failed`; `message` traegt den Grund der Datenbank.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}}},"operationId":"putApiV1Document-foldersById","tags":["documents","folders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ordner umbenennen oder einfaerben","description":"Aendert Anzeigename, Symbol und Farbe eines Ordners. Trotz PUT eine\nTEIL-Aenderung: nur die uebergebenen Felder werden geschrieben, alles\nandere bleibt. Ein Rumpf ohne eines dieser drei Felder ist ein Fehler\n(400 `empty_update`).\n\nDER PFAD BLEIBT, WIE ER IST. Ein umbenannter Ordner behaelt seinen\nltree-Pfad aus dem urspruenglichen Namen; Anzeigename und Pfad laufen\ndanach dauerhaft auseinander. Das ist Absicht: der Pfad haelt die\nHierarchie und die Zuordnung der Belege, ein Umbenennen soll nichts\nverschieben. Sichtbar wird es in `path` und bei der Kollisionspruefung\nvon `POST /document-folders` — die vergleicht Pfade, nicht Namen.\n\nEs wird nichts geloescht und nichts verschoben. Box und Elternordner\nsind hier nicht aenderbar; dafuer gibt es `PATCH /{id}/box` und\n`POST /{id}/move`.\n\nANTWORT MIT EINEM FELD WENIGER: diese Route gibt `lane_status` NICHT\nzurueck, die uebrigen Ordner-Routen schon. Wer die Box nach dem\nUmbenennen braucht, liest sie ueber `GET /document-folders`.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"icon":{"type":["string","null"],"maxLength":64},"color":{"type":["string","null"],"maxLength":32}}},"example":{"name":"string","icon":"string","color":"string"}}}}},"delete":{"responses":{"200":{"description":"Geloescht. `trashedDocs` zaehlt die in den Papierkorb verschobenen Belege — siehe Vorbehalt oben.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deleted":{"type":"string","format":"uuid","description":"Kennung des angefragten Ordners; Unterordner sind mit weg."},"trashedDocs":{"type":"integer","description":"In den Papierkorb verschobene Belege. 0 kann auch „nicht ermittelbar\" heissen."}},"required":["ok","deleted","trashedDocs"]},"example":{"ok":true,"deleted":"00000000-0000-4000-8000-000000000000","trashedDocs":0}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"404":{"description":"Kein solcher Ordner in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"409":{"description":"Die Belege konnten nicht umgehaengt werden — `error: \"documents_not_moved\"`. Es wurde NICHTS geloescht, der Ordner steht unveraendert.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"500":{"description":"Das Loeschen selbst schlug fehl; `message` traegt den Grund der Datenbank.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}}},"operationId":"deleteApiV1Document-foldersById","tags":["documents","folders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ordner samt Unterordnern loeschen","description":"Loescht den Ordner UND ALLE UNTERORDNER. Der Name sagt Einzahl, die\nWirkung ist der ganze Teilbaum: geloescht wird ueber den ltree-Pfad\n(`path <@ …`), nicht ueber die Kennung. Es gibt keine Rueckfrage und\nkeinen Weg zurueck — die Ordnerzeilen werden hart entfernt.\n\nDie BELEGE darin werden nach ihrem Zustand unterschiedlich behandelt:\n· freigegebene (`pipeline_status = approved`) haengen an den Elternordner\n  des geloeschten Ordners um — auch die aus tiefen Unterordnern; die\n  Verschachtelung geht dabei verloren, sie landen alle auf einer Ebene.\n· alle uebrigen wandern in den Papierkorb (`deleted_at`), zaehlbar in\n  `trashedDocs`.\n\nSCHLAEGT DAS UMHAENGEN FEHL, BLEIBT DER ORDNER STEHEN — Antwort 409\n`documents_not_moved`, nichts wurde geloescht. Bis zum 17.08.2026 war es\numgekehrt: der Fehler ging nur ins Serverprotokoll, die Ordner fielen\ntrotzdem, und die Antwort meldete `trashedDocs: 0` unter Status 200 —\nnicht zu unterscheiden von „es lagen keine Belege darin\". Die Belege\nbehielten eine `folder_id` auf einen nicht mehr vorhandenen Ordner; die\nSpalte im Mandantenschema ist ein blankes UUID-Feld OHNE Fremdschluessel,\ndie Datenbank faengt das nicht ab.\n\nAUSNAHME, damit frische Mandanten weiter aufraeumen koennen: fehlt die\n`documents`-Tabelle oder das Mandantenschema ganz (42P01/3F000/42703),\ngibt es nichts zu verlieren — dann wird geloescht wie bisher.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten darf\nloeschen."}},"/api/v1/document-folders/{id}/box":{"patch":{"responses":{"200":{"description":"Ordner und Unterordner stehen in `done`. Die Antwort nennt keine Zahlen.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Der Rumpf haelt das Schema nicht ein — `box` muss `done` sein.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"404":{"description":"`not_found` — Ordner unbekannt oder gehoert einem anderen Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"409":{"description":"`duplicate_path` — in der Zielbox gibt es diesen Pfad schon.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"500":{"description":"`move_failed`; `message` traegt den Grund der Datenbank.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}}},"operationId":"patchApiV1Document-foldersByIdBox","tags":["documents","folders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ordner in die Box „Pruefung & Archiv\" holen","description":"Setzt den Ordner UND ALLE UNTERORDNER auf die Box `done` — den\ngemeinsamen Baum von Pruefung und Archiv. Es gibt nur dieses eine Ziel;\n`box` kennt keinen anderen Wert.\n\nGemeint sind Streu- und Alt-Ordner aus stillgelegten Boxen. Der Aufruf\nwirkt ueber den ltree-Pfad (`path <@ …`), nicht ueber die Kennung: der\nganze Teilbaum wechselt mit, auch wenn nur der oberste Ordner genannt\nist.\n\nDER ANGEFRAGTE ORDNER WIRD ZUM WURZELORDNER: sein `parent_id` wird auf\nnull gesetzt. Sein PFAD bleibt aber unveraendert — ein aus zwei Ebenen\ngeholter Ordner steht danach als Wurzel mit einem zweistufigen Pfad da.\nDie Unterordner behalten ihren Elternordner und haengen weiter unter ihm.\n\nEs wird nichts geloescht und kein Beleg angefasst. Belege folgen ihrem\nOrdner ueber `folder_id`, ihr eigener Bearbeitungsstand\n(`pipeline_status`) aendert sich dabei NICHT.\n\nNicht umkehrbar ueber diese Route: es gibt keinen Weg zurueck in die\nHerkunftsbox, die alte Box steht danach nirgends mehr.\n\nTrifft der Pfad in der Zielbox auf einen gleichnamigen Ordner, bricht der\nAufruf mit 409 ab und aendert nichts. Geprueft wird nur der Pfad des\nangefragten Ordners, nicht die seiner Unterordner.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"box":{"type":"string","enum":["done"]}},"required":["box"]},"example":{"box":"done"}}}}}},"/api/v1/document-folders/{id}/move":{"post":{"responses":{"200":{"description":"Der verschobene Ordner mit seinem neuen Pfad — OHNE `lane_status`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"path":{"type":"string","description":"ltree-Pfad als Text, Punkte trennen die Ebenen."},"parent_id":{"type":["string","null"],"format":"uuid"},"is_smart":{"type":"boolean","description":"true: der Ordner sammelt per Suchanfrage statt eigener Belege."},"smart_query_json":{"type":"null","description":"Gespeicherte Suchanfrage; Form nicht zugesagt."},"icon":{"type":["string","null"]},"color":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","name","path","parent_id","is_smart","icon","color","created_at","updated_at"]},"example":{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"00000000-0000-4000-8000-000000000000","name":"string","path":"string","parent_id":"00000000-0000-4000-8000-000000000000","is_smart":true,"smart_query_json":null,"icon":"string","color":"string","created_at":"string","updated_at":"string"}}}},"400":{"description":"`cycle_detected`, oder der Rumpf haelt das Schema nicht ein (`newParentId` fehlt oder ist keine UUID).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"404":{"description":"Ordner unbekannt (`Not found`) oder neuer Elternordner unbekannt (`parent_not_found`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"500":{"description":"`move_failed`; `message` traegt den Grund der Datenbank — hier landet auch die Pfadkollision.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}}},"operationId":"postApiV1Document-foldersByIdMove","tags":["documents","folders"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ordner unter einen anderen Elternordner haengen","description":"Haengt den Ordner unter `newParentId`. `null` macht ihn zum\nWurzelordner. Der ltree-Pfad des Ordners UND ALLER UNTERORDNER wird neu\ngesetzt; die Belege folgen ueber `folder_id`, ohne angefasst zu werden.\n\nDer neue Pfad entsteht aus dem NAMEN des Ordners, nicht aus seinem\nbisherigen Pfad-Endstueck. Wurde der Ordner zwischenzeitlich ueber\n`PUT /{id}` umbenannt — was den Pfad bewusst stehen laesst —, dann\nschreibt dieses Verschieben die Umbenennung nachtraeglich in den Pfad.\n\nKreise werden verhindert: der neue Elternordner darf nicht der Ordner\nselbst und kein Ordner unter ihm sein (400 `cycle_detected`).\n\nDIE BOX WIRD NICHT GEPRUEFT. `POST /document-folders` verlangt beim\nAnlegen ausdruecklich denselben `lane_status` fuer Eltern und Kind; hier\nwird das nicht geprueft. Ein Ordner kann so unter einen Elternordner aus\neiner ANDEREN Box geraten und behaelt dabei seine eigene Box — Pfad und\nBox sagen danach Verschiedenes. Auch die Tiefen-Regel des Eingangs\n(keine Unterordner) greift hier nicht.\n\nEs wird nichts geloescht. Kollidiert der neue Pfad mit einem\nbestehenden, scheitert die Datenbank und die Route antwortet 500\n`move_failed` — nicht 409 wie beim Anlegen.\n\nANTWORT MIT EINEM FELD WENIGER: diese Route gibt `lane_status` NICHT\nzurueck, die uebrigen Ordner-Routen schon.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"newParentId":{"type":["string","null"],"format":"uuid"}},"required":["newParentId"]},"example":{"newParentId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/document-folders/seed-defaults":{"post":{"responses":{"200":{"description":"Standard-Ordner vorhanden; `data` listet alle Ordner des Mandanten (pfadsortiert).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"count":{"type":"integer","description":"Zahl ALLER Ordner danach, nicht der neu angelegten."},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"path":{"type":"string","description":"ltree-Pfad als Text, Punkte trennen die Ebenen."},"parent_id":{"type":["string","null"],"format":"uuid"},"is_smart":{"type":"boolean","description":"true: der Ordner sammelt per Suchanfrage statt eigener Belege."},"smart_query_json":{"type":"null","description":"Gespeicherte Suchanfrage; Form nicht zugesagt."},"icon":{"type":["string","null"]},"color":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","name","path","parent_id","is_smart","icon","color","created_at","updated_at"]}}},"required":["ok","count","data"]},"example":{"ok":true,"count":0,"data":[{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"00000000-0000-4000-8000-000000000000","name":"string","path":"string","parent_id":"00000000-0000-4000-8000-000000000000","is_smart":true,"smart_query_json":null,"icon":"string","color":"string","created_at":"string","updated_at":"string"}]}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"500":{"description":"Anlegen oder Lesen schlug fehl; `message` traegt den Grund der Datenbank.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."},"retryAfter":{"type":"integer","description":"Sekunden bis zum naechsten Versuch; nur bei 503."}},"required":["error"]}}}}},"operationId":"postApiV1Document-foldersSeed-defaults","tags":["documents","folders"],"parameters":[],"summary":"Standard-Ordner sicherstellen","description":"Legt die zwei Standard-Ordner an (Eingang und Ausgang, Lane `done`) und\ngibt danach ALLE Ordner des Mandanten zurueck, nicht nur die neuen.\n\nMehrfach aufrufbar: jeder Ordner wird ueber Mandant + Lane + Pfad\ngesucht und nur angelegt, wenn er fehlt. Bestehende Ordner bleiben\nunangetastet — auch alte Jahres- und Monatsordner aus frueheren\nFassungen, in denen womoeglich Belege liegen. Ein zweiter Aufruf legt\nalso nichts doppelt an und loescht nichts.\n\nDie Antwort sagt NICHT, ob etwas angelegt wurde: `count` ist die Zahl\nALLER Ordner danach. Ein Mandant mit 17 Alt-Ordnern bekommt `count: 17`,\nob die zwei Standard-Ordner nun neu entstanden oder laengst da waren.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten darf das\nausloesen."}},"/api/v1/documents/{id}/tag-suggestions":{"get":{"responses":{"200":{"description":"Offene Vorschlaege. Leer heisst auch: Beleg unbekannt oder fremd.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"document_id":{"type":"string","format":"uuid"},"tag":{"type":"string","description":"Das vorgeschlagene Etikett."},"confidence":{"type":"number","description":"Zuversicht 0 bis 1, aus dem Erkennungsverfahren."},"source":{"type":"string","enum":["embedding","keyword","manual","llm"],"description":"Woher der Vorschlag stammt."},"status":{"type":"string","description":"Hier immer `pending` — entschiedene werden nicht gelistet."},"created_at":{"type":"string"}},"required":["id","document_id","tag","confidence","source","status","created_at"]}}},"required":["data"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","document_id":"00000000-0000-4000-8000-000000000000","tag":"string","confidence":0,"source":"embedding","status":"string","created_at":"string"}]}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"500":{"description":"Abfrage gescheitert; `message` traegt den Grund der Datenbank.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}}},"operationId":"getApiV1DocumentsByIdTag-suggestions","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Offene Etiketten-Vorschlaege eines Belegs","description":"Listet die noch nicht entschiedenen Vorschlaege zu einem Beleg, nach\nZuversicht absteigend. Angenommene und abgelehnte tauchen nicht mehr\nauf — die Liste ist eine Aufgabenliste, keine Historie.\n\nDie Abfrage filtert nach Mandant UND Beleg. Gibt es den Beleg nicht\noder gehoert er einem anderen Mandanten, kommt eine LEERE Liste unter\n200 — kein 404. Die Existenz des Belegs wird nicht geprueft.\n\nFeldnamen in `data` sind die SPALTENNAMEN der Datenbank\n(`document_id`, `created_at`), nicht die sonst uebliche Schreibweise\nmit Binnengrossbuchstaben. Diese Route serialisiert nicht um.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."}},"/api/v1/documents/{id}/suggest-tags":{"post":{"responses":{"200":{"description":"Die erzeugten Vorschlaege; leer, wenn nichts erkennbar war.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"]},"example":{"data":[{}]}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"404":{"description":"Beleg nicht gefunden — hier PRUEFT die Route die Existenz, anders als die Liste daneben.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"500":{"description":"Erkennung gescheitert; `message` traegt den Grund.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}}},"operationId":"postApiV1DocumentsByIdSuggest-tags","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Etiketten fuer einen Beleg vorschlagen lassen","description":"Stoesst die Erkennung von Hand an und legt die gefundenen Vorschlaege\nals `pending` ab. Normalerweise laeuft das im Hintergrund; diese Route\nist der Knopf dafuer.\n\nGrundlage sind der erkannte Text und der Vektor des Belegs. Fehlt\nbeides, laeuft die Erkennung trotzdem und liefert eben nichts — es gibt\nkeinen eigenen Fehlerfall dafuer.\n\nVorschlaege, deren Etikett auf der Sperrliste des Mandanten steht\n(entstanden durch Ablehnen), werden aussortiert und erscheinen nicht in\nder Antwort.\n\nMEHRFACHES AUSLOESEN ist unschaedlich, aber nicht wirkungsgleich: schon\nvorhandene Vorschlaege werden nicht verdoppelt (Eindeutigkeit ueber\nMandant, Beleg und Etikett), ein zweiter Lauf kann aber andere\nVorschlaege ergeben, wenn sich der Text oder der Vektor geaendert hat.\n\nDie Antwort ist die Liste der ERZEUGTEN Vorschlaege, nicht der Bestand.\nFeldnamen dieser Liste sagt die Route nicht zu: die Form stammt aus\n`services/auto-tagger`, nicht aus dieser Datei.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."}},"/api/v1/document-tag-suggestions/{id}/accept":{"post":{"responses":{"200":{"description":"Angenommen; das Etikett steht am Dokument.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Der Vorschlag wurde entschieden."}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"404":{"description":"Kein solcher Vorschlag in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"500":{"description":"Die Anweisung schlug fehl; `message` traegt den Grund der Datenbank.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}}},"operationId":"postApiV1Document-tag-suggestionsByIdAccept","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Etiketten-Vorschlag annehmen","description":"Setzt einen von der Texterkennung erzeugten Etiketten-Vorschlag auf\n`accepted` und haengt das Etikett an das zugehoerige Dokument. Ist es\ndort schon vorhanden, bleibt es bei einem Eintrag.\n\nDer Vorschlag muss dem eigenen Mandanten gehoeren; sonst antwortet die\nRoute 404 — nicht 403, denn eine fremde Kennung soll nicht durch die\nWahl des Fehlercodes als existierend bestaetigt werden.\n\nEs gibt KEINEN Weg zurueck: die Route kennt kein Zuruecknehmen. Ein\nfaelschlich angenommenes Etikett wird am Dokument entfernt, nicht hier.\n\nKeine Rollenpruefung — jeder angemeldete Benutzer des Mandanten darf\nentscheiden. Wer entschieden hat, steht in `decided_by`; ist der\nBenutzer im Kontext nicht bekannt, bleibt die Spalte LEER statt einen\nErsatzwert zu tragen."}},"/api/v1/document-tag-suggestions/{id}/reject":{"post":{"responses":{"200":{"description":"Abgelehnt; das Etikett steht auf der Sperrliste des Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Der Vorschlag wurde entschieden."}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"Kein Mandantenkontext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"404":{"description":"Kein solcher Vorschlag in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"500":{"description":"Die Anweisung schlug fehl; `message` traegt den Grund der Datenbank.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Grund."},"message":{"type":"string","description":"Klartext der Datenbank; nur bei 500 gesetzt."}},"required":["error"]}}}}},"operationId":"postApiV1Document-tag-suggestionsByIdReject","tags":["dms"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Etiketten-Vorschlag ablehnen","description":"Setzt den Vorschlag auf `rejected` und traegt das Etikett in die\nSperrliste des Mandanten ein (`tag_blacklist`).\n\nWICHTIG, weil die Wirkung ueber diesen einen Vorschlag hinausgeht: die\nSperrliste gilt fuer ALLE Dokumente des Mandanten, nicht nur fuer\ndieses. Ein zweites Ablehnen desselben Etiketts erhoeht nur den Zaehler.\nDie Oberfläche bietet kein Entsperren an — wer ein gesperrtes Etikett\nwieder vorgeschlagen bekommen will, braucht einen Eingriff in die\nDatenbank.\n\nMandantenpruefung, Fehlercodes und `decided_by` wie beim Annehmen."}},"/api/v1/documents/{docId}/shares":{"post":{"responses":{"201":{"description":"Freigabe angelegt — `url` ist der Link fuer den Empfaenger","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"token":{"type":"string"},"expires_at":{"type":["string","null"]},"max_downloads":{"type":["integer","null"]},"url":{"type":"string"},"passwordProtected":{"type":"boolean"}},"required":["id","token","expires_at","max_downloads","url","passwordProtected"]},"example":{"id":"string","token":"string","expires_at":"string","max_downloads":0,"url":"string","passwordProtected":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Beleg nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1DocumentsByDocIdShares","tags":["Documents · Sharing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"docId","required":true}],"summary":"Create a new share-link","description":"Erzeugt einen neuen Token (24 Byte Zufall) fuer den Beleg und gibt den fertigen Empfaenger-Link zurueck. Ein Beleg kann MEHRERE gueltige Freigaben nebeneinander haben — dieser Aufruf ersetzt keine bestehende. Alle drei Rumpf-Felder sind freiwillig: ohne `expiresInDays` laeuft der Link nie ab, ohne `maxDownloads` ist die Zahl der Abrufe unbegrenzt, ohne `password` ist er fuer jeden offen, der ihn hat. Der volle Token steht nur hier und in der Liste — er ist der einzige Schutz des Links. 404, wenn es den Beleg im Mandanten nicht gibt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"expiresInDays":{"type":"integer","minimum":1,"maximum":365},"password":{"type":"string","minLength":4,"maxLength":100},"maxDownloads":{"type":"integer","minimum":1,"maximum":10000}}},"example":{"expiresInDays":1,"password":"string","maxDownloads":1}}}}},"get":{"responses":{"200":{"description":"Freigaben des Belegs, neueste zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"token":{"type":"string"},"expires_at":{"type":["string","null"]},"max_downloads":{"type":["integer","null"]},"download_count":{"type":"integer"},"created_at":{"type":"string"},"revoked_at":{"type":["string","null"]},"password_protected":{"type":"boolean"},"url":{"type":"string"}},"required":["id","token","expires_at","max_downloads","download_count","created_at","revoked_at","password_protected","url"]}}},"required":["data"]},"example":{"data":[{"id":"string","token":"string","expires_at":"string","max_downloads":0,"download_count":0,"created_at":"string","revoked_at":"string","password_protected":true,"url":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1DocumentsByDocIdShares","tags":["Documents · Sharing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"docId","required":true}],"summary":"List share-links for a document","description":"Listet ALLE Freigaben des Belegs, neueste zuerst — auch widerrufene und abgelaufene; ob eine noch gilt, ergibt sich aus `revoked_at`, `expires_at` und dem Verhaeltnis von `download_count` zu `max_downloads`. Ohne Blaetterung und ohne Filter. Jede Zeile enthaelt den vollstaendigen Token und den fertigen Link; ein gesetztes Passwort wird nur als `password_protected` gemeldet, nie im Klartext."}},"/api/v1/documents/{docId}/shares/{id}":{"delete":{"responses":{"200":{"description":"Freigabe widerrufen","content":{"application/json":{"schema":{"type":"object","properties":{"revoked":{"type":"boolean","const":true}},"required":["revoked"]},"example":{"revoked":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Freigabe nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1DocumentsByDocIdSharesById","tags":["Documents · Sharing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"docId","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Revoke a share-link","description":"Setzt `revoked_at` auf die aktuelle Zeit; der Link ist damit sofort und ENDGUELTIG tot — es gibt keinen Endpunkt, der einen Widerruf zuruecknimmt, es braucht eine neue Freigabe. Die Zeile bleibt erhalten und erscheint weiter in der Liste, damit nachvollziehbar bleibt, dass es die Freigabe gab. Der Aufruf trifft nur eine Freigabe, die zu DIESEM Beleg gehoert; eine unbekannte oder bereits einem anderen Beleg zugeordnete Kennung ergibt 404 statt einer stillen Erfolgsmeldung. Andere Freigaben desselben Belegs bleiben gueltig."}},"/api/v1/timesheets/week":{"get":{"responses":{"200":{"description":"Die Woche des angemeldeten Benutzers samt Stundensumme","content":{"application/json":{"schema":{"type":"object","properties":{"week":{"type":"string","description":"Kalenderwoche nach ISO 8601, etwa 2026-W18"},"from":{"type":"string","description":"Montag der Woche als YYYY-MM-DD"},"to":{"type":"string","description":"Sonntag der Woche als YYYY-MM-DD"},"totalHours":{"type":"number","description":"Summe der Netto-Stunden aller Eintraege dieser Woche, auf zwei Stellen gerundet"},"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"userId":{},"userName":{},"date":{"type":["string","null"]},"startTime":{},"endTime":{},"breakMinutes":{"type":"number"},"totalHours":{"type":"number"},"projectId":{},"projectName":{},"description":{},"status":{},"approvedBy":{},"approvedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","date","breakMinutes","totalHours"],"additionalProperties":false},"description":"Die Eintraege der Woche, nach Datum und Beginn aufsteigend"}},"required":["week","from","to","totalHours","entries"],"additionalProperties":false},"example":{"week":"string","from":"string","to":"string","totalHours":0,"entries":[{"id":"string","date":"string","breakMinutes":0,"totalHours":0}]}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1TimesheetsWeek","tags":["timesheets"],"parameters":[],"summary":"Liefert die Wochenübersicht der Zeiteinträge","description":"Liest die Eintraege des ANGEMELDETEN Benutzers fuer die laufende Woche (Montag bis Sonntag, nach Serverdatum) und summiert die Netto-Stunden. Es gibt keine Parameter: weder eine andere Woche noch ein anderer Mitarbeiter sind waehlbar — dafuer ist GET /timesheets mit `week` und `userId` da. Die Huelle ist eine andere als bei der Liste: kein `data`, kein `pagination`."}},"/api/v1/timesheets/reports":{"get":{"responses":{"200":{"description":"Summen je Mitarbeiter, Auftrag und Woche","content":{"application/json":{"schema":{"type":"object","properties":{"byUser":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","description":"Kennung des Mitarbeiters"},"user_name":{"type":"string","description":"Anzeigename, wie er beim Buchen gespeichert wurde"},"total_hours":{"type":"number","description":"Summe der Netto-Stunden"},"entry_count":{"type":"integer","description":"Anzahl der Eintraege"}},"required":["user_id","user_name","total_hours","entry_count"]},"description":"Je Mitarbeiter, meiste Stunden zuerst"},"byProject":{"type":"array","items":{"type":"object","properties":{"project_id":{"type":["string","null"],"description":"Kennung des Auftrags oder Projekts; null bei Eintraegen ohne Bezug"},"project_name":{"type":["string","null"],"description":"Name zum Zeitpunkt der Buchung; null wenn keiner ermittelt wurde"},"total_hours":{"type":"number","description":"Summe der Netto-Stunden"},"entry_count":{"type":"integer","description":"Anzahl der Eintraege"}},"required":["project_id","project_name","total_hours","entry_count"]},"description":"Je Auftrag oder Projekt, meiste Stunden zuerst"},"byWeek":{"type":"array","items":{"type":"object","properties":{"week":{"type":"string","description":"Kalenderwoche nach ISO 8601, etwa 2026-W18"},"total_hours":{"type":"number","description":"Summe der Netto-Stunden"},"entry_count":{"type":"integer","description":"Anzahl der Eintraege"}},"required":["week","total_hours","entry_count"]},"description":"Je Kalenderwoche, neueste zuerst"},"filters":{"type":"object","properties":{"from":{"type":["string","null"],"description":"Der wirksame Von-Tag; null wenn keiner gesetzt war"},"to":{"type":["string","null"],"description":"Der wirksame Bis-Tag; null wenn keiner gesetzt war"},"userId":{"type":["string","null"],"description":"Der WIRKSAME Mitarbeiterfilter — bei einfachen Benutzern die eigene Kennung, auch wenn eine andere angefragt wurde"},"projectId":{"type":["string","null"],"description":"Der wirksame Auftragsfilter; null wenn keiner gesetzt war"}},"required":["from","to","userId","projectId"],"description":"Die tatsaechlich angewandten Filter, nicht die angefragten"}},"required":["byUser","byProject","byWeek","filters"],"additionalProperties":false},"example":{"byUser":[{"user_id":"string","user_name":"string","total_hours":0,"entry_count":0}],"byProject":[{"project_id":"string","project_name":"string","total_hours":0,"entry_count":0}],"byWeek":[{"week":"string","total_hours":0,"entry_count":0}],"filters":{"from":"string","to":"string","userId":"string","projectId":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1TimesheetsReports","tags":["timesheets"],"parameters":[],"summary":"Liefert Auswertungen zu Zeiterfassung (pro Mitarbeiter/Projekt)","description":"Summiert dieselbe gefilterte Menge dreimal: je Mitarbeiter, je Auftrag und je Kalenderwoche. Gefiltert wird ueber `from`, `to`, `userId` und `projectId`; ohne Angabe laeuft die Auswertung ueber den gesamten Bestand. Wer nicht mindestens Manager ist, wertet immer nur die eigenen Zeiten aus — ein mitgeschicktes `userId` wird stillschweigend durch die eigene Kennung ersetzt. Was wirklich gefiltert wurde, steht in `filters`. Die drei Gruppierungen kommen roh aus der Datenbank und tragen deshalb snake_case-Feldnamen, anders als der Rest dieser Datei."}},"/api/v1/timesheets":{"get":{"responses":{"200":{"description":"Liste der Zeiteinträge","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"userId":{},"userName":{},"date":{"type":["string","null"]},"startTime":{},"endTime":{},"breakMinutes":{"type":"number"},"totalHours":{"type":"number"},"projectId":{},"projectName":{},"description":{},"status":{},"approvedBy":{},"approvedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","date","breakMinutes","totalHours"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","date":"string","breakMinutes":0,"totalHours":0}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Timesheets","tags":["timesheets"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"userId","schema":{"type":"string"}},{"in":"query","name":"projectId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"week","schema":{"type":"string","pattern":"^\\d{4}-W\\d{2}$"}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","submitted","approved","rejected"]}},{"in":"query","name":"from","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"in":"query","name":"to","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}}],"summary":"Listet Zeiteinträge mit Filter und Paginierung","description":"Liest die Tabelle timesheets, neueste Tage zuerst und darin nach Beginn aufsteigend. Filter sind `userId`, `projectId`, `status`, `from`, `to` und `week` (Format 2026-W18, wird in Montag bis Sonntag aufgeloest). Wer nicht mindestens Manager ist, sieht ausschliesslich die eigenen Eintraege — ein mitgeschicktes `userId` wird stillschweigend durch die eigene Kennung ersetzt. `limit` liegt zwischen 1 und 200 (Vorgabe 50); `pagination.total` ist eine echte Gesamtzahl ueber denselben Filter, nicht nur die Zeilenzahl der Seite."},"post":{"responses":{"201":{"description":"Zeiteintrag angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"userId":{},"userName":{},"date":{"type":["string","null"]},"startTime":{},"endTime":{},"breakMinutes":{"type":"number"},"totalHours":{"type":"number"},"projectId":{},"projectName":{},"description":{},"status":{},"approvedBy":{},"approvedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","date","breakMinutes","totalHours"],"additionalProperties":false},"example":{"id":"string","date":"string","breakMinutes":0,"totalHours":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"}},"operationId":"postApiV1Timesheets","tags":["timesheets"],"parameters":[],"summary":"Legt einen neuen Zeiteintrag an","description":"Schreibt eine Zeile in timesheets, immer mit `status: \"draft\"`. Mitarbeiter und Anzeigename kommen aus der Sitzung, nicht aus dem Rumpf. Die Netto-Stunden rechnet der Server aus Beginn, Ende und Pause (Vorgabe 30 Minuten) und rundet auf zwei Stellen; ein Ende vor dem Beginn ergibt 0 Stunden statt eines Fehlers, und Nachtschichten ueber Mitternacht sind so nicht erfassbar. `projectName` wird uebernommen, wenn gesetzt; sonst wird der Name einmalig aus projects nachgeschlagen und als Text mitgespeichert — er wandert spaeter nicht mit, wenn das Projekt umbenannt wird.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"startTime":{"type":"string","pattern":"^\\d{2}:\\d{2}$"},"endTime":{"type":"string","pattern":"^\\d{2}:\\d{2}$"},"breakMinutes":{"type":"integer","minimum":0,"default":30},"projectId":{"type":"string","format":"uuid"},"projectName":{"type":"string","maxLength":200},"description":{"type":"string","minLength":1,"maxLength":1000}},"required":["date","startTime","endTime","description"]}}}}}},"/api/v1/timesheets/{id}":{"get":{"responses":{"200":{"description":"Zeiteintrag-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"userId":{},"userName":{},"date":{"type":["string","null"]},"startTime":{},"endTime":{},"breakMinutes":{"type":"number"},"totalHours":{"type":"number"},"projectId":{},"projectName":{},"description":{},"status":{},"approvedBy":{},"approvedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","date","breakMinutes","totalHours"],"additionalProperties":false},"example":{"id":"string","date":"string","breakMinutes":0,"totalHours":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Zeiteintrag nicht gefunden"}},"operationId":"getApiV1TimesheetsById","tags":["timesheets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Liefert einen einzelnen Zeiteintrag","description":"Liest einen Eintrag ueber seine Kennung und liefert ihn ohne Umschlag. Wer nicht mindestens Manager ist, erreicht nur eigene Eintraege; ein fremder bekommt bewusst 404 und nicht 403, damit aus der Antwort nicht hervorgeht, dass es ihn gibt."},"put":{"responses":{"200":{"description":"Der Eintrag nach der Aenderung, wieder im Entwurf","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"userId":{},"userName":{},"date":{"type":["string","null"]},"startTime":{},"endTime":{},"breakMinutes":{"type":"number"},"totalHours":{"type":"number"},"projectId":{},"projectName":{},"description":{},"status":{},"approvedBy":{},"approvedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","date","breakMinutes","totalHours"],"additionalProperties":false},"example":{"id":"string","date":"string","breakMinutes":0,"totalHours":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Zeiteintrag nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1TimesheetsById","tags":["timesheets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aktualisiert einen Zeiteintrag","description":"Uebernimmt die mitgeschickten Felder; weggelassene werden aus dem bestehenden Satz uebernommen, und die Netto-Stunden rechnet der Server aus dem Ergebnis neu. ACHTUNG: jede Aenderung setzt `status` zurueck auf `draft` — eine bereits genehmigte Zeit verliert damit ihre Genehmigung und muss erneut genehmigt werden. `approvedBy` und `approvedAt` bleiben dabei stehen und zeigen dann auf die alte, nicht mehr gueltige Genehmigung. Wer nicht mindestens Manager ist, aendert nur eigene Eintraege; ein fremder bekommt 404 statt 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"startTime":{"type":"string","pattern":"^\\d{2}:\\d{2}$"},"endTime":{"type":"string","pattern":"^\\d{2}:\\d{2}$"},"breakMinutes":{"type":"integer","minimum":0,"default":30},"projectId":{"type":"string","format":"uuid"},"projectName":{"type":"string","maxLength":200},"description":{"type":"string","minLength":1,"maxLength":1000}}},"example":{"date":"2026-01-01","breakMinutes":0,"projectId":"00000000-0000-4000-8000-000000000000","projectName":"string","description":"string"}}}}},"delete":{"responses":{"200":{"description":"Der Eintrag wurde geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string","description":"Kennung des geloeschten Eintrags"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Zeiteintrag nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1TimesheetsById","tags":["timesheets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Löscht einen Zeiteintrag","description":"Entfernt den Eintrag endgueltig aus timesheets — kein Soft-Delete, kein `deleted_at`, kein Wiederherstellen. Auch ein bereits genehmigter Eintrag wird ohne Rueckfrage geloescht. Wer nicht mindestens Manager ist, loescht nur eigene Eintraege: der Loeschbefehl selbst ist auf die eigene Kennung eingeschraenkt, ein fremder Eintrag trifft nichts und ergibt 404."}},"/api/v1/timesheets/{id}/approve":{"post":{"responses":{"200":{"description":"Der Eintrag mit neuem Stand und Genehmigungsvermerk","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"userId":{},"userName":{},"date":{"type":["string","null"]},"startTime":{},"endTime":{},"breakMinutes":{"type":"number"},"totalHours":{"type":"number"},"projectId":{},"projectName":{},"description":{},"status":{},"approvedBy":{},"approvedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","date","breakMinutes","totalHours"],"additionalProperties":false},"example":{"id":"string","date":"string","breakMinutes":0,"totalHours":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Zeiteintrag nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1TimesheetsByIdApprove","tags":["timesheets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Genehmigt einen Zeiteintrag (nur Manager+)","description":"Setzt `status` auf den uebergebenen Wert — `approved` ODER `rejected`, der Endpunkt lehnt also auch ab — und haelt in `approvedBy` den Genehmigenden fest (E-Mail, ersatzweise Benutzerkennung, sonst `system`), in `approvedAt` den Zeitpunkt. Ein bestimmter Vorzustand wird nicht verlangt: ein Entwurf springt ohne Einreichung direkt auf genehmigt, eine Ablehnung laesst sich ebenso wieder umdrehen. Manager duerfen auch eigene Zeiten genehmigen. Das Feld `note` im Rumpf wird angenommen, aber NICHT gespeichert — die Tabelle hat keine Spalte dafuer, eine Begruendung geht also verloren.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["approved","rejected"]},"note":{"type":"string"}},"required":["status"]},"example":{"status":"approved","note":"string"}}}}}},"/api/v1/bom":{"get":{"responses":{"200":{"description":"Liste BOM-Eintraege mit Blaetterung — `children` ist hier immer leer","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"partNo":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"type":{"type":"string"},"parentId":{"type":["string","null"]},"cost":{"type":["number","null"]},"leadTimeDays":{"type":["number","null"]},"children":{"type":"array","items":{},"description":"Unter-Eintraege in derselben Form wie dieses Objekt (rekursiv). Bei GET /bom immer leer — der Baum kommt nur aus GET /bom/{partNo}."},"createdAt":{},"updatedAt":{}},"required":["id","partNo","name","description","quantity","unit","type","parentId","cost","leadTimeDays","children"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","partNo":"string","name":"string","description":"string","quantity":0,"unit":"string","type":"string","parentId":"string","cost":0,"leadTimeDays":0,"children":[]}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Bom","tags":["bom"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"type","schema":{"type":"string","enum":["assembly","component","raw"]}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"topLevelOnly","schema":{"type":"string","enum":["true","false","1","0","yes","no","on","off"]}}],"summary":"List BOM items (Stückliste)","description":"Flache Liste der Stuecklisten-Eintraege, sortiert nach Teilenummer. `children` ist hier IMMER leer — den Baum liefert nur GET /bom/{partNo}. `topLevelOnly=true` beschraenkt auf Eintraege ohne Elternteil. Diese Tabelle kennt kein Soft-Delete."},"post":{"responses":{"201":{"description":"Angelegter BOM-Eintrag — `children` ist beim Anlegen immer leer","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"partNo":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"type":{"type":"string"},"parentId":{"type":["string","null"]},"cost":{"type":["number","null"]},"leadTimeDays":{"type":["number","null"]},"children":{"type":"array","items":{},"description":"Unter-Eintraege in derselben Form wie dieses Objekt (rekursiv). Bei GET /bom immer leer — der Baum kommt nur aus GET /bom/{partNo}."},"createdAt":{},"updatedAt":{}},"required":["id","partNo","name","description","quantity","unit","type","parentId","cost","leadTimeDays","children"],"additionalProperties":false},"example":{"id":"string","partNo":"string","name":"string","description":"string","quantity":0,"unit":"string","type":"string","parentId":"string","cost":0,"leadTimeDays":0,"children":[]}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"409":{"description":"partNo bereits vergeben","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"part_no_exists"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Bom","tags":["bom"],"parameters":[],"summary":"Create BOM item","description":"Legt einen Stuecklisten-Eintrag an, ohne `parentId` als Kopfteil, mit `parentId` als Unterposition darunter. `children` ist in der Antwort immer leer. Die Teilenummer traegt in der Tabelle nur einen normalen Index, KEINE Eindeutigkeitsbedingung — dieselbe Teilenummer laesst sich mehrfach anlegen, und der unten dokumentierte 409 tritt dann nicht ein.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"partNo":{"type":"string","minLength":1,"maxLength":100},"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"quantity":{"type":"number","exclusiveMinimum":0},"unit":{"type":"string","minLength":1,"maxLength":30,"default":"Stück"},"type":{"type":"string","enum":["assembly","component","raw"],"default":"component"},"parentId":{"type":"string","format":"uuid"},"cost":{"type":"number","minimum":0},"leadTimeDays":{"type":"integer","minimum":0}},"required":["partNo","name","quantity"]},"example":{"partNo":"string","name":"string","description":"string","quantity":1,"unit":"string","type":"assembly","parentId":"00000000-0000-4000-8000-000000000000","cost":0,"leadTimeDays":0}}}}}},"/api/v1/bom/headers":{"get":{"responses":{"200":{"description":"BOM-Header-Liste mit Blaetterung","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"version":{"type":"string"},"status":{"type":"string","enum":["draft","active","obsolete"]},"validFrom":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","version","status","notes"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","productId":"string","version":"string","status":"draft","notes":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1BomHeaders","tags":["bom"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","active","obsolete"]}},{"in":"query","name":"productId","schema":{"type":"string","format":"uuid"}}],"summary":"List BOM headers","description":"Liste der Stuecklisten-Koepfe (bom_headers), neueste zuerst. Diese Header sind ein EIGENER Datenbestand, getrennt von den Alt-Eintraegen unter GET /bom — ein Header taucht dort nicht auf und umgekehrt. Filterbar nach Status und Produkt."},"post":{"responses":{"201":{"description":"Angelegter BOM-Header","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"version":{"type":"string"},"status":{"type":"string","enum":["draft","active","obsolete"]},"validFrom":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","version","status","notes"],"additionalProperties":false},"example":{"id":"string","productId":"string","version":"string","status":"draft","notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1BomHeaders","tags":["bom"],"parameters":[],"summary":"Create BOM header","description":"Legt einen Stuecklisten-Kopf an (Vorgabe: Version \"1.0\", Status \"draft\"). Der Kopf kommt ohne Positionen zur Welt; die kommen einzeln ueber POST /bom/headers/{id}/lines dazu. Weder Produkt noch Version werden auf Eindeutigkeit geprueft — dasselbe Produkt kann mehrere aktive Stuecklisten haben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"version":{"type":"string","maxLength":30,"default":"1.0"},"status":{"type":"string","enum":["draft","active","obsolete"],"default":"draft"},"validFrom":{"type":"string"},"notes":{"type":"string"}}},"example":{"productId":"00000000-0000-4000-8000-000000000000","version":"string","status":"draft","validFrom":"string","notes":"string"}}}}}},"/api/v1/bom/by-part/{partNo}":{"get":{"responses":{"200":{"description":"BOM-Baum — oder der Koerper `null`, wenn die Teilenummer nur als Unterposition existiert (siehe bomTreeResponseSchema)","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"partNo":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"type":{"type":"string"},"parentId":{"type":["string","null"]},"cost":{"type":["number","null"]},"leadTimeDays":{"type":["number","null"]},"children":{"type":"array","items":{},"description":"Unter-Eintraege in derselben Form wie dieses Objekt (rekursiv). Bei GET /bom immer leer — der Baum kommt nur aus GET /bom/{partNo}."},"createdAt":{},"updatedAt":{}},"required":["id","partNo","name","description","quantity","unit","type","parentId","cost","leadTimeDays","children"],"additionalProperties":false},{"type":"null"}]},"example":{"id":"string","partNo":"string","name":"string","description":"string","quantity":0,"unit":"string","type":"string","parentId":"string","cost":0,"leadTimeDays":0,"children":[]}}}},"401":{"description":"Unauthorized"},"404":{"description":"BOM nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1BomBy-partByPartNo","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"partNo","required":true}],"summary":"Get BOM tree by part number","description":"Stuecklisten-Baum zu einer Teilenummer, rekursiv aufgeloest. Ein Fall, den man kennen muss: existiert die Teilenummer nur als Unterposition, antwortet die Route mit HTTP 200 und dem Koerper `null` — nicht mit 404. Ist sie unbekannt, kommt ein 404, das die gesuchte Teilenummer nennt."}},"/api/v1/bom/{id}":{"put":{"responses":{"200":{"description":"Aktualisierter BOM-Eintrag","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"partNo":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"type":{"type":"string"},"parentId":{"type":["string","null"]},"cost":{"type":["number","null"]},"leadTimeDays":{"type":["number","null"]},"children":{"type":"array","items":{},"description":"Unter-Eintraege in derselben Form wie dieses Objekt (rekursiv). Bei GET /bom immer leer — der Baum kommt nur aus GET /bom/{partNo}."},"createdAt":{},"updatedAt":{}},"required":["id","partNo","name","description","quantity","unit","type","parentId","cost","leadTimeDays","children"],"additionalProperties":false},"example":{"id":"string","partNo":"string","name":"string","description":"string","quantity":0,"unit":"string","type":"string","parentId":"string","cost":0,"leadTimeDays":0,"children":[]}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"BOM-Eintrag nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1BomById","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update BOM item","description":"Aendert einen Stuecklisten-Eintrag; nicht gesendete Felder bleiben erhalten. `parentId` laesst sich hier umhaengen — der gesamte Unterbaum wandert mit. Ein Zyklus (Eintrag unter sein eigenes Kind haengen) wird NICHT geprueft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"partNo":{"type":"string","minLength":1,"maxLength":100},"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"quantity":{"type":"number","exclusiveMinimum":0},"unit":{"type":"string","minLength":1,"maxLength":30,"default":"Stück"},"type":{"type":"string","enum":["assembly","component","raw"],"default":"component"},"parentId":{"type":"string","format":"uuid"},"cost":{"type":"number","minimum":0},"leadTimeDays":{"type":"integer","minimum":0}}},"example":{"partNo":"string","name":"string","description":"string","quantity":1,"unit":"string","type":"assembly","parentId":"00000000-0000-4000-8000-000000000000","cost":0,"leadTimeDays":0}}}}},"delete":{"responses":{"200":{"description":"Geloescht — nur die getroffene ID (Kinder gehen per Cascade mit)","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"BOM-Eintrag nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1BomById","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete BOM item","description":"ENDGUELTIGES Loeschen: die Zeile wird entfernt, nicht als geloescht markiert, und der GESAMTE Unterbaum geht per Cascade mit. Die Antwort nennt nur die getroffene ID; wie viele Kinder dabei verschwunden sind, sagt sie nicht."}},"/api/v1/bom/headers/{id}":{"get":{"responses":{"200":{"description":"BOM-Header FLACH, mit angehaengten Positionen unter `lines`","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"version":{"type":"string"},"status":{"type":"string","enum":["draft","active","obsolete"]},"validFrom":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{},"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"bomId":{"type":"string"},"componentProductId":{"type":["string","null"]},"componentName":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"position":{"type":"integer"},"unitCost":{"type":["number","null"]},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","bomId","componentProductId","componentName","quantity","unit","position","unitCost","notes"],"additionalProperties":false}}},"required":["id","productId","version","status","notes","lines"],"additionalProperties":false},"example":{"id":"string","productId":"string","version":"string","status":"draft","notes":"string","lines":[{"id":"string","bomId":"string","componentProductId":"string","componentName":"string","quantity":0,"unit":"string","position":0,"unitCost":0,"notes":"string"}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1BomHeadersById","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get BOM header with lines","description":"Ein Stuecklisten-Kopf samt seiner Positionen unter `lines`, nach Position sortiert. Nur die DIREKTEN Positionen, nicht rekursiv — dafuer GET /bom/{id}/explode."},"put":{"responses":{"200":{"description":"Aktualisierter BOM-Header (OHNE Positionen)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"version":{"type":"string"},"status":{"type":"string","enum":["draft","active","obsolete"]},"validFrom":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","version","status","notes"],"additionalProperties":false},"example":{"id":"string","productId":"string","version":"string","status":"draft","notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1BomHeadersById","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update BOM header","description":"Aendert einen Stuecklisten-Kopf; nicht gesendete Felder bleiben erhalten. Die Antwort enthaelt den Kopf OHNE Positionen. Ein Wechsel auf \"obsolete\" macht die Stueckliste nicht unsichtbar und sperrt sie nirgends — sie laesst sich weiterhin aufloesen und bearbeiten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"version":{"type":"string","maxLength":30,"default":"1.0"},"status":{"type":"string","enum":["draft","active","obsolete"],"default":"draft"},"validFrom":{"type":"string"},"notes":{"type":"string"}}},"example":{"productId":"00000000-0000-4000-8000-000000000000","version":"string","status":"draft","validFrom":"string","notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Geloescht — nur die getroffene ID (Positionen gehen per Cascade mit)","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1BomHeadersById","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete BOM header","description":"ENDGUELTIGES Loeschen: die Zeile wird entfernt, nicht als geloescht markiert, und ALLE Positionen gehen per Cascade mit. Fertigungsauftraege, die ueber `bomId` auf diesen Kopf zeigen, bleiben bestehen und verweisen danach ins Leere."}},"/api/v1/bom/headers/{id}/lines":{"get":{"responses":{"200":{"description":"BOM-Positionen — NACKTES Array, kein data/pagination-Umschlag","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"bomId":{"type":"string"},"componentProductId":{"type":["string","null"]},"componentName":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"position":{"type":"integer"},"unitCost":{"type":["number","null"]},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","bomId","componentProductId","componentName","quantity","unit","position","unitCost","notes"],"additionalProperties":false}},"example":[{"id":"string","bomId":"string","componentProductId":"string","componentName":"string","quantity":0,"unit":"string","position":0,"unitCost":0,"notes":"string"}]}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1BomHeadersByIdLines","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List BOM lines","description":"Positionen eines Stuecklisten-Kopfs, nach `position` sortiert. Antwortet mit einem NACKTEN Array ohne `data`/`pagination`-Umschlag und ohne Blaetterung. Ein unbekannter Kopf ergibt kein 404, sondern ein leeres Array."},"post":{"responses":{"201":{"description":"Angelegte BOM-Position","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"bomId":{"type":"string"},"componentProductId":{"type":["string","null"]},"componentName":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"position":{"type":"integer"},"unitCost":{"type":["number","null"]},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","bomId","componentProductId","componentName","quantity","unit","position","unitCost","notes"],"additionalProperties":false},"example":{"id":"string","bomId":"string","componentProductId":"string","componentName":"string","quantity":0,"unit":"string","position":0,"unitCost":0,"notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1BomHeadersByIdLines","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create BOM line","description":"Haengt eine Position an einen Stuecklisten-Kopf. Ein UNBEKANNTER Kopf laeuft in den Fremdschluessel und wird als 503 beantwortet, nicht als 404 oder 400 — die Position entsteht dann nicht. `unitCost` ist optional; fehlt sie, laesst der Kostenrollup diese Position aus und zaehlt sie unter `linesWithoutCost`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"componentProductId":{"type":"string","format":"uuid"},"componentName":{"type":"string"},"quantity":{"type":"number","exclusiveMinimum":0,"default":1},"unit":{"type":"string","maxLength":30,"default":"Stück"},"position":{"type":"integer","minimum":0,"default":0},"unitCost":{"type":"number","minimum":0},"notes":{"type":"string"}}},"example":{"componentProductId":"00000000-0000-4000-8000-000000000000","componentName":"string","quantity":1,"unit":"string","position":0,"unitCost":0,"notes":"string"}}}}}},"/api/v1/bom/headers/{headerId}/lines/{lineId}":{"put":{"responses":{"200":{"description":"Aktualisierte BOM-Position","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"bomId":{"type":"string"},"componentProductId":{"type":["string","null"]},"componentName":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"position":{"type":"integer"},"unitCost":{"type":["number","null"]},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","bomId","componentProductId","componentName","quantity","unit","position","unitCost","notes"],"additionalProperties":false},"example":{"id":"string","bomId":"string","componentProductId":"string","componentName":"string","quantity":0,"unit":"string","position":0,"unitCost":0,"notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1BomHeadersByHeaderIdLinesByLineId","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"headerId","required":true},{"schema":{"type":"string"},"in":"path","name":"lineId","required":true}],"summary":"Update BOM line","description":"Aendert eine Stuecklisten-Position; nicht gesendete Felder bleiben erhalten. ACHTUNG: der Header-Anteil des Pfades wird NICHT geprueft — die Position wird allein ueber `lineId` gefunden, ein beliebiger `headerId` fuehrt zum selben Ergebnis. Die Position laesst sich hier nicht auf einen anderen Kopf umhaengen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"componentProductId":{"type":"string","format":"uuid"},"componentName":{"type":"string"},"quantity":{"type":"number","exclusiveMinimum":0,"default":1},"unit":{"type":"string","maxLength":30,"default":"Stück"},"position":{"type":"integer","minimum":0,"default":0},"unitCost":{"type":"number","minimum":0},"notes":{"type":"string"}}},"example":{"componentProductId":"00000000-0000-4000-8000-000000000000","componentName":"string","quantity":1,"unit":"string","position":0,"unitCost":0,"notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Geloescht — nur die getroffene Positions-ID","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1BomHeadersByHeaderIdLinesByLineId","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"headerId","required":true},{"schema":{"type":"string"},"in":"path","name":"lineId","required":true}],"summary":"Delete BOM line","description":"ENDGUELTIGES Loeschen einer Stuecklisten-Position, kein Soft-Delete. Auch hier wird der Header-Anteil des Pfades NICHT geprueft: es zaehlt allein `lineId`."}},"/api/v1/bom/{id}/explode":{"get":{"responses":{"200":{"description":"Explodierte BOM — Positionen mit `level` und `path`, Mengen bereits mit der Elternmenge multipliziert (max. 10 Stufen)","content":{"application/json":{"schema":{"type":"object","properties":{"bomId":{"type":"string"},"header":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"version":{"type":"string"},"status":{"type":"string","enum":["draft","active","obsolete"]},"validFrom":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","version","status","notes"],"additionalProperties":false},"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"componentProductId":{"type":["string","null"]},"componentName":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"position":{"type":"integer"},"unitCost":{"type":["number","null"]},"level":{"type":"integer"},"path":{"type":"string","description":"Pfad der durchlaufenen Stuecklisten, verkettet mit '>'"}},"required":["id","componentProductId","componentName","quantity","unit","position","unitCost","level","path"],"additionalProperties":false}}},"required":["bomId","header","lines"],"additionalProperties":false},"example":{"bomId":"string","header":{"id":"string","productId":"string","version":"string","status":"draft","notes":"string"},"lines":[{"id":"string","componentProductId":"string","componentName":"string","quantity":0,"unit":"string","position":0,"unitCost":0,"level":0,"path":"string"}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1BomByIdExplode","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Explode BOM multi-level","description":"Loest eine Stueckliste ueber alle Stufen auf. `{id}` ist die ID eines BOM-HEADERS, nicht eine Teilenummer und nicht die ID eines Alt-Eintrags aus GET /bom. Jede Position traegt `level` (0 = direkt) und `path`, die Mengen sind bereits mit der Elternmenge multipliziert. Eine Unterstufe wird nur gefunden, wenn ein Header auf dasselbe Produkt zeigt wie die Komponente. Die Rekursion bricht nach 10 Stufen ab — tiefere Zweige fehlen dann WORTLOS in der Antwort."}},"/api/v1/bom/{id}/cost-rollup":{"get":{"responses":{"200":{"description":"Kostenrollup — nur DIREKTE Positionen (nicht rekursiv). `linesWithoutCost` sagt, wie viele Positionen mangels Stueckkosten fehlen","content":{"application/json":{"schema":{"type":"object","properties":{"bomId":{"type":"string"},"header":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"version":{"type":"string"},"status":{"type":"string","enum":["draft","active","obsolete"]},"validFrom":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","version","status","notes"],"additionalProperties":false},"totalDirectCost":{"type":"number"},"lineCount":{"type":"integer"},"linesWithoutCost":{"type":"integer"},"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"bomId":{"type":"string"},"componentProductId":{"type":["string","null"]},"componentName":{"type":["string","null"]},"quantity":{"type":"number"},"unit":{"type":"string"},"position":{"type":"integer"},"unitCost":{"type":["number","null"]},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{},"lineCost":{"type":["number","null"]}},"required":["id","bomId","componentProductId","componentName","quantity","unit","position","unitCost","notes","lineCost"],"additionalProperties":false}}},"required":["bomId","header","totalDirectCost","lineCount","linesWithoutCost","lines"],"additionalProperties":false},"example":{"bomId":"string","header":{"id":"string","productId":"string","version":"string","status":"draft","notes":"string"},"totalDirectCost":0,"lineCount":0,"linesWithoutCost":0,"lines":[{"id":"string","bomId":"string","componentProductId":"string","componentName":"string","quantity":0,"unit":"string","position":0,"unitCost":0,"notes":"string","lineCost":0}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1BomByIdCost-rollup","tags":["bom"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Roll up BOM cost","description":"Kalkulation ueber eine Stueckliste. `{id}` ist die ID eines BOM-HEADERS. Trotz des Namens NICHT rekursiv: gerechnet werden nur die direkten Positionen dieses Kopfes, Unterstuecklisten fliessen nicht ein. `totalDirectCost` summiert ausserdem nur Positionen MIT hinterlegten Stueckkosten; wie viele mangels Preis uebergangen wurden, sagt `linesWithoutCost` — eine Summe ohne diese Zahl zu lesen, ist der Fehler, den dieser Endpunkt verhindern soll."}},"/api/v1/manufacturing/work-orders":{"get":{"responses":{"200":{"description":"Liste Work Orders","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"productId":{},"bomId":{},"plannedQty":{"type":"number"},"status":{},"plannedStart":{},"plannedEnd":{},"actualStart":{},"actualEnd":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["total","limit","offset"]}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"plannedQty":0}],"pagination":{"total":0,"limit":0,"offset":0}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1ManufacturingWork-orders","tags":["manufacturing"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","released","in_progress","completed","cancelled"]}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"List work orders (Fertigungsauftraege)","description":"Liste der Fertigungsauftraege, neueste zuerst. `search` durchsucht AUSSCHLIESSLICH das Notizfeld — es gibt keine Auftragsnummer, ein Auftrag wird ueber seine UUID angesprochen. Diese Tabelle kennt kein Soft-Delete."},"post":{"responses":{"201":{"description":"Work Order angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"productId":{},"bomId":{},"plannedQty":{"type":"number"},"status":{},"plannedStart":{},"plannedEnd":{},"actualStart":{},"actualEnd":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty"],"additionalProperties":false},"example":{"plannedQty":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1ManufacturingWork-orders","tags":["manufacturing"],"parameters":[],"summary":"Create work order","description":"Legt einen Fertigungsauftrag an (Vorgabestatus \"draft\"). Der Auftrag entsteht LEER: ein angegebener `bomId` wird NICHT aufgeloest, Arbeitsgaenge und Material muessen einzeln ueber die Unterrouten angelegt werden. Es wird auch nicht geprueft, ob Produkt oder Stueckliste ueberhaupt existieren.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"bomId":{"type":"string","format":"uuid"},"plannedQty":{"type":"number","exclusiveMinimum":0,"default":1},"status":{"type":"string","enum":["draft","released","in_progress","completed","cancelled"],"default":"draft"},"plannedStart":{"type":"string"},"plannedEnd":{"type":"string"},"notes":{"type":"string"}}},"example":{"productId":"00000000-0000-4000-8000-000000000000","bomId":"00000000-0000-4000-8000-000000000000","plannedQty":1,"status":"draft","plannedStart":"string","plannedEnd":"string","notes":"string"}}}}}},"/api/v1/manufacturing/work-orders/{id}":{"get":{"responses":{"200":{"description":"Work Order","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"productId":{},"bomId":{},"plannedQty":{"type":"number"},"status":{},"plannedStart":{},"plannedEnd":{},"actualStart":{},"actualEnd":{},"notes":{},"createdAt":{},"updatedAt":{},"operations":{"type":"array","items":{"type":"object","properties":{"id":{},"workOrderId":{},"operation":{},"resource":{},"plannedHours":{"type":"number"},"actualHours":{"type":"number"},"status":{},"sequence":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["plannedHours","actualHours","sequence"],"additionalProperties":false}},"materials":{"type":"array","items":{"type":"object","properties":{"id":{},"workOrderId":{},"productId":{},"productName":{},"plannedQty":{"type":"number"},"issuedQty":{"type":"number"},"unit":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty","issuedQty"],"additionalProperties":false}}},"required":["plannedQty","operations","materials"],"additionalProperties":false},"example":{"plannedQty":0,"operations":[{"plannedHours":0,"actualHours":0,"sequence":0}],"materials":[{"plannedQty":0,"issuedQty":0}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1ManufacturingWork-ordersById","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get work order with operations and materials","description":"Ein Fertigungsauftrag samt seiner Arbeitsgaenge (`operations`, nach Reihenfolge) und seiner Materialzuordnung (`materials`) in EINER Antwort — beide flach angehaengt, ohne Blaetterung."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"productId":{},"bomId":{},"plannedQty":{"type":"number"},"status":{},"plannedStart":{},"plannedEnd":{},"actualStart":{},"actualEnd":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty"],"additionalProperties":false},"example":{"plannedQty":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1ManufacturingWork-ordersById","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update work order","description":"Aendert einen Fertigungsauftrag; nicht gesendete Felder bleiben erhalten. ACHTUNG: `status` ist hier FREI setzbar und umgeht damit die Pruefungen von start, complete und cancel — ein Auftrag laesst sich so direkt von \"draft\" auf \"completed\" setzen, ohne dass `actualStart`/`actualEnd` gefuellt werden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"bomId":{"type":"string","format":"uuid"},"plannedQty":{"type":"number","exclusiveMinimum":0,"default":1},"status":{"type":"string","enum":["draft","released","in_progress","completed","cancelled"],"default":"draft"},"plannedStart":{"type":"string"},"plannedEnd":{"type":"string"},"notes":{"type":"string"}}},"example":{"productId":"00000000-0000-4000-8000-000000000000","bomId":"00000000-0000-4000-8000-000000000000","plannedQty":1,"status":"draft","plannedStart":"string","plannedEnd":"string","notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1ManufacturingWork-ordersById","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete work order","description":"ENDGUELTIGES Loeschen: die Zeile wird entfernt, nicht als geloescht markiert, und ALLE Arbeitsgaenge und Materialzuordnungen gehen per Cascade mit — die erfassten Ist-Stunden sind damit weg. Der Status wird nicht geprueft: auch ein laufender oder abgeschlossener Auftrag laesst sich loeschen."}},"/api/v1/manufacturing/work-orders/{id}/start":{"post":{"responses":{"200":{"description":"Gestartet","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"productId":{},"bomId":{},"plannedQty":{"type":"number"},"status":{},"plannedStart":{},"plannedEnd":{},"actualStart":{},"actualEnd":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty"],"additionalProperties":false},"example":{"plannedQty":0}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"409":{"description":"Ungueltige Status-Transition"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1ManufacturingWork-ordersByIdStart","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Start work order","description":"Startet einen Fertigungsauftrag: Status auf \"in_progress\", `actualStart` auf das heutige Datum. Nur aus \"draft\" oder \"released\" moeglich, sonst 409. Antwortet mit 200, nicht 201. Es wird KEIN Material entnommen und kein Lagerbestand beruehrt."}},"/api/v1/manufacturing/work-orders/{id}/complete":{"post":{"responses":{"200":{"description":"Abgeschlossen","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"productId":{},"bomId":{},"plannedQty":{"type":"number"},"status":{},"plannedStart":{},"plannedEnd":{},"actualStart":{},"actualEnd":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty"],"additionalProperties":false},"example":{"plannedQty":0}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"409":{"description":"Ungueltige Status-Transition"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1ManufacturingWork-ordersByIdComplete","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Complete work order","description":"Schliesst einen Fertigungsauftrag ab: Status auf \"completed\", `actualEnd` auf das heutige Datum. Nur aus \"in_progress\" moeglich, sonst 409. Antwortet mit 200, nicht 201. Der Abschluss bucht KEINE Fertigmeldung: es entsteht kein Zugang des gefertigten Produkts, kein Materialverbrauch und keine Journalbuchung. Auch ob alle Arbeitsgaenge fertig sind, wird nicht geprueft."}},"/api/v1/manufacturing/work-orders/{id}/cancel":{"post":{"responses":{"200":{"description":"Storniert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"productId":{},"bomId":{},"plannedQty":{"type":"number"},"status":{},"plannedStart":{},"plannedEnd":{},"actualStart":{},"actualEnd":{},"notes":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty"],"additionalProperties":false},"example":{"plannedQty":0}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"409":{"description":"Bereits abgeschlossen"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1ManufacturingWork-ordersByIdCancel","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Cancel work order","description":"Setzt einen Fertigungsauftrag auf \"cancelled\". Aus \"completed\" oder \"cancelled\" heraus nicht moeglich (409), aus jedem anderen Status schon. Antwortet mit 200, nicht 201. Storniert wird NUR der Status: erfasste Ist-Stunden und ausgegebenes Material bleiben stehen, nichts wird zurueckgebucht."}},"/api/v1/manufacturing/work-orders/{id}/cost-actual":{"get":{"responses":{"200":{"description":"Ist-Kosten","content":{"application/json":{"schema":{"type":"object","properties":{"workOrderId":{"type":"string"},"totalActualHours":{"type":"number"},"labourCost":{"type":"number"},"materialCost":{"type":"number"},"totalCost":{"type":"number"},"materials":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["workOrderId","totalActualHours","labourCost","materialCost","totalCost","materials"],"additionalProperties":false},"example":{"workOrderId":"string","totalActualHours":0,"labourCost":0,"materialCost":0,"totalCost":0,"materials":[{}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1ManufacturingWork-ordersByIdCost-actual","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get work order actual cost","description":"Ist-Kosten eines Fertigungsauftrags aus Arbeit und Material. WIE GERECHNET WIRD, muss man wissen, um die Zahl lesen zu koennen: der Stundensatz steckt im NAMEN des Arbeitsplatzes als Suffix \"@75.00\" (z. B. \"Schweissplatz 1 @75.00\"); fehlt er, rechnet der Server mit fest verdrahteten 50,00 EUR/h. Es gibt keine Stammdaten fuer Stundensaetze. Materialkosten sind `issuedQty` mal Produktpreis; ist kein Produkt hinterlegt oder die Produkttabelle im Mandanten nicht vorhanden, geht der Preis STILL mit 0 ein und die Summe faellt zu niedrig aus, ohne dass die Antwort darauf hinweist. Gemeinkosten und Ruestzeiten fehlen ganz."}},"/api/v1/manufacturing/work-orders/{id}/operations":{"get":{"responses":{"200":{"description":"Arbeitsgaenge","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{},"workOrderId":{},"operation":{},"resource":{},"plannedHours":{"type":"number"},"actualHours":{"type":"number"},"status":{},"sequence":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["plannedHours","actualHours","sequence"],"additionalProperties":false}},"example":[{"plannedHours":0,"actualHours":0,"sequence":0}]}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1ManufacturingWork-ordersByIdOperations","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List work order operations","description":"Arbeitsgaenge eines Fertigungsauftrags, nach `sequence` sortiert. Antwortet mit einem NACKTEN Array ohne `data`/`pagination`-Umschlag. Ein unbekannter Auftrag ergibt kein 404, sondern ein leeres Array."},"post":{"responses":{"201":{"description":"Arbeitsgang angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"workOrderId":{},"operation":{},"resource":{},"plannedHours":{"type":"number"},"actualHours":{"type":"number"},"status":{},"sequence":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["plannedHours","actualHours","sequence"],"additionalProperties":false},"example":{"plannedHours":0,"actualHours":0,"sequence":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1ManufacturingWork-ordersByIdOperations","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create work order operation","description":"Legt einen Arbeitsgang an. Ein UNBEKANNTER Fertigungsauftrag laeuft in den Fremdschluessel und wird als 503 beantwortet, nicht als 404 oder 400 — der Arbeitsgang entsteht dann nicht. Der Stundensatz gehoert als Suffix \"@75.00\" in `resource`, sonst rechnen die Ist-Kosten mit 50,00 EUR/h.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"operation":{"type":"string","minLength":1,"maxLength":255},"resource":{"type":"string"},"plannedHours":{"type":"number","minimum":0,"default":0},"actualHours":{"type":"number","minimum":0,"default":0},"status":{"type":"string","enum":["pending","in_progress","completed"],"default":"pending"},"sequence":{"type":"integer","minimum":0,"default":0}},"required":["operation"]},"example":{"operation":"string","resource":"string","plannedHours":0,"actualHours":0,"status":"pending","sequence":0}}}}}},"/api/v1/manufacturing/work-orders/{woId}/operations/{opId}":{"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"workOrderId":{},"operation":{},"resource":{},"plannedHours":{"type":"number"},"actualHours":{"type":"number"},"status":{},"sequence":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["plannedHours","actualHours","sequence"],"additionalProperties":false},"example":{"plannedHours":0,"actualHours":0,"sequence":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1ManufacturingWork-ordersByWoIdOperationsByOpId","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"woId","required":true},{"schema":{"type":"string"},"in":"path","name":"opId","required":true}],"summary":"Update work order operation","description":"Aendert einen Arbeitsgang; nicht gesendete Felder bleiben erhalten. Hier werden die Ist-Stunden (`actualHours`) erfasst, die in die Ist-Kosten eingehen. ACHTUNG: der Auftragsanteil des Pfades wird NICHT geprueft — der Arbeitsgang wird allein ueber `opId` gefunden, ein beliebiger `woId` fuehrt zum selben Ergebnis.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"operation":{"type":"string","minLength":1,"maxLength":255},"resource":{"type":"string"},"plannedHours":{"type":"number","minimum":0,"default":0},"actualHours":{"type":"number","minimum":0,"default":0},"status":{"type":"string","enum":["pending","in_progress","completed"],"default":"pending"},"sequence":{"type":"integer","minimum":0,"default":0}}},"example":{"operation":"string","resource":"string","plannedHours":0,"actualHours":0,"status":"pending","sequence":0}}}}}},"/api/v1/manufacturing/work-orders/{id}/materials":{"get":{"responses":{"200":{"description":"Materialien","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{},"workOrderId":{},"productId":{},"productName":{},"plannedQty":{"type":"number"},"issuedQty":{"type":"number"},"unit":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty","issuedQty"],"additionalProperties":false}},"example":[{"plannedQty":0,"issuedQty":0}]}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1ManufacturingWork-ordersByIdMaterials","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List work order materials","description":"Materialzuordnung eines Fertigungsauftrags mit Soll- (`plannedQty`) und Ist-Menge (`issuedQty`). Antwortet mit einem NACKTEN Array ohne `data`/`pagination`-Umschlag; ein unbekannter Auftrag ergibt ein leeres Array."},"post":{"responses":{"201":{"description":"Material hinzugefuegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"workOrderId":{},"productId":{},"productName":{},"plannedQty":{"type":"number"},"issuedQty":{"type":"number"},"unit":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty","issuedQty"],"additionalProperties":false},"example":{"plannedQty":0,"issuedQty":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1ManufacturingWork-ordersByIdMaterials","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Add material to work order","description":"Nimmt eine Materialposition in den Fertigungsauftrag auf. Ein UNBEKANNTER Auftrag laeuft in den Fremdschluessel und wird als 503 beantwortet, nicht als 404 oder 400. Ein hier direkt gesetztes `issuedQty` gilt sofort als ausgegeben, ohne dass ein Lagerbestand geprueft oder veraendert wird.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"productName":{"type":"string"},"plannedQty":{"type":"number","minimum":0,"default":0},"issuedQty":{"type":"number","minimum":0,"default":0},"unit":{"type":"string","default":"Stück"}}},"example":{"productId":"00000000-0000-4000-8000-000000000000","productName":"string","plannedQty":0,"issuedQty":0,"unit":"string"}}}}}},"/api/v1/manufacturing/work-orders/{woId}/materials/{matId}":{"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"workOrderId":{},"productId":{},"productName":{},"plannedQty":{"type":"number"},"issuedQty":{"type":"number"},"unit":{},"createdAt":{},"updatedAt":{}},"required":["plannedQty","issuedQty"],"additionalProperties":false},"example":{"plannedQty":0,"issuedQty":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar — spaeter erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1ManufacturingWork-ordersByWoIdMaterialsByMatId","tags":["manufacturing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"woId","required":true},{"schema":{"type":"string"},"in":"path","name":"matId","required":true}],"summary":"Update work order material","description":"Aendert eine Materialposition; nicht gesendete Felder bleiben erhalten. Hier wird die Materialentnahme erfasst (`issuedQty`), und zwar als ABSOLUTE Menge, nicht als Zubuchung. Der Lagerbestand bleibt dabei UNBERUEHRT: es entsteht keine Lagerbewegung und keine Journalbuchung, die Zahl wirkt nur auf die Ist-Kosten. ACHTUNG: der Auftragsanteil des Pfades wird NICHT geprueft — es zaehlt allein `matId`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"productName":{"type":"string"},"plannedQty":{"type":"number","minimum":0,"default":0},"issuedQty":{"type":"number","minimum":0,"default":0},"unit":{"type":"string","default":"Stück"}}},"example":{"productId":"00000000-0000-4000-8000-000000000000","productName":"string","plannedQty":0,"issuedQty":0,"unit":"string"}}}}}},"/api/v1/lot-tracking/lots":{"get":{"responses":{"200":{"description":"Chargen-Liste mit Blaetterung","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"lotNumber":{"type":"string"},"productionDate":{},"expiryDate":{},"supplierLot":{"type":["string","null"]},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"]},"qtyOnHand":{"type":"number"},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","lotNumber","supplierLot","status","qtyOnHand","notes"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","productId":"string","lotNumber":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Lot-trackingLots","tags":["lot-tracking"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"productId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"List lots (Chargen)","description":"Liste aller Chargen des Mandanten, neueste zuerst. `search` sucht in der Chargennummer. Diese Tabelle kennt kein Soft-Delete — was hier fehlt, ist endgueltig geloescht."},"post":{"responses":{"201":{"description":"Angelegte Charge","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"lotNumber":{"type":"string"},"productionDate":{},"expiryDate":{},"supplierLot":{"type":["string","null"]},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"]},"qtyOnHand":{"type":"number"},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","lotNumber","supplierLot","status","qtyOnHand","notes"],"additionalProperties":false},"example":{"id":"string","productId":"string","lotNumber":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Lot-trackingLots","tags":["lot-tracking"],"parameters":[],"summary":"Create lot","description":"Legt eine Charge an. Ein hier gesetztes `qtyOnHand` ist der Anfangsbestand und wird OHNE Bewegung geschrieben — unter /lots/{id}/movements taucht er nicht auf. Die Chargennummer wird nicht auf Eindeutigkeit geprueft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"lotNumber":{"type":"string","minLength":1,"maxLength":100},"productionDate":{"type":"string"},"expiryDate":{"type":"string"},"supplierLot":{"type":"string"},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"],"default":"active"},"qtyOnHand":{"type":"number","minimum":0,"default":0},"notes":{"type":"string"}},"required":["lotNumber"]},"example":{"productId":"00000000-0000-4000-8000-000000000000","lotNumber":"string","productionDate":"string","expiryDate":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"}}}}}},"/api/v1/lot-tracking/lots/expiring":{"get":{"responses":{"200":{"description":"Ablaufende Chargen, aufsteigend nach Ablaufdatum","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"lotNumber":{"type":"string"},"productionDate":{},"expiryDate":{},"supplierLot":{"type":["string","null"]},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"]},"qtyOnHand":{"type":"number"},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","lotNumber","supplierLot","status","qtyOnHand","notes"],"additionalProperties":false}},"withinDays":{"type":"integer"}},"required":["data","withinDays"],"additionalProperties":false},"example":{"data":[{"id":"string","productId":"string","lotNumber":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"}],"withinDays":0}}}},"400":{"description":"Ungültiger Zeitraum"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Lot-trackingLotsExpiring","tags":["lot-tracking"],"parameters":[{"in":"query","name":"days","schema":{"type":"integer","minimum":1,"maximum":3650,"default":30}}],"summary":"List lots expiring soon","description":"Chargen, die innerhalb der naechsten `days` Tage ablaufen (Vorgabe 30, hoechstens 3650), aufsteigend nach Ablaufdatum. Bereits ABGELAUFENE Chargen sind NICHT enthalten, ebenso wenig Chargen ohne Ablaufdatum. Antwortet mit `{ data, withinDays }` statt mit dem sonst ueblichen Blaetterungs-Umschlag."}},"/api/v1/lot-tracking/lots/{id}":{"get":{"responses":{"200":{"description":"Charge","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"lotNumber":{"type":"string"},"productionDate":{},"expiryDate":{},"supplierLot":{"type":["string","null"]},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"]},"qtyOnHand":{"type":"number"},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","lotNumber","supplierLot","status","qtyOnHand","notes"],"additionalProperties":false},"example":{"id":"string","productId":"string","lotNumber":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Lot-trackingLotsById","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get lot","description":"Eine einzelne Charge mit ihrem aktuellen Bestand (`qtyOnHand`)."},"put":{"responses":{"200":{"description":"Aktualisierte Charge","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"lotNumber":{"type":"string"},"productionDate":{},"expiryDate":{},"supplierLot":{"type":["string","null"]},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"]},"qtyOnHand":{"type":"number"},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","lotNumber","supplierLot","status","qtyOnHand","notes"],"additionalProperties":false},"example":{"id":"string","productId":"string","lotNumber":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1Lot-trackingLotsById","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update lot","description":"Aendert Stammdaten einer Charge; nicht gesendete Felder bleiben erhalten. ACHTUNG Bestand: ein hier gesetztes `qtyOnHand` UEBERSCHREIBT den Bestand direkt, statt ihn fortzuschreiben, und erzeugt dabei KEINE Bewegung — die Aenderung ist unter /lots/{id}/movements spaeter nicht nachvollziehbar. Fuer nachvollziehbare Bestandsaenderungen POST /lots/{id}/movements verwenden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"lotNumber":{"type":"string","minLength":1,"maxLength":100},"productionDate":{"type":"string"},"expiryDate":{"type":"string"},"supplierLot":{"type":"string"},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"],"default":"active"},"qtyOnHand":{"type":"number","minimum":0,"default":0},"notes":{"type":"string"}}},"example":{"productId":"00000000-0000-4000-8000-000000000000","lotNumber":"string","productionDate":"string","expiryDate":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Geloescht — nur die getroffene ID","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1Lot-trackingLotsById","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete lot","description":"ENDGUELTIGES Loeschen: die Zeile wird entfernt, nicht als geloescht markiert, und ALLE Bewegungen dieser Charge gehen per Cascade mit — die Rueckverfolgung ist danach unwiederbringlich weg. Zugeordnete Seriennummern bleiben bestehen und verlieren nur ihren Chargenbezug (`lotId` wird null)."}},"/api/v1/lot-tracking/lots/{id}/trace-forward":{"get":{"responses":{"200":{"description":"Charge, Abgangs-Bewegungen und zugehoerige Seriennummern","content":{"application/json":{"schema":{"type":"object","properties":{"lot":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"lotNumber":{"type":"string"},"productionDate":{},"expiryDate":{},"supplierLot":{"type":["string","null"]},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"]},"qtyOnHand":{"type":"number"},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","lotNumber","supplierLot","status","qtyOnHand","notes"],"additionalProperties":false},"outgoingMovements":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"lotId":{"type":"string"},"movementType":{"type":"string","enum":["receipt","issue","transfer","adjust","recall","scrap"]},"quantity":{"type":"number"},"referenceType":{"type":["string","null"]},"referenceId":{"type":["string","null"]},"locationFrom":{"type":["string","null"]},"locationTo":{"type":["string","null"]},"date":{},"notes":{"type":["string","null"]},"createdAt":{}},"required":["id","lotId","movementType","quantity","referenceType","referenceId","locationFrom","locationTo","notes"],"additionalProperties":false}},"serialNumbers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"serialNumber":{"type":"string"},"status":{"type":"string","enum":["in_stock","sold","returned","scrapped","recalled"]},"lotId":{"type":["string","null"]},"customerId":{"type":["string","null"]},"soldDate":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","serialNumber","status","lotId","customerId","notes"],"additionalProperties":false}}},"required":["lot","outgoingMovements","serialNumbers"],"additionalProperties":false},"example":{"lot":{"id":"string","productId":"string","lotNumber":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"},"outgoingMovements":[{"id":"string","lotId":"string","movementType":"receipt","quantity":0,"referenceType":"string","referenceId":"string","locationFrom":"string","locationTo":"string","notes":"string"}],"serialNumbers":[{"id":"string","productId":"string","serialNumber":"string","status":"in_stock","lotId":"string","customerId":"string","notes":"string"}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Lot-trackingLotsByIdTrace-forward","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Trace lot forward","description":"Vorwaerts-Rueckverfolgung: wohin ist diese Charge gegangen? Liefert die Charge, ihre Abgangsbewegungen (issue, transfer, recall, scrap) und alle Seriennummern, die auf sie zeigen. Der Kunde steht nicht in der Bewegung, sondern nur an der Seriennummer (`customerId`)."}},"/api/v1/lot-tracking/lots/{id}/trace-backward":{"get":{"responses":{"200":{"description":"Charge und Zugangs-Bewegungen","content":{"application/json":{"schema":{"type":"object","properties":{"lot":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"lotNumber":{"type":"string"},"productionDate":{},"expiryDate":{},"supplierLot":{"type":["string","null"]},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"]},"qtyOnHand":{"type":"number"},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","lotNumber","supplierLot","status","qtyOnHand","notes"],"additionalProperties":false},"incomingMovements":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"lotId":{"type":"string"},"movementType":{"type":"string","enum":["receipt","issue","transfer","adjust","recall","scrap"]},"quantity":{"type":"number"},"referenceType":{"type":["string","null"]},"referenceId":{"type":["string","null"]},"locationFrom":{"type":["string","null"]},"locationTo":{"type":["string","null"]},"date":{},"notes":{"type":["string","null"]},"createdAt":{}},"required":["id","lotId","movementType","quantity","referenceType","referenceId","locationFrom","locationTo","notes"],"additionalProperties":false}}},"required":["lot","incomingMovements"],"additionalProperties":false},"example":{"lot":{"id":"string","productId":"string","lotNumber":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"},"incomingMovements":[{"id":"string","lotId":"string","movementType":"receipt","quantity":0,"referenceType":"string","referenceId":"string","locationFrom":"string","locationTo":"string","notes":"string"}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Lot-trackingLotsByIdTrace-backward","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Trace lot backward","description":"Rueckwaerts-Rueckverfolgung: woher kommt diese Charge? Liefert die Charge und ihre Zugangsbewegungen (receipt, adjust). Der Lieferant steht nicht in der Bewegung, sondern als `supplierLot` an der Charge selbst."}},"/api/v1/lot-tracking/lots/{id}/recall":{"post":{"responses":{"200":{"description":"Rueckruf eingeleitet — Quittung plus die Charge in ihrem neuen Zustand","content":{"application/json":{"schema":{"type":"object","properties":{"recalled":{"type":"boolean","const":true},"lot":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"lotNumber":{"type":"string"},"productionDate":{},"expiryDate":{},"supplierLot":{"type":["string","null"]},"status":{"type":"string","enum":["active","quarantine","recalled","consumed","expired"]},"qtyOnHand":{"type":"number"},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","lotNumber","supplierLot","status","qtyOnHand","notes"],"additionalProperties":false}},"required":["recalled","lot"],"additionalProperties":false},"example":{"recalled":true,"lot":{"id":"string","productId":"string","lotNumber":"string","supplierLot":"string","status":"active","qtyOnHand":0,"notes":"string"}}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Lot-trackingLotsByIdRecall","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Recall lot","description":"Leitet einen Rueckruf ein und aendert dabei DREI Dinge: die Charge geht auf \"recalled\", ALLE Seriennummern dieser Charge gehen ebenfalls auf \"recalled\", und es wird eine recall-Bewegung ueber den vollen aktuellen Bestand gebucht. Der Bestand `qtyOnHand` selbst bleibt dabei UNVERAENDERT. Antwortet mit 200, nicht 201. Es gibt keinen Endpunkt, der einen Rueckruf zuruecknimmt: Status per PUT von Hand zuruecksetzen, die gebuchte Bewegung bleibt stehen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}},"example":{"reason":"string"}}}}}},"/api/v1/lot-tracking/lots/{id}/movements":{"get":{"responses":{"200":{"description":"Bewegungen — NACKTES Array, kein data/pagination-Umschlag","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"lotId":{"type":"string"},"movementType":{"type":"string","enum":["receipt","issue","transfer","adjust","recall","scrap"]},"quantity":{"type":"number"},"referenceType":{"type":["string","null"]},"referenceId":{"type":["string","null"]},"locationFrom":{"type":["string","null"]},"locationTo":{"type":["string","null"]},"date":{},"notes":{"type":["string","null"]},"createdAt":{}},"required":["id","lotId","movementType","quantity","referenceType","referenceId","locationFrom","locationTo","notes"],"additionalProperties":false}},"example":[{"id":"string","lotId":"string","movementType":"receipt","quantity":0,"referenceType":"string","referenceId":"string","locationFrom":"string","locationTo":"string","notes":"string"}]}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Lot-trackingLotsByIdMovements","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List lot movements","description":"Alle Bewegungen einer Charge, neueste zuerst. Antwortet mit einem NACKTEN Array ohne `data`/`pagination`-Umschlag und ohne Blaetterung — bei langlebigen Chargen kann die Antwort entsprechend gross werden."},"post":{"responses":{"201":{"description":"Gebuchte Bewegung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"lotId":{"type":"string"},"movementType":{"type":"string","enum":["receipt","issue","transfer","adjust","recall","scrap"]},"quantity":{"type":"number"},"referenceType":{"type":["string","null"]},"referenceId":{"type":["string","null"]},"locationFrom":{"type":["string","null"]},"locationTo":{"type":["string","null"]},"date":{},"notes":{"type":["string","null"]},"createdAt":{}},"required":["id","lotId","movementType","quantity","referenceType","referenceId","locationFrom","locationTo","notes"],"additionalProperties":false},"example":{"id":"string","lotId":"string","movementType":"receipt","quantity":0,"referenceType":"string","referenceId":"string","locationFrom":"string","locationTo":"string","notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Lot-trackingLotsByIdMovements","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Book lot movement","description":"Bucht eine Bewegung und SCHREIBT DEN BESTAND der Charge fort: `receipt` und `adjust` erhoehen `qtyOnHand` um die Menge, alle uebrigen Arten (issue, transfer, recall, scrap) vermindern ihn um deren Betrag — ein negatives `quantity` mindert dort also ebenfalls. Der Bestand wird nicht gegen 0 geprueft und kann negativ werden. Es gibt keinen Endpunkt zum Loeschen einer Bewegung: rueckgaengig nur durch eine Gegenbuchung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"movementType":{"type":"string","enum":["receipt","issue","transfer","adjust","recall","scrap"]},"quantity":{"type":"number"},"referenceType":{"type":"string"},"referenceId":{"type":"string","format":"uuid"},"locationFrom":{"type":"string"},"locationTo":{"type":"string"},"date":{"type":"string"},"notes":{"type":"string"}},"required":["movementType","quantity"]},"example":{"movementType":"receipt","quantity":0,"referenceType":"string","referenceId":"00000000-0000-4000-8000-000000000000","locationFrom":"string","locationTo":"string","date":"string","notes":"string"}}}}}},"/api/v1/lot-tracking/serials":{"get":{"responses":{"200":{"description":"Seriennummern-Liste mit Blaetterung","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"serialNumber":{"type":"string"},"status":{"type":"string","enum":["in_stock","sold","returned","scrapped","recalled"]},"lotId":{"type":["string","null"]},"customerId":{"type":["string","null"]},"soldDate":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","serialNumber","status","lotId","customerId","notes"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","productId":"string","serialNumber":"string","status":"in_stock","lotId":"string","customerId":"string","notes":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Lot-trackingSerials","tags":["lot-tracking"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"productId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"List serial numbers","description":"Liste aller Seriennummern des Mandanten, neueste zuerst. `search` sucht in der Seriennummer. Kein Soft-Delete — was hier fehlt, ist endgueltig geloescht."},"post":{"responses":{"201":{"description":"Angelegte Seriennummer","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"serialNumber":{"type":"string"},"status":{"type":"string","enum":["in_stock","sold","returned","scrapped","recalled"]},"lotId":{"type":["string","null"]},"customerId":{"type":["string","null"]},"soldDate":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","serialNumber","status","lotId","customerId","notes"],"additionalProperties":false},"example":{"id":"string","productId":"string","serialNumber":"string","status":"in_stock","lotId":"string","customerId":"string","notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Lot-trackingSerials","tags":["lot-tracking"],"parameters":[],"summary":"Create serial number","description":"Legt eine Seriennummer an, wahlweise mit Bezug auf eine Charge (`lotId`). Die Seriennummer wird nicht auf Eindeutigkeit geprueft, und der Chargenbestand aendert sich dadurch nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"serialNumber":{"type":"string","minLength":1,"maxLength":150},"status":{"type":"string","enum":["in_stock","sold","returned","scrapped","recalled"],"default":"in_stock"},"lotId":{"type":"string","format":"uuid"},"customerId":{"type":"string","format":"uuid"},"soldDate":{"type":"string"},"notes":{"type":"string"}},"required":["serialNumber"]},"example":{"productId":"00000000-0000-4000-8000-000000000000","serialNumber":"string","status":"in_stock","lotId":"00000000-0000-4000-8000-000000000000","customerId":"00000000-0000-4000-8000-000000000000","soldDate":"string","notes":"string"}}}}}},"/api/v1/lot-tracking/serials/{id}":{"get":{"responses":{"200":{"description":"Seriennummer","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"serialNumber":{"type":"string"},"status":{"type":"string","enum":["in_stock","sold","returned","scrapped","recalled"]},"lotId":{"type":["string","null"]},"customerId":{"type":["string","null"]},"soldDate":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","serialNumber","status","lotId","customerId","notes"],"additionalProperties":false},"example":{"id":"string","productId":"string","serialNumber":"string","status":"in_stock","lotId":"string","customerId":"string","notes":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Lot-trackingSerialsById","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get serial number","description":"Eine einzelne Seriennummer mit Status, Charge und Kundenbezug."},"put":{"responses":{"200":{"description":"Aktualisierte Seriennummer","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"serialNumber":{"type":"string"},"status":{"type":"string","enum":["in_stock","sold","returned","scrapped","recalled"]},"lotId":{"type":["string","null"]},"customerId":{"type":["string","null"]},"soldDate":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","serialNumber","status","lotId","customerId","notes"],"additionalProperties":false},"example":{"id":"string","productId":"string","serialNumber":"string","status":"in_stock","lotId":"string","customerId":"string","notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1Lot-trackingSerialsById","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update serial number","description":"Aendert eine Seriennummer; nicht gesendete Felder bleiben erhalten. Hier sind ALLE fuenf Status setzbar, auch \"scrapped\" und \"recalled\" — anders als bei POST /serial-numbers/{id}/status.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"serialNumber":{"type":"string","minLength":1,"maxLength":150},"status":{"type":"string","enum":["in_stock","sold","returned","scrapped","recalled"],"default":"in_stock"},"lotId":{"type":"string","format":"uuid"},"customerId":{"type":"string","format":"uuid"},"soldDate":{"type":"string"},"notes":{"type":"string"}}},"example":{"productId":"00000000-0000-4000-8000-000000000000","serialNumber":"string","status":"in_stock","lotId":"00000000-0000-4000-8000-000000000000","customerId":"00000000-0000-4000-8000-000000000000","soldDate":"string","notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Geloescht — nur die getroffene ID","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1Lot-trackingSerialsById","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete serial number","description":"ENDGUELTIGES Loeschen: die Zeile wird entfernt, nicht als geloescht markiert. Damit geht auch die Zuordnung zur Charge verloren, die Charge selbst bleibt unberuehrt."}},"/api/v1/lot-tracking/serial-numbers/{id}/status":{"post":{"responses":{"200":{"description":"Seriennummer nach dem Statuswechsel","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"productId":{"type":["string","null"]},"serialNumber":{"type":"string"},"status":{"type":"string","enum":["in_stock","sold","returned","scrapped","recalled"]},"lotId":{"type":["string","null"]},"customerId":{"type":["string","null"]},"soldDate":{},"notes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","productId","serialNumber","status","lotId","customerId","notes"],"additionalProperties":false},"example":{"id":"string","productId":"string","serialNumber":"string","status":"in_stock","lotId":"string","customerId":"string","notes":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Lot-trackingSerial-numbersByIdStatus","tags":["lot-tracking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Set serial number status","description":"Wechselt den Status einer Seriennummer. Nimmt NUR sold, in_stock und returned an; \"scrapped\" und \"recalled\" sind hier nicht setzbar (die vergibt der Rueckruf oder PUT /serials/{id}). Antwortet mit 200 und der vollstaendigen Seriennummer. Weder `soldDate` noch `customerId` noch der Chargenbestand aendern sich dabei. Achtung, abweichender Pfad: /serial-numbers, nicht /serials.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["sold","in_stock","returned"]}},"required":["status"]},"example":{"status":"sold"}}}}}},"/api/v1/maengel":{"get":{"responses":{"200":{"description":"Maengel-Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":["string","null"],"description":"Bleibt beim Anlegen leer — die Trennung macht das Schema"},"projectId":{"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["open","in_progress","resolved","accepted"]},"priority":{"type":"string","enum":["low","medium","high","critical"]},"assignedTo":{"type":["string","null"]},"dueDate":{"type":["string","null"]},"photos":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","minLength":1},"geo":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"taken_at":{"type":"string"}},"required":["url"]}},"location":{"type":["string","null"]},"resolvedAt":{"type":["string","null"]},"resolvedNotes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","projectId","title","description","status","priority","assignedTo","dueDate","photos","location","resolvedAt","resolvedNotes","createdAt","updatedAt"]}},"pagination":{"type":"object","properties":{"total":{"type":"integer","minimum":0,"description":"Zaehlt MIT den gesetzten Filtern, nicht alle Maengel"},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"required":["total","limit","offset"]}},"required":["data","pagination"]},"example":{"data":[{"id":"string","tenantId":"string","projectId":"string","title":"string","description":"string","status":"open","priority":"low","assignedTo":"string","dueDate":"string","photos":[{"url":"string","geo":{"lat":0,"lng":0},"taken_at":"string"}],"location":"string","resolvedAt":"string","resolvedNotes":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"total":0,"limit":1,"offset":0}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Maengel","tags":["maengel"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["open","in_progress","resolved","accepted"]}},{"in":"query","name":"priority","schema":{"type":"string","enum":["low","medium","high","critical"]}},{"in":"query","name":"projectId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"search","schema":{"type":"string"}}],"description":"Liste aller Maengel. Gelesen wird die Tabelle maengel im Mandantenschema, sortiert nach Dringlichkeit (kritisch, hoch, mittel, niedrig) und darin neueste zuerst. status, priority, projectId und search grenzen ein; search sucht in Titel UND Beschreibung. limit nimmt 1 bis 200 an (Vorgabe 50), offset beginnt bei 0. total zaehlt mit den gesetzten Filtern. Ein Soft-Delete gibt es nicht: was hier fehlt, ist geloescht.","summary":"Liste aller Maengel","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Mangel angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":["string","null"],"description":"Bleibt beim Anlegen leer — die Trennung macht das Schema"},"projectId":{"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["open","in_progress","resolved","accepted"]},"priority":{"type":"string","enum":["low","medium","high","critical"]},"assignedTo":{"type":["string","null"]},"dueDate":{"type":["string","null"]},"photos":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","minLength":1},"geo":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"taken_at":{"type":"string"}},"required":["url"]}},"location":{"type":["string","null"]},"resolvedAt":{"type":["string","null"]},"resolvedNotes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","projectId","title","description","status","priority","assignedTo","dueDate","photos","location","resolvedAt","resolvedNotes","createdAt","updatedAt"]},"example":{"id":"string","tenantId":"string","projectId":"string","title":"string","description":"string","status":"open","priority":"low","assignedTo":"string","dueDate":"string","photos":[{"url":"string","geo":{"lat":0,"lng":0},"taken_at":"string"}],"location":"string","resolvedAt":"string","resolvedNotes":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1Maengel","tags":["maengel"],"parameters":[],"description":"Mangel anlegen. Schreibt eine Zeile in die Tabelle maengel des Mandantenschemas; fehlt sie, legt der Aufruf sie samt Indizes zuvor an. Ohne Angabe gelten status=open und priority=medium, photos ist dann eine leere Liste. Eine fortlaufende Mangelnummer wird NICHT vergeben — es gibt nur die technische Kennung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"status":{"type":"string","enum":["open","in_progress","resolved","accepted"],"default":"open"},"priority":{"type":"string","enum":["low","medium","high","critical"],"default":"medium"},"assignedTo":{"type":"string","format":"uuid"},"dueDate":{"type":"string"},"photos":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","minLength":1},"geo":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"taken_at":{"type":"string"}},"required":["url"]},"default":[]},"location":{"type":"string"}},"required":["title"]},"example":{"projectId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","status":"open","priority":"low","assignedTo":"00000000-0000-4000-8000-000000000000","dueDate":"string","photos":[{"url":"string","geo":{"lat":0,"lng":0},"taken_at":"string"}],"location":"string"}}}},"summary":"Mangel anlegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/maengel/{id}":{"get":{"responses":{"200":{"description":"Mangel","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":["string","null"],"description":"Bleibt beim Anlegen leer — die Trennung macht das Schema"},"projectId":{"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["open","in_progress","resolved","accepted"]},"priority":{"type":"string","enum":["low","medium","high","critical"]},"assignedTo":{"type":["string","null"]},"dueDate":{"type":["string","null"]},"photos":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","minLength":1},"geo":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"taken_at":{"type":"string"}},"required":["url"]}},"location":{"type":["string","null"]},"resolvedAt":{"type":["string","null"]},"resolvedNotes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","projectId","title","description","status","priority","assignedTo","dueDate","photos","location","resolvedAt","resolvedNotes","createdAt","updatedAt"]},"example":{"id":"string","tenantId":"string","projectId":"string","title":"string","description":"string","status":"open","priority":"low","assignedTo":"string","dueDate":"string","photos":[{"url":"string","geo":{"lat":0,"lng":0},"taken_at":"string"}],"location":"string","resolvedAt":"string","resolvedNotes":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1MaengelById","tags":["maengel"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Mangel Detail. Liest genau eine Zeile aus der Tabelle maengel des Mandantenschemas. Gesucht wird allein ueber die Kennung; eine unbekannte ergibt 404. Ein Soft-Delete gibt es nicht, ein geloeschter Mangel ist auch hier weg.","summary":"Mangel Detail","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":["string","null"],"description":"Bleibt beim Anlegen leer — die Trennung macht das Schema"},"projectId":{"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["open","in_progress","resolved","accepted"]},"priority":{"type":"string","enum":["low","medium","high","critical"]},"assignedTo":{"type":["string","null"]},"dueDate":{"type":["string","null"]},"photos":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","minLength":1},"geo":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"taken_at":{"type":"string"}},"required":["url"]}},"location":{"type":["string","null"]},"resolvedAt":{"type":["string","null"]},"resolvedNotes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","projectId","title","description","status","priority","assignedTo","dueDate","photos","location","resolvedAt","resolvedNotes","createdAt","updatedAt"]},"example":{"id":"string","tenantId":"string","projectId":"string","title":"string","description":"string","status":"open","priority":"low","assignedTo":"string","dueDate":"string","photos":[{"url":"string","geo":{"lat":0,"lng":0},"taken_at":"string"}],"location":"string","resolvedAt":"string","resolvedNotes":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1MaengelById","tags":["maengel"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Mangel aktualisieren. Der Rumpf darf Teilfelder tragen: die Route liest den Mangel zuerst und schreibt fuer jedes nicht gesendete Feld den bisherigen Wert zurueck, waehrend ein ausdrueckliches null es leert. Der Status laesst sich hier auch auf resolved setzen — resolved_at und resolved_notes bleiben dabei aber unberuehrt; dafuer ist POST /maengel/{id}/resolve da. Eine unbekannte Kennung ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"status":{"type":"string","enum":["open","in_progress","resolved","accepted"],"default":"open"},"priority":{"type":"string","enum":["low","medium","high","critical"],"default":"medium"},"assignedTo":{"type":"string","format":"uuid"},"dueDate":{"type":"string"},"photos":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","minLength":1},"geo":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"taken_at":{"type":"string"}},"required":["url"]},"default":[]},"location":{"type":"string"}}},"example":{"projectId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","status":"open","priority":"low","assignedTo":"00000000-0000-4000-8000-000000000000","dueDate":"string","photos":[{"url":"string","geo":{"lat":0,"lng":0},"taken_at":"string"}],"location":"string"}}}},"summary":"Mangel aktualisieren","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string","description":"Kennung des endgueltig geloeschten Mangels"}},"required":["deleted"]},"example":{"deleted":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1MaengelById","tags":["maengel"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Mangel loeschen. Geloescht wird endgueltig — kein deleted_at, kein Papierkorb, keine Wiederherstellung, und auch die hinterlegten Fotos verschwinden mit der Zeile. Eine unbekannte Kennung loescht nichts und ergibt 404. Die Antwort nennt nur die Kennung des geloeschten Mangels.","summary":"Mangel loeschen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/maengel/{id}/resolve":{"post":{"responses":{"200":{"description":"Behoben","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":["string","null"],"description":"Bleibt beim Anlegen leer — die Trennung macht das Schema"},"projectId":{"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["open","in_progress","resolved","accepted"]},"priority":{"type":"string","enum":["low","medium","high","critical"]},"assignedTo":{"type":["string","null"]},"dueDate":{"type":["string","null"]},"photos":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","minLength":1},"geo":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"taken_at":{"type":"string"}},"required":["url"]}},"location":{"type":["string","null"]},"resolvedAt":{"type":["string","null"]},"resolvedNotes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","projectId","title","description","status","priority","assignedTo","dueDate","photos","location","resolvedAt","resolvedNotes","createdAt","updatedAt"]},"example":{"id":"string","tenantId":"string","projectId":"string","title":"string","description":"string","status":"open","priority":"low","assignedTo":"string","dueDate":"string","photos":[{"url":"string","geo":{"lat":0,"lng":0},"taken_at":"string"}],"location":"string","resolvedAt":"string","resolvedNotes":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden"},"409":{"description":"Bereits abgeschlossen"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1MaengelByIdResolve","tags":["maengel"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Mangel als behoben markieren (status -> resolved). Setzt den Status auf resolved, resolved_at auf jetzt und uebernimmt notes als resolved_notes. Der Uebergang greift nur aus open und in_progress: steht der Mangel schon auf resolved oder accepted, kommt 409 und es wird nichts geaendert. Eine unbekannte Kennung ergibt 404. Die Antwort ist der vollstaendige Mangel nach der Aenderung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string"}}},"example":{"notes":"string"}}}},"summary":"Mangel als behoben markieren (status -> resolved)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/projects/{projectId}/maengel":{"get":{"responses":{"200":{"description":"Maengel des Projekts plus Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":["string","null"],"description":"Bleibt beim Anlegen leer — die Trennung macht das Schema"},"projectId":{"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["open","in_progress","resolved","accepted"]},"priority":{"type":"string","enum":["low","medium","high","critical"]},"assignedTo":{"type":["string","null"]},"dueDate":{"type":["string","null"]},"photos":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","minLength":1},"geo":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"taken_at":{"type":"string"}},"required":["url"]}},"location":{"type":["string","null"]},"resolvedAt":{"type":["string","null"]},"resolvedNotes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","projectId","title","description","status","priority","assignedTo","dueDate","photos","location","resolvedAt","resolvedNotes","createdAt","updatedAt"]}},"pagination":{"type":"object","properties":{"total":{"type":"integer","minimum":0,"description":"Zaehlt MIT den gesetzten Filtern, nicht alle Maengel"},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"required":["total","limit","offset"]}},"required":["data","pagination"]},"example":{"data":[{"id":"string","tenantId":"string","projectId":"string","title":"string","description":"string","status":"open","priority":"low","assignedTo":"string","dueDate":"string","photos":[{"url":"string","geo":{"lat":0,"lng":0},"taken_at":"string"}],"location":"string","resolvedAt":"string","resolvedNotes":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"total":0,"limit":1,"offset":0}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1ProjectsByProjectIdMaengel","tags":["maengel"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["open","in_progress","resolved","accepted"]}},{"in":"query","name":"priority","schema":{"type":"string","enum":["low","medium","high","critical"]}},{"in":"query","name":"search","schema":{"type":"string"}},{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"summary":"Alle Maengel eines Projekts","description":"Dieselbe Liste wie `GET /maengel`, fest auf das Projekt aus dem Pfad eingeschraenkt — ein `projectId` in der Abfrage gibt es hier deshalb nicht. Gelesen wird die Tabelle `maengel` im Mandantenschema, sortiert nach Dringlichkeit (kritisch, hoch, mittel, niedrig) und darin neueste zuerst.\n\n`status`, `priority` und `search` grenzen weiter ein; `search` sucht in Titel UND Beschreibung. `limit` nimmt 1 bis 200 an (Vorgabe 50), `offset` beginnt bei 0; `pagination.total` zaehlt mit den gesetzten Filtern, aber ohne Blaetterung.\n\nEin unbekanntes Projekt ergibt KEIN 404, sondern eine leere Liste — dass es das Projekt gibt, wird nicht geprueft. Ein Soft-Delete gibt es nicht: was hier fehlt, ist geloescht."}},"/api/v1/projects/{projectId}/maengel/summary":{"get":{"responses":{"200":{"description":"Zusammenfassung"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1ProjectsByProjectIdMaengelSummary","tags":["maengel"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"description":"KPI-Zusammenfassung der Maengel eines Projekts. Liefert die Gesamtzahl, die Verteilung nach Status (open, in_progress, resolved, accepted) und Prioritaet (low bis critical) sowie `overdueCount`: Maengel mit Faelligkeit vor heute, die weder resolved noch accepted sind. Ein unbekanntes Projekt ergibt keine 404, sondern lauter Nullen.","summary":"KPI-Zusammenfassung der Maengel eines Projekts","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/projects/{projectId}/maengel/export-pdf":{"get":{"responses":{"200":{"description":"Das Maengelprotokoll als druckoptimierte HTML-Seite (kein JSON)","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar (JSON)"}},"operationId":"getApiV1ProjectsByProjectIdMaengelExport-pdf","tags":["maengel"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"summary":"Mängelprotokoll als druckbares HTML (MVP-Fallback)","description":"Liefert das Maengelprotokoll des Projekts als druckfertige HTML-Seite — KEIN PDF und kein JSON. Eine PDF-Engine ist nicht installiert; die Seite loest im Browser selbst `window.print()` aus, aus dem Druckdialog entsteht das PDF.\n\nEnthalten sind ALLE Maengel des Projekts, sortiert nach Dringlichkeit (kritisch, hoch, mittel, niedrig) und darin nach Anlagezeitpunkt. Es wird nicht gefiltert und nicht geblaettert; je Mangel erscheint hoechstens das erste Foto. Der Aufruf ist rein lesend und antwortet mit `Cache-Control: no-store`."}},"/api/v1/payroll/runs":{"get":{"responses":{"200":{"description":"Liste der Payroll-Runs — mit `total`, ohne Paginierung","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"month":{},"year":{},"status":{},"employeeCount":{},"totalGross":{"type":"number"},"totalNet":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["totalGross","totalNet"],"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"totalGross":0,"totalNet":0}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1PayrollRuns","tags":["payroll"],"parameters":[],"summary":"Lohnläufe des Mandanten auflisten","description":"Liest `<mandant>.payroll_runs`, neueste Periode zuerst (Jahr, dann Monat absteigend). Es gibt weder Blätterung noch Filter — `total` zählt die gelieferten Zeilen, es ist kein Gesamtzähler. Die Payroll-Tabellen werden beim ersten Aufruf angelegt. Wie jede Route dieser Datei erfordert der Aufruf mindestens die Rolle `hr_manager`."},"post":{"responses":{"201":{"description":"Payroll-Run angelegt — leerer Rahmen im Status `draft`. Betraege und Lohnzettel entstehen erst durch den Aufruf `/calculate`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"month":{},"year":{},"status":{},"employeeCount":{},"totalGross":{"type":"number"},"totalNet":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["totalGross","totalNet"],"additionalProperties":false},"example":{"totalGross":0,"totalNet":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PayrollRuns","tags":["payroll"],"parameters":[],"description":"Legt einen leeren Lauf im Status `draft` für Monat und Jahr an. Es wird dabei WEDER gerechnet noch ein Lohnzettel erzeugt — beides passiert erst bei `POST /runs/{id}/calculate`. Auf Monat und Jahr liegt keine Eindeutigkeit: ein zweiter Aufruf für dieselbe Periode legt einen zweiten Lauf an, statt abgewiesen zu werden. Der Vorgang wird als signierter GoBD-Eintrag `payroll.run_created` protokolliert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"month":{"type":"integer","minimum":1,"maximum":12},"year":{"type":"integer","minimum":2000,"maximum":2100}},"required":["month","year"]},"example":{"month":1,"year":2000}}}},"summary":"Legt einen leeren Lauf im Status `draft` für Monat und Jahr an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/payroll/runs/{id}":{"get":{"responses":{"200":{"description":"Payroll-Run Details — der Lauf FLACH ausgebreitet, `slips` daneben. Keine Huelle, kein `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"month":{},"year":{},"status":{},"employeeCount":{},"totalGross":{"type":"number"},"totalNet":{"type":"number"},"createdAt":{},"updatedAt":{},"slips":{"type":"array","items":{"type":"object","properties":{"id":{},"runId":{},"tenantId":{},"employeeId":{},"employeeNumber":{},"employeeName":{},"grossSalary":{"type":"number"},"socialSecurity":{"type":"number"},"incomeTax":{"type":"number"},"netSalary":{"type":"number"},"workingDays":{},"sickDays":{},"createdAt":{},"updatedAt":{}},"required":["grossSalary","socialSecurity","incomeTax","netSalary"],"additionalProperties":false}}},"required":["totalGross","totalNet","slips"],"additionalProperties":false},"example":{"totalGross":0,"totalNet":0,"slips":[{"grossSalary":0,"socialSecurity":0,"incomeTax":0,"netSalary":0}]}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Run nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1PayrollRunsById","tags":["payroll"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen Lohnlauf mit seinen Lohnzetteln lesen","description":"Gibt den Lauf flach ausgebreitet zurück und die zugehörigen Lohnzettel daneben unter `slips`, nach Mitarbeiternamen sortiert — ohne Hülle, ohne `data`. Solange `/calculate` nicht gelaufen ist, bleibt `slips` leer. Gesucht wird über Kennung UND Mandant: eine fremde Kennung ist von einer unbekannten nicht zu unterscheiden, beides ergibt 404."}},"/api/v1/payroll/runs/{id}/calculate":{"post":{"responses":{"200":{"description":"Berechnung abgeschlossen, Status `calculated`. Antwort ist der Lauf OHNE die erzeugten Lohnzettel — die holt `GET /runs/:id`. Gerechnet wird nur ueber Mitarbeiter mit `status=active` und `salary_type=monthly`; wer anders bezahlt wird, faellt still heraus.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"month":{},"year":{},"status":{},"employeeCount":{},"totalGross":{"type":"number"},"totalNet":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["totalGross","totalNet"],"additionalProperties":false},"example":{"totalGross":0,"totalNet":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Run nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Lauf ist bereits finalisiert und wird nicht neu gerechnet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PayrollRunsByIdCalculate","tags":["payroll"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Rechnet den Lauf neu: die vorhandenen Lohnzettel dieses Laufs werden GELÖSCHT und aus allen aktiven Mitarbeitern mit monatlichem Gehalt neu erzeugt. Die Abzüge sind PAUSCHAL — 20,5 % Sozialversicherung und 15 % Lohnsteuer vom Brutto, ohne Lohnsteuertabelle, Steuerklasse oder Freibeträge. Danach steht der Lauf auf `calculated` und trägt Mitarbeiterzahl, Brutto- und Nettosumme; ein bereits finalisierter Lauf wird mit 409 abgewiesen. Fehlt die Mitarbeitertabelle, läuft der Vorgang mit null Mitarbeitern durch, statt zu scheitern. Protokolliert als GoBD-Eintrag `payroll.run_calculated`.","summary":"Rechnet den Lauf neu","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/payroll/runs/{id}/finalize":{"post":{"responses":{"200":{"description":"Run finalisiert und gesperrt. Nebenwirkung: es entsteht eine Buchung im Journal (Loehne 4120 gegen Verbindlichkeiten 1740/1750/1755). Schlaegt diese Buchung aus einem anderen Grund als einer geschlossenen Periode fehl, kommt trotzdem 200 — der Lauf gilt dann als finalisiert, ohne Journalzeile.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"month":{},"year":{},"status":{},"employeeCount":{},"totalGross":{"type":"number"},"totalNet":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["totalGross","totalNet"],"additionalProperties":false},"example":{"totalGross":0,"totalNet":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Run nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Lauf ist bereits finalisiert (`run_already_finalized`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"423":{"description":"Die Buchungsperiode ist geschlossen (`period_closed`) — es wurde NICHTS geändert. Die Prüfung läuft vor dem Sperren, der Lauf behält seinen Status und derselbe Aufruf funktioniert, sobald die Periode offen ist. (Bis 01.09.2026 stand der Lauf zu diesem Zeitpunkt bereits auf `finalized`; ein erneuter Aufruf antwortete dann mit 409 statt 423, und der Abschluss war nicht mehr rückgängig zu machen.)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PayrollRunsByIdFinalize","tags":["payroll"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt den Lauf auf `finalized` und bucht danach den Sammelbeleg ins Journal: von Konto 4120 gegen 1740 (Lohnsteuer), 1750 (Sozialversicherung) und 1755 (Nettolohn), Belegnummer `PAYROLL-JJJJ-MM`. Gebucht wird nur, wenn die Bruttosumme über 0 liegt. Die Buchungsperiode wird VOR dem Sperren geprüft: ist sie geschlossen, kommt 423 und der Lauf bleibt unverändert — derselbe Aufruf lässt sich nach dem Öffnen wiederholen. Scheitert eine der drei Buchungen aus einem anderen Grund, bleibt der Lauf finalisiert und die Antwort meldet `journalGebucht: false` samt `journalFehler`; da nacheinander gebucht wird, heißt das „unvollständig\", nicht zwingend „gar nichts\". Der Statuscode ist auch dann 200, weil der Abschluss stattgefunden hat. Ein schon finalisierter Lauf wird mit 409 abgewiesen; ein Zurücksetzen sieht diese Datei nicht vor. Protokolliert als GoBD-Eintrag `payroll.run_finalized`.","summary":"Setzt den Lauf auf `finalized` und bucht danach den Sammelbeleg ins Journal","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/payroll/slips/{id}":{"get":{"responses":{"200":{"description":"Lohnzettel — nackt, ohne Huelle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"runId":{},"tenantId":{},"employeeId":{},"employeeNumber":{},"employeeName":{},"grossSalary":{"type":"number"},"socialSecurity":{"type":"number"},"incomeTax":{"type":"number"},"netSalary":{"type":"number"},"workingDays":{},"sickDays":{},"createdAt":{},"updatedAt":{}},"required":["grossSalary","socialSecurity","incomeTax","netSalary"],"additionalProperties":false},"example":{"grossSalary":0,"socialSecurity":0,"incomeTax":0,"netSalary":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Lohnzettel nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1PayrollSlipsById","tags":["payroll"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen Lohnzettel lesen","description":"Liest einen einzelnen Lohnzettel aus `<mandant>.payroll_slips` und gibt ihn nackt zurück, ohne Hülle. Gesucht wird über Kennung UND Mandant, eine fremde Kennung ist von einer unbekannten nicht zu unterscheiden. Reine Leseoperation: der Zettel wird hier weder erzeugt noch neu gerechnet — das tut `POST /runs/{id}/calculate`."}},"/api/v1/payroll/lst-anmeldung":{"get":{"responses":{"200":{"description":"Liste der Lohnsteueranmeldungen — mit `total`, ohne Paginierung","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"jahr":{"type":["number","null"]},"monat":{"type":["number","null"]},"zeitraumTyp":{},"status":{},"summeLst":{"type":"number"},"summeSolz":{"type":"number"},"summeKist":{"type":"number"},"xmlPath":{},"eingereichtAm":{},"elsterTransferTicket":{},"createdAt":{},"updatedAt":{}},"required":["jahr","monat","summeLst","summeSolz","summeKist"],"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"jahr":0,"monat":0,"summeLst":0,"summeSolz":0,"summeKist":0}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1PayrollLst-anmeldung","tags":["payroll","elster"],"parameters":[],"summary":"Lohnsteueranmeldungen des Mandanten, neueste Periode zuerst","description":"Liest `<mandant>.lst_anmeldungen` für den eigenen Mandanten, neueste Periode zuerst (Jahr, dann Monat absteigend). Es gibt weder Blätterung noch Filter — `total` zählt die gelieferten Zeilen, es ist kein Gesamtzähler. Die Tabelle wird beim ersten Aufruf angelegt, eine leere Liste ist bei einem frischen Mandanten also der Normalfall."},"post":{"responses":{"201":{"description":"LStA angelegt — als einzige Antwort dieser Datei in einer `data`-Huelle. Die Summen stammen aus finalisierten Lohnzetteln; wo dort keine Lohnsteuer steht, SCHAETZT der Server sie aus dem Brutto (Naeherung, keine Lohnsteuertabelle nach §39b EStG).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"tenantId":{},"jahr":{"type":["number","null"]},"monat":{"type":["number","null"]},"zeitraumTyp":{},"status":{},"summeLst":{"type":"number"},"summeSolz":{"type":"number"},"summeKist":{"type":"number"},"xmlPath":{},"eingereichtAm":{},"elsterTransferTicket":{},"createdAt":{},"updatedAt":{}},"required":["jahr","monat","summeLst","summeSolz","summeKist"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"jahr":0,"monat":0,"summeLst":0,"summeSolz":0,"summeKist":0}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PayrollLst-anmeldung","tags":["payroll","elster"],"parameters":[],"description":"Neue Lohnsteueranmeldung anlegen. Summiert LSt/SolZ/KiSt aus finalisierten Lohnzetteln.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jahr":{"type":"integer","minimum":2000,"maximum":2099},"monat":{"type":"integer","minimum":1,"maximum":12},"zeitraumTyp":{"type":"string","enum":["monat","quartal","jahr"],"default":"monat"}},"required":["jahr","monat"]},"example":{"jahr":2000,"monat":1,"zeitraumTyp":"monat"}}}},"summary":"Neue Lohnsteueranmeldung anlegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/payroll/lst-anmeldung/{id}":{"get":{"responses":{"200":{"description":"LStA-Detail — die Anmeldung flach, `mitarbeiter` daneben (kein `data`). Die Mitarbeiter-Zeilen werden bei JEDEM Aufruf neu gerechnet, nicht gespeichert: sie koennen von den Summen der Anmeldung abweichen, wenn sich die Lohnzettel seither geaendert haben.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"jahr":{"type":["number","null"]},"monat":{"type":["number","null"]},"zeitraumTyp":{},"status":{},"summeLst":{"type":"number"},"summeSolz":{"type":"number"},"summeKist":{"type":"number"},"xmlPath":{},"eingereichtAm":{},"elsterTransferTicket":{},"createdAt":{},"updatedAt":{},"mitarbeiter":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"lstBrutto":{"type":"number"},"lst":{"type":"number"},"solz":{"type":"number"},"kistEvang":{"type":"number"},"kistKath":{"type":"number"}},"required":["name","lstBrutto","lst","solz","kistEvang","kistKath"],"additionalProperties":false}}},"required":["jahr","monat","summeLst","summeSolz","summeKist","mitarbeiter"],"additionalProperties":false},"example":{"jahr":0,"monat":0,"summeLst":0,"summeSolz":0,"summeKist":0,"mitarbeiter":[{"name":"string","lstBrutto":0,"lst":0,"solz":0,"kistEvang":0,"kistKath":0}]}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"LStA nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1PayrollLst-anmeldungById","tags":["payroll","elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Lohnsteueranmeldung mit Mitarbeiter-Aufschlüsselung lesen","description":"Gibt die gespeicherte Anmeldung flach zurück und daneben die Aufschlüsselung je Mitarbeiter. Diese Zeilen sind NICHT gespeichert: sie werden bei jedem Aufruf aus den finalisierten Lohnzetteln derselben Periode neu gerechnet und können von den Summen der Anmeldung abweichen, wenn sich die Lohnzettel seither geändert haben. Steht in einem Lohnzettel keine Lohnsteuer, schätzt der Server sie aus dem Brutto — eine Näherung, keine Lohnsteuertabelle nach §39b EStG. `kistKath` ist fest 0."}},"/api/v1/payroll/lst-anmeldung/{periode}/generate":{"post":{"responses":{"200":{"description":"XML erzeugt, Status → `xml_generiert`. WICHTIG: `validation.ok` kann hier `false` sein — die Pruefung blockt nicht, sie berichtet nur. Wer die Datei einreicht, muss `validation.errors` selbst auswerten. Fehlen Steuernummer oder Firmenname in den Einstellungen, setzt der Server Platzhalter ein (`0000000000000`, Mandanten-Kuerzel) statt abzubrechen.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"xml_path":{"type":"string"},"validation":{"type":"object","properties":{"ok":{"type":"boolean"},"errors":{"type":"array","items":{"type":"string"}}},"required":["ok","errors"]}},"required":["id","xml_path","validation"],"additionalProperties":false},"example":{"id":"string","xml_path":"string","validation":{"ok":true,"errors":["string"]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"LStA nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PayrollLst-anmeldungByPeriodeGenerate","tags":["payroll","elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"periode","required":true}],"description":"ELSTER-XML fuer eine Lohnsteueranmeldung erzeugen. :periode = UUID oder YYYY-MM.","summary":"ELSTER-XML fuer eine Lohnsteueranmeldung erzeugen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/payroll/lst-anmeldung/{id}/submit":{"post":{"responses":{"200":{"description":"Der Datensatz steht danach auf `eingereicht` — an ELSTER ging aber NICHTS. Das Ticket beginnt mit `STUB-` und ist eine erfundene Kennung. Die echte ERiC-Uebertragung fehlt noch. Wer diesen Status als Nachweis einer Abgabe liest, liegt falsch.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","const":"eingereicht"},"elsterTransferTicket":{"type":"string"}},"required":["id","status","elsterTransferTicket"],"additionalProperties":false},"example":{"id":"string","status":"eingereicht","elsterTransferTicket":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"ELSTER_SUBMIT_ENABLED ist nicht gesetzt (`submit_disabled`) — nichts wurde veraendert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"404":{"description":"LStA nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PayrollLst-anmeldungByIdSubmit","tags":["payroll","elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"LStA bei ELSTER einreichen (GATED: ELSTER_SUBMIT_ENABLED=true erforderlich). Stub-Implementierung.","summary":"LStA bei ELSTER einreichen (GATED: ELSTER_SUBMIT_ENABLED=true erforderlich)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/payroll/lst-anmeldung/{id}/xml":{"get":{"responses":{"200":{"description":"ELSTER-LStA-XML als Datei-Download. Gelesen wird der Pfad aus `xmlPath` — ein Pfad im API-Container unter `/tmp`. Nach einem Neustart des Containers ist die Datei weg und derselbe Aufruf antwortet mit 404 `xml_file_missing`, obwohl der Datensatz weiter `xml_generiert` meldet. Dann hilft nur ein erneutes `/generate`.","content":{"application/xml":{"schema":{"type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Anmeldung nicht gefunden (`lst_anmeldung_not_found`), noch kein XML erzeugt (`xml_not_generated`) oder die erzeugte Datei liegt nicht mehr auf der Platte (`xml_file_missing`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1PayrollLst-anmeldungByIdXml","tags":["payroll","elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Die ELSTER-XML einer Lohnsteueranmeldung herunterladen","description":"Gibt die zuvor mit `POST /lst-anmeldung/{periode}/generate` erzeugte Datei als Download aus — `application/xml; charset=utf-8`, `Content-Disposition: attachment`, Dateiname `LStA-JJJJ-MM.xml`. Gelesen wird der in `xmlPath` vermerkte Pfad, eine Datei im API-Container unter `/tmp`: nach einem Neustart ist sie fort, und derselbe Aufruf antwortet 404 `xml_file_missing`, obwohl der Datensatz weiter `xml_generiert` meldet — dann hilft nur ein erneutes `/generate`. Der Abruf übermittelt NICHTS an ELSTER und ändert am Datensatz nichts."}},"/api/v1/calendar/events/upcoming":{"get":{"responses":{"200":{"description":"Die anstehenden Termine der naechsten 14 Tage","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"title":{"type":"string"},"description":{},"eventType":{},"startAt":{},"endAt":{},"allDay":{},"location":{},"status":{},"createdBy":{},"assigneeId":{},"relatedEntityType":{},"relatedEntityId":{},"color":{},"createdAt":{},"updatedAt":{}},"required":["id","title"],"additionalProperties":false},"description":"Die anstehenden Termine, frueheste zuerst"},"total":{"type":"integer","description":"Anzahl der gelieferten Zeilen — nicht die Gesamtzahl im Bestand"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"string","title":"string"}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1CalendarEventsUpcoming","tags":["calendar"],"parameters":[],"summary":"Liefert die nächsten anstehenden Kalenderereignisse","description":"Liest aus calendar_events die Termine, deren Beginn zwischen jetzt und in 14 Tagen liegt, abgesagte ausgenommen, frueheste zuerst, hoechstens 50. Der Zeitraum und die Obergrenze sind fest verdrahtet — es gibt hier keine Parameter. Die Huelle ist `{ data, total }` und damit eine ANDERE als bei GET /calendar/events, das `pagination` fuehrt."}},"/api/v1/calendar/events":{"get":{"responses":{"200":{"description":"Liste der Ereignisse","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"title":{"type":"string"},"description":{},"eventType":{},"startAt":{},"endAt":{},"allDay":{},"location":{},"status":{},"createdBy":{},"assigneeId":{},"relatedEntityType":{},"relatedEntityId":{},"color":{},"createdAt":{},"updatedAt":{}},"required":["id","title"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","title":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1CalendarEvents","tags":["calendar"],"parameters":[{"in":"query","name":"start","schema":{"type":"string"}},{"in":"query","name":"end","schema":{"type":"string"}},{"in":"query","name":"type","schema":{"type":"string","enum":["meeting","task","reminder","deadline","visit"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":500,"default":200}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Listet Kalenderereignisse im angegebenen Zeitraum","description":"Liest calendar_events des Mandanten, nach Beginn aufsteigend. `start` filtert auf Beginn ab, `end` auf Ende bis, `type` auf die Terminart; ohne Angabe wird nicht eingeschraenkt. `limit` liegt zwischen 1 und 500 (Vorgabe 200), `offset` blaettert. ACHTUNG: `pagination.total` zaehlt nur die Zeilen DIESER Seite, nicht den Gesamtbestand — die letzte Seite ist daran also nicht zu erkennen. Abgesagte Termine sind hier enthalten."},"post":{"responses":{"201":{"description":"Der angelegte Termin","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"title":{"type":"string"},"description":{},"eventType":{},"startAt":{},"endAt":{},"allDay":{},"location":{},"status":{},"createdBy":{},"assigneeId":{},"relatedEntityType":{},"relatedEntityId":{},"color":{},"createdAt":{},"updatedAt":{}},"required":["id","title"],"additionalProperties":false},"example":{"id":"string","title":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1CalendarEvents","tags":["calendar"],"parameters":[],"summary":"Legt ein neues Kalenderereignis an","description":"Schreibt eine Zeile in calendar_events. Pflicht sind `title`, `startAt` und `endAt` (beide als Zeitstempel nach ISO 8601); ohne Angabe gelten `eventType: \"meeting\"`, `status: \"confirmed\"`, `allDay: false` und die Farbe #3b82f6. Der Server prueft NICHT, ob das Ende nach dem Beginn liegt, und auch nicht auf Ueberschneidungen mit vorhandenen Terminen. Es wird nichts versendet und niemand benachrichtigt. Die Antwort ist der angelegte Datensatz selbst, ohne Umschlag.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"eventType":{"type":"string","enum":["meeting","task","reminder","deadline","visit"],"default":"meeting"},"startAt":{"type":"string","format":"date-time"},"endAt":{"type":"string","format":"date-time"},"allDay":{"type":"boolean","default":false},"location":{"type":"string"},"status":{"type":"string","enum":["confirmed","tentative","cancelled"],"default":"confirmed"},"createdBy":{"type":"string"},"assigneeId":{"type":"string"},"relatedEntityType":{"type":"string"},"relatedEntityId":{"type":"string"},"color":{"type":"string","default":"#3b82f6"}},"required":["title","startAt","endAt"]},"example":{"title":"string","description":"string","eventType":"meeting","startAt":"2026-01-01T12:00:00.000Z","endAt":"2026-01-01T12:00:00.000Z","allDay":true,"location":"string","status":"confirmed","createdBy":"string","assigneeId":"string","relatedEntityType":"string","relatedEntityId":"string","color":"string"}}}}}},"/api/v1/calendar/events/{id}":{"get":{"responses":{"200":{"description":"Ereignis-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"title":{"type":"string"},"description":{},"eventType":{},"startAt":{},"endAt":{},"allDay":{},"location":{},"status":{},"createdBy":{},"assigneeId":{},"relatedEntityType":{},"relatedEntityId":{},"color":{},"createdAt":{},"updatedAt":{}},"required":["id","title"],"additionalProperties":false},"example":{"id":"string","title":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Ereignis nicht gefunden"}},"operationId":"getApiV1CalendarEventsById","tags":["calendar"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Liefert ein einzelnes Kalenderereignis","description":"Liest einen Termin ueber Kennung UND Mandant aus calendar_events und liefert ihn ohne Umschlag, also nicht unter `data`. Eine Kennung aus einem fremden Mandanten wird wie eine unbekannte behandelt: 404 `event_not_found`."},"patch":{"responses":{"200":{"description":"Der Termin nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"title":{"type":"string"},"description":{},"eventType":{},"startAt":{},"endAt":{},"allDay":{},"location":{},"status":{},"createdBy":{},"assigneeId":{},"relatedEntityType":{},"relatedEntityId":{},"color":{},"createdAt":{},"updatedAt":{}},"required":["id","title"],"additionalProperties":false},"example":{"id":"string","title":"string"}}}},"400":{"description":"Validierungsfehler oder leerer Rumpf (`no_fields_to_update`)"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Ereignis nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"patchApiV1CalendarEventsById","tags":["calendar"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aktualisiert einzelne Felder eines Kalenderereignisses","description":"Schreibt nur die mitgeschickten Felder; alle uebrigen bleiben stehen, `updated_at` setzt der Server. Ein Feld ausdruecklich als null zu senden leert es. Ein leerer Rumpf ist ein Fehler und keine Nullaenderung: 400 `no_fields_to_update`. Der Datensatz wird ueber Kennung UND Mandant gesucht — greift das nicht, kommt 404 `event_not_found`. Die Antwort ist der geaenderte Datensatz, ohne Umschlag.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"eventType":{"type":"string","enum":["meeting","task","reminder","deadline","visit"],"default":"meeting"},"startAt":{"type":"string","format":"date-time"},"endAt":{"type":"string","format":"date-time"},"allDay":{"type":"boolean","default":false},"location":{"type":"string"},"status":{"type":"string","enum":["confirmed","tentative","cancelled"],"default":"confirmed"},"createdBy":{"type":"string"},"assigneeId":{"type":"string"},"relatedEntityType":{"type":"string"},"relatedEntityId":{"type":"string"},"color":{"type":"string","default":"#3b82f6"}}},"example":{"title":"string","description":"string","eventType":"meeting","startAt":"2026-01-01T12:00:00.000Z","endAt":"2026-01-01T12:00:00.000Z","allDay":true,"location":"string","status":"confirmed","createdBy":"string","assigneeId":"string","relatedEntityType":"string","relatedEntityId":"string","color":"string"}}}}},"delete":{"responses":{"200":{"description":"Der Termin wurde geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Bestaetigung im Klartext, enthaelt die geloeschte Kennung"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Ereignis nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1CalendarEventsById","tags":["calendar"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Löscht ein Kalenderereignis","description":"Entfernt den Termin endgueltig aus calendar_events — kein Soft-Delete, kein `deleted_at`, kein Wiederherstellen. Wer einen Termin nur absagen will, setzt per PATCH `status: \"cancelled\"`. Vor dem Loeschen wird geprueft, ob die Kennung zu diesem Mandanten gehoert; sonst 404 `event_not_found`. Die Antwort ist ein Satz, kein Datensatz."}},"/api/v1/contracts/expiring":{"get":{"responses":{"200":{"description":"Liste auslaufender Verträge — „total\" ist die Länge dieser Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Vertrags (UUID)"},"tenantId":{"description":"Mandant, dem der Vertrag gehoert"},"contractNumber":{"description":"Vertragsnummer"},"title":{"description":"Titel des Vertrags"},"contractType":{"description":"Vertragsart, Vorgabewert \"service\""},"status":{"description":"Status: draft, active, signed, terminated, expired oder cancelled"},"counterpartyName":{"description":"Name des Vertragspartners"},"counterpartyType":{"description":"Art des Partners, Vorgabewert \"customer\""},"counterpartyId":{"description":"Kennung des Partners im Kunden- oder Lieferantenstamm"},"value":{"type":["number","null"],"description":"Vertragswert als Zahl; null = nicht angegeben"},"currency":{"description":"Waehrungscode (ISO-4217), Vorgabewert \"EUR\""},"startDate":{"description":"Vertragsbeginn als Datum; null = offen"},"endDate":{"description":"Vertragsende als Datum; null = unbefristet"},"noticePeriodDays":{"description":"Kuendigungsfrist in Tagen, Vorgabewert 30"},"autoRenewal":{"description":"Verlaengert sich der Vertrag automatisch?"},"signedAt":{"description":"Zeitpunkt der Unterzeichnung; null = nicht unterzeichnet"},"signedBy":{"description":"Wer unterzeichnet hat, als Freitext"},"terminatedAt":{"description":"Kuendigungsdatum; null = nicht gekuendigt"},"terminationReason":{"description":"Kuendigungsgrund als Freitext"},"description":{"description":"Beschreibung des Vertragsgegenstands"},"createdBy":{"description":"Wer den Vertrag angelegt hat"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","value"],"additionalProperties":false},"description":"Die auslaufenden Vertraege, nach Ende sortiert"},"total":{"type":"integer","minimum":0,"description":"Laenge der gelieferten Liste"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"string","value":0}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1ContractsExpiring","tags":["contracts"],"parameters":[],"summary":"List expiring contracts","description":"Listet Verträge, die in den nächsten 30 Tagen auslaufen, nach Enddatum sortiert. Nur Verträge im Status „active\" mit gesetztem Enddatum. Unbefristete Verträge tauchen nie auf. Es wird nicht geblättert."}},"/api/v1/contracts/stats":{"get":{"responses":{"200":{"description":"Vertrags-Statistiken — sechs Anzahlen, keine Beträge","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer","minimum":0,"description":"Alle Vertraege, ueber ALLE Status summiert — auch stornierte und geloeschte"},"active":{"type":"integer","minimum":0,"description":"Vertraege im Status active"},"expiring":{"type":"integer","minimum":0,"description":"Aktive Vertraege, die in den naechsten 30 Tagen enden"},"expired":{"type":"integer","minimum":0,"description":"Vertraege im Status expired"},"draft":{"type":"integer","minimum":0,"description":"Vertraege im Status draft"},"terminated":{"type":"integer","minimum":0,"description":"Vertraege im Status terminated"}},"required":["total","active","expiring","expired","draft","terminated"],"additionalProperties":false},"example":{"total":0,"active":0,"expiring":0,"expired":0,"draft":0,"terminated":0}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1ContractsStats","tags":["contracts"],"parameters":[],"summary":"Contract key figures","description":"Kennzahlen zu Verträgen. ACHTUNG: „total\" zählt ALLE Status mit — auch stornierte und gelöschte. Die Summe der fünf anderen Zahlen ergibt deshalb in der Regel weniger als „total\". Es sind Anzahlen, keine Beträge; ein Vertragsvolumen in Euro liefert dieser Aufruf nicht."}},"/api/v1/contracts":{"get":{"responses":{"200":{"description":"Liste der Verträge","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Vertrags (UUID)"},"tenantId":{"description":"Mandant, dem der Vertrag gehoert"},"contractNumber":{"description":"Vertragsnummer"},"title":{"description":"Titel des Vertrags"},"contractType":{"description":"Vertragsart, Vorgabewert \"service\""},"status":{"description":"Status: draft, active, signed, terminated, expired oder cancelled"},"counterpartyName":{"description":"Name des Vertragspartners"},"counterpartyType":{"description":"Art des Partners, Vorgabewert \"customer\""},"counterpartyId":{"description":"Kennung des Partners im Kunden- oder Lieferantenstamm"},"value":{"type":["number","null"],"description":"Vertragswert als Zahl; null = nicht angegeben"},"currency":{"description":"Waehrungscode (ISO-4217), Vorgabewert \"EUR\""},"startDate":{"description":"Vertragsbeginn als Datum; null = offen"},"endDate":{"description":"Vertragsende als Datum; null = unbefristet"},"noticePeriodDays":{"description":"Kuendigungsfrist in Tagen, Vorgabewert 30"},"autoRenewal":{"description":"Verlaengert sich der Vertrag automatisch?"},"signedAt":{"description":"Zeitpunkt der Unterzeichnung; null = nicht unterzeichnet"},"signedBy":{"description":"Wer unterzeichnet hat, als Freitext"},"terminatedAt":{"description":"Kuendigungsdatum; null = nicht gekuendigt"},"terminationReason":{"description":"Kuendigungsgrund als Freitext"},"description":{"description":"Beschreibung des Vertragsgegenstands"},"createdBy":{"description":"Wer den Vertrag angelegt hat"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","value"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","value":0}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Contracts","tags":["contracts"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"type","schema":{"type":"string"}}],"summary":"Listet alle Verträge des Mandanten","description":"Liest `contracts` des Mandanten, neueste zuerst, geblättert über `limit` (1-200, Standard 50) und `offset`; `pagination.total` zählt alle Treffer des Filters, nicht nur die Seite. `status` und `type` (Spalte `contract_type`) filtern jeweils auf GENAU einen Wert, als exakter Vergleich ohne Teiltreffer. Stornierte Verträge stehen mit in der Liste — wer sie ausblenden will, filtert selbst."},"post":{"responses":{"201":{"description":"Vertrag angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Vertrags (UUID)"},"tenantId":{"description":"Mandant, dem der Vertrag gehoert"},"contractNumber":{"description":"Vertragsnummer"},"title":{"description":"Titel des Vertrags"},"contractType":{"description":"Vertragsart, Vorgabewert \"service\""},"status":{"description":"Status: draft, active, signed, terminated, expired oder cancelled"},"counterpartyName":{"description":"Name des Vertragspartners"},"counterpartyType":{"description":"Art des Partners, Vorgabewert \"customer\""},"counterpartyId":{"description":"Kennung des Partners im Kunden- oder Lieferantenstamm"},"value":{"type":["number","null"],"description":"Vertragswert als Zahl; null = nicht angegeben"},"currency":{"description":"Waehrungscode (ISO-4217), Vorgabewert \"EUR\""},"startDate":{"description":"Vertragsbeginn als Datum; null = offen"},"endDate":{"description":"Vertragsende als Datum; null = unbefristet"},"noticePeriodDays":{"description":"Kuendigungsfrist in Tagen, Vorgabewert 30"},"autoRenewal":{"description":"Verlaengert sich der Vertrag automatisch?"},"signedAt":{"description":"Zeitpunkt der Unterzeichnung; null = nicht unterzeichnet"},"signedBy":{"description":"Wer unterzeichnet hat, als Freitext"},"terminatedAt":{"description":"Kuendigungsdatum; null = nicht gekuendigt"},"terminationReason":{"description":"Kuendigungsgrund als Freitext"},"description":{"description":"Beschreibung des Vertragsgegenstands"},"createdBy":{"description":"Wer den Vertrag angelegt hat"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","value"],"additionalProperties":false},"example":{"id":"string","value":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"}},"operationId":"postApiV1Contracts","tags":["contracts"],"parameters":[],"summary":"Legt einen neuen Vertrag an","description":"Vergibt die Vertragsnummer selbst im Format `VTR-2026-0001`: laufendes Jahr plus eine Nummer, die aus den vorhandenen Verträgen DIESES Jahres gezählt wird. `status` beginnt immer auf `draft` und lässt sich im Rumpf nicht setzen — dafür gibt es /{id}/sign und /{id}/terminate. Die Antwort ist der neue Datensatz ohne Umschlag. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":255},"contractType":{"type":"string","default":"service"},"counterpartyName":{"type":"string"},"counterpartyType":{"type":"string","default":"customer"},"counterpartyId":{"type":"string"},"value":{"type":"number","exclusiveMinimum":0},"currency":{"type":"string","default":"EUR"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"noticePeriodDays":{"type":"integer","minimum":0,"default":30},"autoRenewal":{"type":"boolean","default":false},"description":{"type":"string"}},"required":["title"]},"example":{"title":"string","contractType":"string","counterpartyName":"string","counterpartyType":"string","counterpartyId":"string","value":1,"currency":"string","startDate":"2026-01-01","endDate":"2026-01-01","noticePeriodDays":0,"autoRenewal":true,"description":"string"}}}}}},"/api/v1/contracts/{id}":{"get":{"responses":{"200":{"description":"Vertrags-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Vertrags (UUID)"},"tenantId":{"description":"Mandant, dem der Vertrag gehoert"},"contractNumber":{"description":"Vertragsnummer"},"title":{"description":"Titel des Vertrags"},"contractType":{"description":"Vertragsart, Vorgabewert \"service\""},"status":{"description":"Status: draft, active, signed, terminated, expired oder cancelled"},"counterpartyName":{"description":"Name des Vertragspartners"},"counterpartyType":{"description":"Art des Partners, Vorgabewert \"customer\""},"counterpartyId":{"description":"Kennung des Partners im Kunden- oder Lieferantenstamm"},"value":{"type":["number","null"],"description":"Vertragswert als Zahl; null = nicht angegeben"},"currency":{"description":"Waehrungscode (ISO-4217), Vorgabewert \"EUR\""},"startDate":{"description":"Vertragsbeginn als Datum; null = offen"},"endDate":{"description":"Vertragsende als Datum; null = unbefristet"},"noticePeriodDays":{"description":"Kuendigungsfrist in Tagen, Vorgabewert 30"},"autoRenewal":{"description":"Verlaengert sich der Vertrag automatisch?"},"signedAt":{"description":"Zeitpunkt der Unterzeichnung; null = nicht unterzeichnet"},"signedBy":{"description":"Wer unterzeichnet hat, als Freitext"},"terminatedAt":{"description":"Kuendigungsdatum; null = nicht gekuendigt"},"terminationReason":{"description":"Kuendigungsgrund als Freitext"},"description":{"description":"Beschreibung des Vertragsgegenstands"},"createdBy":{"description":"Wer den Vertrag angelegt hat"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","value"],"additionalProperties":false},"example":{"id":"string","value":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Vertrag nicht gefunden"}},"operationId":"getApiV1ContractsById","tags":["contracts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Liefert einen einzelnen Vertrag","description":"Liest genau einen Vertrag über die id und gibt den Datensatz ohne Umschlag zurück; eine unbekannte id ergibt 404 `contract_not_found`. Ein stornierter Vertrag bleibt hier lesbar — beim Stornieren wird nur der Status gesetzt, die Zeile bleibt bestehen."},"patch":{"responses":{"200":{"description":"Vertrag aktualisiert — der Vertrag nach der Änderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Vertrags (UUID)"},"tenantId":{"description":"Mandant, dem der Vertrag gehoert"},"contractNumber":{"description":"Vertragsnummer"},"title":{"description":"Titel des Vertrags"},"contractType":{"description":"Vertragsart, Vorgabewert \"service\""},"status":{"description":"Status: draft, active, signed, terminated, expired oder cancelled"},"counterpartyName":{"description":"Name des Vertragspartners"},"counterpartyType":{"description":"Art des Partners, Vorgabewert \"customer\""},"counterpartyId":{"description":"Kennung des Partners im Kunden- oder Lieferantenstamm"},"value":{"type":["number","null"],"description":"Vertragswert als Zahl; null = nicht angegeben"},"currency":{"description":"Waehrungscode (ISO-4217), Vorgabewert \"EUR\""},"startDate":{"description":"Vertragsbeginn als Datum; null = offen"},"endDate":{"description":"Vertragsende als Datum; null = unbefristet"},"noticePeriodDays":{"description":"Kuendigungsfrist in Tagen, Vorgabewert 30"},"autoRenewal":{"description":"Verlaengert sich der Vertrag automatisch?"},"signedAt":{"description":"Zeitpunkt der Unterzeichnung; null = nicht unterzeichnet"},"signedBy":{"description":"Wer unterzeichnet hat, als Freitext"},"terminatedAt":{"description":"Kuendigungsdatum; null = nicht gekuendigt"},"terminationReason":{"description":"Kuendigungsgrund als Freitext"},"description":{"description":"Beschreibung des Vertragsgegenstands"},"createdBy":{"description":"Wer den Vertrag angelegt hat"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","value"],"additionalProperties":false},"example":{"id":"string","value":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Vertrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"contract_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1ContractsById","tags":["contracts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update contract","description":"Ändert einzelne Felder eines Vertrags — nicht mitgeschickte bleiben, wie sie sind. Status, Unterschrift und Kündigung lassen sich hier NICHT setzen; dafür gibt es /sign und /terminate. Ein gelöschter Vertrag ist nicht änderbar (404).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":255},"contractType":{"type":"string","default":"service"},"counterpartyName":{"type":"string"},"counterpartyType":{"type":"string","default":"customer"},"counterpartyId":{"type":"string"},"value":{"type":"number","exclusiveMinimum":0},"currency":{"type":"string","default":"EUR"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"noticePeriodDays":{"type":"integer","minimum":0,"default":30},"autoRenewal":{"type":"boolean","default":false},"description":{"type":"string"}}},"example":{"title":"string","contractType":"string","counterpartyName":"string","counterpartyType":"string","counterpartyId":"string","value":1,"currency":"string","startDate":"2026-01-01","endDate":"2026-01-01","noticePeriodDays":0,"autoRenewal":true,"description":"string"}}}}},"delete":{"responses":{"200":{"description":"Vertrag storniert — nur eine Quittung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","minLength":1,"description":"Erfolgsmeldung im Klartext, enthaelt die Id"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Vertrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"contract_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1ContractsById","tags":["contracts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Cancel contract","description":"Setzt einen Vertrag auf „cancelled\". Kein echtes Löschen: die Zeile bleibt bestehen und ist über GET /contracts/{id} weiter lesbar. Sie zählt danach in /stats weiterhin ins „total\", aber in keine der Status-Zahlen. Ein bereits stornierter Vertrag lässt sich erneut stornieren, ohne dass sich etwas ändert."}},"/api/v1/contracts/{id}/sign":{"post":{"responses":{"200":{"description":"Vertrag unterzeichnet — der Vertrag nach der Änderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Vertrags (UUID)"},"tenantId":{"description":"Mandant, dem der Vertrag gehoert"},"contractNumber":{"description":"Vertragsnummer"},"title":{"description":"Titel des Vertrags"},"contractType":{"description":"Vertragsart, Vorgabewert \"service\""},"status":{"description":"Status: draft, active, signed, terminated, expired oder cancelled"},"counterpartyName":{"description":"Name des Vertragspartners"},"counterpartyType":{"description":"Art des Partners, Vorgabewert \"customer\""},"counterpartyId":{"description":"Kennung des Partners im Kunden- oder Lieferantenstamm"},"value":{"type":["number","null"],"description":"Vertragswert als Zahl; null = nicht angegeben"},"currency":{"description":"Waehrungscode (ISO-4217), Vorgabewert \"EUR\""},"startDate":{"description":"Vertragsbeginn als Datum; null = offen"},"endDate":{"description":"Vertragsende als Datum; null = unbefristet"},"noticePeriodDays":{"description":"Kuendigungsfrist in Tagen, Vorgabewert 30"},"autoRenewal":{"description":"Verlaengert sich der Vertrag automatisch?"},"signedAt":{"description":"Zeitpunkt der Unterzeichnung; null = nicht unterzeichnet"},"signedBy":{"description":"Wer unterzeichnet hat, als Freitext"},"terminatedAt":{"description":"Kuendigungsdatum; null = nicht gekuendigt"},"terminationReason":{"description":"Kuendigungsgrund als Freitext"},"description":{"description":"Beschreibung des Vertragsgegenstands"},"createdBy":{"description":"Wer den Vertrag angelegt hat"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","value"],"additionalProperties":false},"example":{"id":"string","value":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Vertrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"contract_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1ContractsByIdSign","tags":["contracts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Sign contract","description":"Setzt einen Vertrag auf „signed\" und schreibt Unterzeichner und Zeitpunkt. Ohne Angabe von signedAt gilt der Zeitpunkt des Aufrufs. Es wird KEIN Statusübergang geprüft: auch ein gekündigter oder abgelaufener Vertrag lässt sich so unterschreiben, und eine zweite Unterschrift überschreibt die erste.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"signedBy":{"type":"string","minLength":1},"signedAt":{"type":"string","format":"date-time"}},"required":["signedBy"]},"example":{"signedBy":"string","signedAt":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/contracts/{id}/terminate":{"post":{"responses":{"200":{"description":"Vertrag gekündigt — der Vertrag nach der Änderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung des Vertrags (UUID)"},"tenantId":{"description":"Mandant, dem der Vertrag gehoert"},"contractNumber":{"description":"Vertragsnummer"},"title":{"description":"Titel des Vertrags"},"contractType":{"description":"Vertragsart, Vorgabewert \"service\""},"status":{"description":"Status: draft, active, signed, terminated, expired oder cancelled"},"counterpartyName":{"description":"Name des Vertragspartners"},"counterpartyType":{"description":"Art des Partners, Vorgabewert \"customer\""},"counterpartyId":{"description":"Kennung des Partners im Kunden- oder Lieferantenstamm"},"value":{"type":["number","null"],"description":"Vertragswert als Zahl; null = nicht angegeben"},"currency":{"description":"Waehrungscode (ISO-4217), Vorgabewert \"EUR\""},"startDate":{"description":"Vertragsbeginn als Datum; null = offen"},"endDate":{"description":"Vertragsende als Datum; null = unbefristet"},"noticePeriodDays":{"description":"Kuendigungsfrist in Tagen, Vorgabewert 30"},"autoRenewal":{"description":"Verlaengert sich der Vertrag automatisch?"},"signedAt":{"description":"Zeitpunkt der Unterzeichnung; null = nicht unterzeichnet"},"signedBy":{"description":"Wer unterzeichnet hat, als Freitext"},"terminatedAt":{"description":"Kuendigungsdatum; null = nicht gekuendigt"},"terminationReason":{"description":"Kuendigungsgrund als Freitext"},"description":{"description":"Beschreibung des Vertragsgegenstands"},"createdBy":{"description":"Wer den Vertrag angelegt hat"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","value"],"additionalProperties":false},"example":{"id":"string","value":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Vertrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"contract_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1ContractsByIdTerminate","tags":["contracts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Terminate contract","description":"Setzt einen Vertrag auf „terminated\" und schreibt Kündigungsdatum und Grund. Trotz des Namens wird KEIN aktiver Status vorausgesetzt und die hinterlegte Kündigungsfrist NICHT geprüft: ein Entwurf lässt sich ebenso kündigen wie ein laufender Vertrag, und das Kündigungsdatum darf in der Vergangenheit liegen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"terminationDate":{"type":"string","format":"date"},"reason":{"type":"string"}},"required":["terminationDate"]},"example":{"terminationDate":"2026-01-01","reason":"string"}}}}}},"/api/v1/contracts/{id}/risk-analysis":{"post":{"responses":{"200":{"description":"Risikoanalyse. Das Feld „source\" sagt, ob ein Modell beteiligt war — und nur dann tragen die Faktoren eine Kategorie.","content":{"application/json":{"schema":{"type":"object","properties":{"contractId":{"type":"string","format":"uuid","description":"Vertrags-Id aus dem Pfad"},"contractNumber":{"description":"Vertragsnummer des analysierten Vertrags"},"source":{"type":"string","enum":["rule-based","claude-haiku-4-5"],"description":"\"rule-based\" heisst: kein Modell befragt (nicht konfiguriert oder Modellfehler)"},"overallRisk":{"type":"string","description":"Gesamteinstufung; im KI-Zweig ungeprueft vom Modell uebernommen"},"factors":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"severity":{"type":"string","enum":["low","medium","high"],"description":"Gewicht des Faktors"},"note":{"type":"string","minLength":1,"description":"Begruendung im Klartext, deutsch"}},"required":["severity","note"],"additionalProperties":false,"description":"Regelbasierter Faktor — ohne Kategorie"},{"type":"object","properties":{"severity":{"type":"string","description":"Gewicht, wie das Modell es geliefert hat — ungeprueft"},"category":{"type":"string","description":"Rubrik, etwa Laufzeit, Kuendigung, Haftung oder Wert"},"note":{"type":"string","description":"Begruendung im Klartext, deutsch"}},"required":["severity","category","note"],"additionalProperties":false,"description":"Vom Modell erzeugter Faktor — mit Kategorie"}]},"description":"Die einzelnen Risikofaktoren"},"summary":{"type":"string","description":"Gesamtbewertung in Worten"},"recommendations":{"type":"array","items":{"type":"string"},"description":"Handlungsempfehlungen; im regelbasierten Zweig immer leer"},"analysedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Auswertung (ISO)"}},"required":["contractId","source","overallRisk","factors","summary","recommendations","analysedAt"],"additionalProperties":false},"example":{"contractId":"00000000-0000-4000-8000-000000000000","source":"rule-based","overallRisk":"string","factors":[{"severity":"low","note":"string"}],"summary":"string","recommendations":["string"],"analysedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Vertrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"contract_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1ContractsByIdRisk-analysis","tags":["contracts","ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Analyse contract risk","description":"W24-F: Risikoanalyse für einen Vertrag. Die Faktoren entstehen zuerst regelbasiert (Kündigungsfrist, automatische Verlängerung, fehlendes Enddatum, fehlender Vertragspartner, hoher Wert); ist ein Sprachmodell konfiguriert, ergänzt es Zusammenfassung, Empfehlungen und eigene Faktoren. Ohne Modell — oder wenn der Modellaufruf scheitert — antwortet der Aufruf trotzdem 200, dann mit source = rule-based und leerer Empfehlungsliste. Der Aufruf ändert nichts am Vertrag und speichert das Ergebnis nicht; zweimal gerufen kann er zweierlei sagen."}},"/api/v1/rma/stats":{"get":{"responses":{"200":{"description":"Die fuenf Kennzahlen","content":{"application/json":{"schema":{"type":"object","properties":{"open":{"type":"integer","minimum":0,"description":"Anzahl Faelle im Status `open`"},"inProgress":{"type":"integer","minimum":0,"description":"Anzahl Faelle im Status `in_progress`"},"resolved":{"type":"integer","minimum":0,"description":"Anzahl geloester Faelle INSGESAMT, nicht nur in diesem Monat"},"rejected":{"type":"integer","minimum":0,"description":"Anzahl abgelehnter Faelle insgesamt"},"resolvedThisMonth":{"type":"integer","minimum":0,"description":"Anzahl im laufenden KALENDERMONAT geloester Faelle — gezaehlt ab dem Ersten des Monats, nicht ueber die letzten 30 Tage"}},"required":["open","inProgress","resolved","rejected","resolvedThisMonth"],"additionalProperties":false},"example":{"open":0,"inProgress":0,"resolved":0,"rejected":0,"resolvedThisMonth":0}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"503":{"description":"Abfrage fehlgeschlagen (JSON nach dem Schema). Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext `database unavailable`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1RmaStats","tags":["rma"],"parameters":[],"summary":"RMA-Kennzahlen","description":"Fuenf Kennzahlen ueber ALLE Reklamationsfaelle des Mandanten. Der Aufruf kennt keine Filter und keinen Zeitraum-Parameter. Faelle in einem anderen als den vier genannten Status zaehlen in keine der Zahlen mit hinein — die Summe der ersten vier muss also nicht der Gesamtzahl entsprechen."}},"/api/v1/rma":{"get":{"responses":{"200":{"description":"Liste RMA-Faelle","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Falls (UUID)"},"tenantId":{"description":"Mandant, zu dem der Fall gehoert"},"rmaNumber":{"description":"Fortlaufende Nummer in der Form `RMA-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"description":"Betreff des Reklamationsfalls"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"description":"`open`, `in_progress`, `resolved` oder `rejected`"},"priority":{"description":"`critical`, `high`, `medium` oder `low`"},"category":{"description":"Art der Reklamation, Vorgabe `defect`; freier Text, keine feste Liste"},"customerName":{"description":"Name des Kunden als Text; `null`, wenn keiner erfasst wurde"},"customerId":{"description":"Kennung des Kunden; `null`, wenn der Fall keinem Kunden zugeordnet ist"},"orderId":{"description":"Kennung des betroffenen Auftrags; `null` ohne Zuordnung"},"orderNumber":{"description":"Auftragsnummer als Text; `null` ohne Zuordnung"},"productName":{"description":"Bezeichnung des betroffenen Artikels; `null`, wenn nicht erfasst"},"productSku":{"description":"Artikelnummer des betroffenen Artikels; `null`, wenn nicht erfasst"},"quantity":{"type":"number","description":"Betroffene Menge; 1, wenn nichts erfasst wurde"},"reportedAt":{"description":"Zeitpunkt der Meldung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Eine spaetere ABLEHNUNG loescht ihn NICHT — ein abgelehnter Fall kann also weiterhin einen Loesungszeitpunkt tragen"},"resolution":{"description":"Loesungstext ODER Ablehnungsgrund — beide Aufrufe schreiben in dieses eine Feld"},"refundAmount":{"type":"number","description":"Erstattungsbetrag in Euro. IMMER eine Zahl, nie `null` — 0 heisst „keine Erstattung\" und ist von „nicht erfasst\" nicht zu unterscheiden"},"assignedTo":{"description":"Bearbeiter; `null`, solange niemand zustaendig ist"},"createdBy":{"description":"Anlegender Benutzer; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","quantity","refundAmount"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","quantity":0,"refundAmount":0}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Rma","tags":["rma"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string"}}],"summary":"RMA-Fälle auflisten","description":"Liste aller RMA-Faelle mit Status-Filter und Pagination."},"post":{"responses":{"201":{"description":"RMA-Fall angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Falls (UUID)"},"tenantId":{"description":"Mandant, zu dem der Fall gehoert"},"rmaNumber":{"description":"Fortlaufende Nummer in der Form `RMA-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"description":"Betreff des Reklamationsfalls"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"description":"`open`, `in_progress`, `resolved` oder `rejected`"},"priority":{"description":"`critical`, `high`, `medium` oder `low`"},"category":{"description":"Art der Reklamation, Vorgabe `defect`; freier Text, keine feste Liste"},"customerName":{"description":"Name des Kunden als Text; `null`, wenn keiner erfasst wurde"},"customerId":{"description":"Kennung des Kunden; `null`, wenn der Fall keinem Kunden zugeordnet ist"},"orderId":{"description":"Kennung des betroffenen Auftrags; `null` ohne Zuordnung"},"orderNumber":{"description":"Auftragsnummer als Text; `null` ohne Zuordnung"},"productName":{"description":"Bezeichnung des betroffenen Artikels; `null`, wenn nicht erfasst"},"productSku":{"description":"Artikelnummer des betroffenen Artikels; `null`, wenn nicht erfasst"},"quantity":{"type":"number","description":"Betroffene Menge; 1, wenn nichts erfasst wurde"},"reportedAt":{"description":"Zeitpunkt der Meldung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Eine spaetere ABLEHNUNG loescht ihn NICHT — ein abgelehnter Fall kann also weiterhin einen Loesungszeitpunkt tragen"},"resolution":{"description":"Loesungstext ODER Ablehnungsgrund — beide Aufrufe schreiben in dieses eine Feld"},"refundAmount":{"type":"number","description":"Erstattungsbetrag in Euro. IMMER eine Zahl, nie `null` — 0 heisst „keine Erstattung\" und ist von „nicht erfasst\" nicht zu unterscheiden"},"assignedTo":{"description":"Bearbeiter; `null`, solange niemand zustaendig ist"},"createdBy":{"description":"Anlegender Benutzer; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","quantity","refundAmount"],"additionalProperties":false},"example":{"id":"string","quantity":0,"refundAmount":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1Rma","tags":["rma"],"parameters":[],"summary":"RMA-Fall anlegen","description":"Neuen RMA-Fall anlegen (RMA-Nummer wird automatisch generiert).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"priority":{"type":"string","enum":["critical","high","medium","low"],"default":"medium"},"category":{"type":"string","default":"defect"},"customerName":{"type":"string"},"customerId":{"type":"string"},"orderId":{"type":"string"},"orderNumber":{"type":"string"},"productName":{"type":"string"},"productSku":{"type":"string"},"quantity":{"type":"integer","minimum":1,"default":1},"assignedTo":{"type":"string"}},"required":["title"]},"example":{"title":"string","description":"string","priority":"critical","category":"string","customerName":"string","customerId":"string","orderId":"string","orderNumber":"string","productName":"string","productSku":"string","quantity":1,"assignedTo":"string"}}}}}},"/api/v1/rma/{id}":{"get":{"responses":{"200":{"description":"RMA-Fall","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Falls (UUID)"},"tenantId":{"description":"Mandant, zu dem der Fall gehoert"},"rmaNumber":{"description":"Fortlaufende Nummer in der Form `RMA-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"description":"Betreff des Reklamationsfalls"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"description":"`open`, `in_progress`, `resolved` oder `rejected`"},"priority":{"description":"`critical`, `high`, `medium` oder `low`"},"category":{"description":"Art der Reklamation, Vorgabe `defect`; freier Text, keine feste Liste"},"customerName":{"description":"Name des Kunden als Text; `null`, wenn keiner erfasst wurde"},"customerId":{"description":"Kennung des Kunden; `null`, wenn der Fall keinem Kunden zugeordnet ist"},"orderId":{"description":"Kennung des betroffenen Auftrags; `null` ohne Zuordnung"},"orderNumber":{"description":"Auftragsnummer als Text; `null` ohne Zuordnung"},"productName":{"description":"Bezeichnung des betroffenen Artikels; `null`, wenn nicht erfasst"},"productSku":{"description":"Artikelnummer des betroffenen Artikels; `null`, wenn nicht erfasst"},"quantity":{"type":"number","description":"Betroffene Menge; 1, wenn nichts erfasst wurde"},"reportedAt":{"description":"Zeitpunkt der Meldung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Eine spaetere ABLEHNUNG loescht ihn NICHT — ein abgelehnter Fall kann also weiterhin einen Loesungszeitpunkt tragen"},"resolution":{"description":"Loesungstext ODER Ablehnungsgrund — beide Aufrufe schreiben in dieses eine Feld"},"refundAmount":{"type":"number","description":"Erstattungsbetrag in Euro. IMMER eine Zahl, nie `null` — 0 heisst „keine Erstattung\" und ist von „nicht erfasst\" nicht zu unterscheiden"},"assignedTo":{"description":"Bearbeiter; `null`, solange niemand zustaendig ist"},"createdBy":{"description":"Anlegender Benutzer; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"},"notes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Notiz (UUID)"},"rmaId":{"type":"string","description":"Fall, an dem die Notiz haengt"},"tenantId":{"type":"string","description":"Mandant, zu dem die Notiz gehoert"},"author":{"type":["string","null"],"description":"Verfasser als Text; `null`, wenn keiner erfasst wurde — dieser Aufruf setzt ihn NICHT selbst"},"content":{"type":"string","description":"Der Notiztext"},"isInternal":{"type":["boolean","null"],"description":"`true` = nur intern sichtbar. `null` ist moeglich, weil die Spalte nur einen Vorgabewert hat und kein NOT NULL. Wer die Sichtbarkeit durchsetzt, ist die lesende Oberflaeche"},"createdAt":{"description":"Anlagezeitpunkt"}},"required":["id","rmaId","tenantId","author","content","isInternal"],"additionalProperties":false},"description":"Alle Notizen des Falls, interne wie externe — ungefiltert und ohne Obergrenze"}},"required":["id","quantity","refundAmount","notes"],"additionalProperties":false},"example":{"id":"string","quantity":0,"refundAmount":0,"notes":[{"id":"string","rmaId":"string","tenantId":"string","author":"string","content":"string","isInternal":true}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"RMA nicht gefunden"}},"operationId":"getApiV1RmaById","tags":["rma"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"RMA-Fall-Detail abrufen","description":"RMA-Fall mit allen Notizen abrufen."},"patch":{"responses":{"200":{"description":"Der Fall nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Falls (UUID)"},"tenantId":{"description":"Mandant, zu dem der Fall gehoert"},"rmaNumber":{"description":"Fortlaufende Nummer in der Form `RMA-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"description":"Betreff des Reklamationsfalls"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"description":"`open`, `in_progress`, `resolved` oder `rejected`"},"priority":{"description":"`critical`, `high`, `medium` oder `low`"},"category":{"description":"Art der Reklamation, Vorgabe `defect`; freier Text, keine feste Liste"},"customerName":{"description":"Name des Kunden als Text; `null`, wenn keiner erfasst wurde"},"customerId":{"description":"Kennung des Kunden; `null`, wenn der Fall keinem Kunden zugeordnet ist"},"orderId":{"description":"Kennung des betroffenen Auftrags; `null` ohne Zuordnung"},"orderNumber":{"description":"Auftragsnummer als Text; `null` ohne Zuordnung"},"productName":{"description":"Bezeichnung des betroffenen Artikels; `null`, wenn nicht erfasst"},"productSku":{"description":"Artikelnummer des betroffenen Artikels; `null`, wenn nicht erfasst"},"quantity":{"type":"number","description":"Betroffene Menge; 1, wenn nichts erfasst wurde"},"reportedAt":{"description":"Zeitpunkt der Meldung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Eine spaetere ABLEHNUNG loescht ihn NICHT — ein abgelehnter Fall kann also weiterhin einen Loesungszeitpunkt tragen"},"resolution":{"description":"Loesungstext ODER Ablehnungsgrund — beide Aufrufe schreiben in dieses eine Feld"},"refundAmount":{"type":"number","description":"Erstattungsbetrag in Euro. IMMER eine Zahl, nie `null` — 0 heisst „keine Erstattung\" und ist von „nicht erfasst\" nicht zu unterscheiden"},"assignedTo":{"description":"Bearbeiter; `null`, solange niemand zustaendig ist"},"createdBy":{"description":"Anlegender Benutzer; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","quantity","refundAmount"],"additionalProperties":false},"example":{"id":"string","quantity":0,"refundAmount":0}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Fall mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rma_not_found","description":"Kein Fall mit dieser Kennung im Mandanten"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Aenderung fehlgeschlagen (JSON nach dem Schema) — es wurde nichts geaendert. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1RmaById","tags":["rma"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"RMA-Fall aktualisieren","description":"Aendert einzelne Felder eines Reklamationsfalls. Der Handler liest den Fall zuerst und schreibt ihn dann VOLLSTAENDIG zurueck — nicht mitgesendete Felder behalten dabei ihren Wert. Ein leerer Rumpf ist deshalb kein Fehler: er speichert den unveraenderten Stand und setzt nur `updatedAt` neu. `status`, `resolution` und der Erstattungsbetrag lassen sich hier NICHT setzen — dafuer gibt es `/resolve` und `/reject`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string"},"priority":{"type":"string","enum":["critical","high","medium","low"],"default":"medium"},"category":{"type":"string","default":"defect"},"customerName":{"type":"string"},"customerId":{"type":"string"},"orderId":{"type":"string"},"orderNumber":{"type":"string"},"productName":{"type":"string"},"productSku":{"type":"string"},"quantity":{"type":"integer","minimum":1,"default":1},"assignedTo":{"type":"string"}}},"example":{"title":"string","description":"string","priority":"critical","category":"string","customerName":"string","customerId":"string","orderId":"string","orderNumber":"string","productName":"string","productSku":"string","quantity":1,"assignedTo":"string"}}}}}},"/api/v1/rma/{id}/notes":{"post":{"responses":{"201":{"description":"Die angelegte Notiz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Notiz (UUID)"},"rmaId":{"type":"string","description":"Fall, an dem die Notiz haengt"},"tenantId":{"type":"string","description":"Mandant, zu dem die Notiz gehoert"},"author":{"type":["string","null"],"description":"Verfasser als Text; `null`, wenn keiner erfasst wurde — dieser Aufruf setzt ihn NICHT selbst"},"content":{"type":"string","description":"Der Notiztext"},"isInternal":{"type":["boolean","null"],"description":"`true` = nur intern sichtbar. `null` ist moeglich, weil die Spalte nur einen Vorgabewert hat und kein NOT NULL. Wer die Sichtbarkeit durchsetzt, ist die lesende Oberflaeche"},"createdAt":{"description":"Anlagezeitpunkt"}},"required":["id","rmaId","tenantId","author","content","isInternal"],"additionalProperties":false},"example":{"id":"string","rmaId":"string","tenantId":"string","author":"string","content":"string","isInternal":true}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis) — etwa ein leerer Text","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Fall mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rma_not_found","description":"Kein Fall mit dieser Kennung im Mandanten"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Anlegen fehlgeschlagen (JSON nach dem Schema) — die Transaktion wurde zurueckgerollt, der Status blieb also ebenfalls stehen. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1RmaByIdNotes","tags":["rma"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Notiz zu einem RMA-Fall hinzufügen","description":"Haengt eine Notiz an einen Reklamationsfall. NEBENWIRKUNG: steht der Fall auf `open`, springt er dabei auf `in_progress` — Notiz und Statuswechsel landen zusammen oder gar nicht. Wer nur einen Vermerk hinterlassen will, veraendert damit also den Status. `isInternal: true` markiert die Notiz als nur intern sichtbar; wer das durchsetzt, entscheidet die lesende Oberflaeche, nicht diese Route. Die Antwort ist die NOTIZ, nicht der Fall.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","minLength":1},"isInternal":{"type":"boolean","default":false}},"required":["content"]},"example":{"content":"string","isInternal":true}}}}}},"/api/v1/rma/{id}/resolve":{"post":{"responses":{"200":{"description":"Der geloeste Fall","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Falls (UUID)"},"tenantId":{"description":"Mandant, zu dem der Fall gehoert"},"rmaNumber":{"description":"Fortlaufende Nummer in der Form `RMA-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"description":"Betreff des Reklamationsfalls"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"description":"`open`, `in_progress`, `resolved` oder `rejected`"},"priority":{"description":"`critical`, `high`, `medium` oder `low`"},"category":{"description":"Art der Reklamation, Vorgabe `defect`; freier Text, keine feste Liste"},"customerName":{"description":"Name des Kunden als Text; `null`, wenn keiner erfasst wurde"},"customerId":{"description":"Kennung des Kunden; `null`, wenn der Fall keinem Kunden zugeordnet ist"},"orderId":{"description":"Kennung des betroffenen Auftrags; `null` ohne Zuordnung"},"orderNumber":{"description":"Auftragsnummer als Text; `null` ohne Zuordnung"},"productName":{"description":"Bezeichnung des betroffenen Artikels; `null`, wenn nicht erfasst"},"productSku":{"description":"Artikelnummer des betroffenen Artikels; `null`, wenn nicht erfasst"},"quantity":{"type":"number","description":"Betroffene Menge; 1, wenn nichts erfasst wurde"},"reportedAt":{"description":"Zeitpunkt der Meldung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Eine spaetere ABLEHNUNG loescht ihn NICHT — ein abgelehnter Fall kann also weiterhin einen Loesungszeitpunkt tragen"},"resolution":{"description":"Loesungstext ODER Ablehnungsgrund — beide Aufrufe schreiben in dieses eine Feld"},"refundAmount":{"type":"number","description":"Erstattungsbetrag in Euro. IMMER eine Zahl, nie `null` — 0 heisst „keine Erstattung\" und ist von „nicht erfasst\" nicht zu unterscheiden"},"assignedTo":{"description":"Bearbeiter; `null`, solange niemand zustaendig ist"},"createdBy":{"description":"Anlegender Benutzer; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","quantity","refundAmount"],"additionalProperties":false},"example":{"id":"string","quantity":0,"refundAmount":0}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis) — etwa ein leerer Loesungstext","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Fall mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rma_not_found","description":"Kein Fall mit dieser Kennung im Mandanten"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Loesen fehlgeschlagen (JSON nach dem Schema) — es wurde nichts geaendert. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1RmaByIdResolve","tags":["rma"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"RMA-Fall als gelöst markieren","description":"Setzt Status, Loesungszeitpunkt, Loesungstext und Erstattungsbetrag. Der Loesungstext ist Pflicht. ACHTUNG: OHNE `refundAmount` wird 0 GESCHRIEBEN — ein bereits erfasster Erstattungsbetrag geht dabei verloren. Wer nur den Text nachtragen will, muss den Betrag mitschicken. Der Aufruf prueft den bisherigen Status nicht: auch ein abgelehnter Fall laesst sich so loesen, und ein zweiter Aufruf ueberschreibt Zeitpunkt und Text.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"resolution":{"type":"string","minLength":1},"refundAmount":{"type":"number","minimum":0}},"required":["resolution"]},"example":{"resolution":"string","refundAmount":0}}}}}},"/api/v1/rma/{id}/reject":{"post":{"responses":{"200":{"description":"Der abgelehnte Fall","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Falls (UUID)"},"tenantId":{"description":"Mandant, zu dem der Fall gehoert"},"rmaNumber":{"description":"Fortlaufende Nummer in der Form `RMA-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"description":"Betreff des Reklamationsfalls"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"description":"`open`, `in_progress`, `resolved` oder `rejected`"},"priority":{"description":"`critical`, `high`, `medium` oder `low`"},"category":{"description":"Art der Reklamation, Vorgabe `defect`; freier Text, keine feste Liste"},"customerName":{"description":"Name des Kunden als Text; `null`, wenn keiner erfasst wurde"},"customerId":{"description":"Kennung des Kunden; `null`, wenn der Fall keinem Kunden zugeordnet ist"},"orderId":{"description":"Kennung des betroffenen Auftrags; `null` ohne Zuordnung"},"orderNumber":{"description":"Auftragsnummer als Text; `null` ohne Zuordnung"},"productName":{"description":"Bezeichnung des betroffenen Artikels; `null`, wenn nicht erfasst"},"productSku":{"description":"Artikelnummer des betroffenen Artikels; `null`, wenn nicht erfasst"},"quantity":{"type":"number","description":"Betroffene Menge; 1, wenn nichts erfasst wurde"},"reportedAt":{"description":"Zeitpunkt der Meldung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Eine spaetere ABLEHNUNG loescht ihn NICHT — ein abgelehnter Fall kann also weiterhin einen Loesungszeitpunkt tragen"},"resolution":{"description":"Loesungstext ODER Ablehnungsgrund — beide Aufrufe schreiben in dieses eine Feld"},"refundAmount":{"type":"number","description":"Erstattungsbetrag in Euro. IMMER eine Zahl, nie `null` — 0 heisst „keine Erstattung\" und ist von „nicht erfasst\" nicht zu unterscheiden"},"assignedTo":{"description":"Bearbeiter; `null`, solange niemand zustaendig ist"},"createdBy":{"description":"Anlegender Benutzer; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","quantity","refundAmount"],"additionalProperties":false},"example":{"id":"string","quantity":0,"refundAmount":0}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis) — etwa eine leere Begruendung","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Fall mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rma_not_found","description":"Kein Fall mit dieser Kennung im Mandanten"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Ablehnen fehlgeschlagen (JSON nach dem Schema) — es wurde nichts geaendert. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1RmaByIdReject","tags":["rma"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"RMA-Fall ablehnen","description":"Setzt den Status auf `rejected` und schreibt die Begruendung in dasselbe Feld, in dem sonst der Loesungstext steht — eine zuvor erfasste Loesung wird dabei ueberschrieben. Der Loesungszeitpunkt bleibt unangetastet: ein zuvor geloester und dann abgelehnter Fall traegt weiterhin seinen Loesungszeitpunkt. Der Aufruf prueft den bisherigen Status nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","minLength":1}},"required":["reason"]},"example":{"reason":"string"}}}}}},"/api/v1/tickets/stats":{"get":{"responses":{"200":{"description":"Die vier Kennzahlen","content":{"application/json":{"schema":{"type":"object","properties":{"open":{"type":"integer","minimum":0,"description":"Anzahl Tickets im Status `open`; geloeschte zaehlen nicht mit"},"inProgress":{"type":"integer","minimum":0,"description":"Anzahl Tickets im Status `in_progress`"},"closedThisMonth":{"type":"integer","minimum":0,"description":"Anzahl im laufenden KALENDERMONAT geloester Tickets — gezaehlt ab dem Ersten des Monats, nicht ueber die letzten 30 Tage"},"avgResolutionHours":{"type":["number","null"],"minimum":0,"description":"Durchschnittliche Loesungsdauer in Stunden ueber ALLE geschlossenen Tickets, auf eine Nachkommastelle gerundet. `null` heisst „noch nichts geloest\" — nicht 0 Stunden"}},"required":["open","inProgress","closedThisMonth","avgResolutionHours"],"additionalProperties":false},"example":{"open":0,"inProgress":0,"closedThisMonth":0,"avgResolutionHours":0}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"503":{"description":"Abfrage fehlgeschlagen (JSON nach dem Schema). Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext `database unavailable`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1TicketsStats","tags":["tickets"],"parameters":[],"summary":"Ticket-Kennzahlen","description":"Vier Kennzahlen ueber ALLE Tickets des Mandanten: offen, in Bearbeitung, in diesem Kalendermonat geloest, und die durchschnittliche Loesungsdauer in Stunden. Geloeschte Tickets bleiben ueberall aussen vor. Der Aufruf kennt keine Filter und keinen Zeitraum-Parameter."}},"/api/v1/tickets":{"get":{"responses":{"200":{"description":"Liste Tickets","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Tickets (UUID)"},"tenantId":{"description":"Mandant, zu dem das Ticket gehoert"},"ticketNumber":{"type":"string","description":"Fortlaufende Nummer in der Form `TKT-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"type":"string","description":"Betreff des Tickets"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"type":"string","description":"`open`, `in_progress`, `waiting` oder `closed`"},"priority":{"type":"string","description":"`low`, `medium`, `high` oder `critical`"},"category":{"type":"string","description":"`general`, `bug`, `feature`, `billing` oder `access`"},"source":{"type":"string","description":"Woher das Ticket kam: `internal`, `email`, `api` oder `customer_portal`"},"reporterName":{"description":"Name des Melders; `null`, wenn keiner erfasst wurde"},"reporterEmail":{"description":"E-Mail des Melders; `null`, wenn keine erfasst wurde"},"reporterId":{"description":"Benutzerkennung des Melders; `null` bei Meldungen von aussen"},"assigneeId":{"description":"Benutzerkennung des Bearbeiters; `null`, solange niemand zustaendig ist"},"assigneeName":{"description":"Name des Bearbeiters; `null`, solange niemand zustaendig ist"},"relatedEntityType":{"description":"Art eines verknuepften Datensatzes, z. B. `invoice`; `null` ohne Verknuepfung"},"relatedEntityId":{"description":"Kennung des verknuepften Datensatzes; `null` ohne Verknuepfung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Setzt NUR `POST /tickets/{id}/close` — ein Statuswechsel per PATCH laesst das Feld leer"},"resolution":{"description":"Loesungstext; `null`, solange offen"},"createdBy":{"description":"Benutzerkennung des Anlegenden; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","ticketNumber","title","status","priority","category","source"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","ticketNumber":"string","title":"string","status":"string","priority":"string","category":"string","source":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Tickets","tags":["tickets"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["open","in_progress","waiting","closed"]}},{"in":"query","name":"priority","schema":{"type":"string","enum":["low","medium","high","critical"]}},{"in":"query","name":"assigneeId","schema":{"type":"string"}},{"in":"query","name":"category","schema":{"type":"string","enum":["general","bug","feature","billing","access"]}},{"in":"query","name":"search","schema":{"type":"string"}}],"summary":"Tickets auflisten","description":"Liste aller Tickets mit Filter (Status, Prioritaet, Assignee, Kategorie, Suche)."},"post":{"responses":{"201":{"description":"Ticket erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Tickets (UUID)"},"tenantId":{"description":"Mandant, zu dem das Ticket gehoert"},"ticketNumber":{"type":"string","description":"Fortlaufende Nummer in der Form `TKT-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"type":"string","description":"Betreff des Tickets"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"type":"string","description":"`open`, `in_progress`, `waiting` oder `closed`"},"priority":{"type":"string","description":"`low`, `medium`, `high` oder `critical`"},"category":{"type":"string","description":"`general`, `bug`, `feature`, `billing` oder `access`"},"source":{"type":"string","description":"Woher das Ticket kam: `internal`, `email`, `api` oder `customer_portal`"},"reporterName":{"description":"Name des Melders; `null`, wenn keiner erfasst wurde"},"reporterEmail":{"description":"E-Mail des Melders; `null`, wenn keine erfasst wurde"},"reporterId":{"description":"Benutzerkennung des Melders; `null` bei Meldungen von aussen"},"assigneeId":{"description":"Benutzerkennung des Bearbeiters; `null`, solange niemand zustaendig ist"},"assigneeName":{"description":"Name des Bearbeiters; `null`, solange niemand zustaendig ist"},"relatedEntityType":{"description":"Art eines verknuepften Datensatzes, z. B. `invoice`; `null` ohne Verknuepfung"},"relatedEntityId":{"description":"Kennung des verknuepften Datensatzes; `null` ohne Verknuepfung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Setzt NUR `POST /tickets/{id}/close` — ein Statuswechsel per PATCH laesst das Feld leer"},"resolution":{"description":"Loesungstext; `null`, solange offen"},"createdBy":{"description":"Benutzerkennung des Anlegenden; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","ticketNumber","title","status","priority","category","source"],"additionalProperties":false},"example":{"id":"string","ticketNumber":"string","title":"string","status":"string","priority":"string","category":"string","source":"string"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1Tickets","tags":["tickets"],"parameters":[],"summary":"Ticket erstellen","description":"Neues Ticket erstellen (Ticket-Nummer wird automatisch generiert).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":500},"description":{"type":"string"},"priority":{"type":"string","enum":["low","medium","high","critical"],"default":"medium"},"category":{"type":"string","enum":["general","bug","feature","billing","access"],"default":"general"},"source":{"type":"string","enum":["internal","email","api","customer_portal"],"default":"internal"},"reporterName":{"type":"string","maxLength":200},"reporterEmail":{"type":"string","format":"email"},"reporterId":{"type":"string"},"assigneeId":{"type":"string"},"assigneeName":{"type":"string","maxLength":200},"relatedEntityType":{"type":"string"},"relatedEntityId":{"type":"string"}},"required":["title"]},"example":{"title":"string","description":"string","priority":"low","category":"general","source":"internal","reporterName":"string","reporterEmail":"beispiel@example.com","reporterId":"string","assigneeId":"string","assigneeName":"string","relatedEntityType":"string","relatedEntityId":"string"}}}}}},"/api/v1/tickets/{id}":{"get":{"responses":{"200":{"description":"Ticket mit Kommentaren","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Tickets (UUID)"},"tenantId":{"description":"Mandant, zu dem das Ticket gehoert"},"ticketNumber":{"type":"string","description":"Fortlaufende Nummer in der Form `TKT-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"type":"string","description":"Betreff des Tickets"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"type":"string","description":"`open`, `in_progress`, `waiting` oder `closed`"},"priority":{"type":"string","description":"`low`, `medium`, `high` oder `critical`"},"category":{"type":"string","description":"`general`, `bug`, `feature`, `billing` oder `access`"},"source":{"type":"string","description":"Woher das Ticket kam: `internal`, `email`, `api` oder `customer_portal`"},"reporterName":{"description":"Name des Melders; `null`, wenn keiner erfasst wurde"},"reporterEmail":{"description":"E-Mail des Melders; `null`, wenn keine erfasst wurde"},"reporterId":{"description":"Benutzerkennung des Melders; `null` bei Meldungen von aussen"},"assigneeId":{"description":"Benutzerkennung des Bearbeiters; `null`, solange niemand zustaendig ist"},"assigneeName":{"description":"Name des Bearbeiters; `null`, solange niemand zustaendig ist"},"relatedEntityType":{"description":"Art eines verknuepften Datensatzes, z. B. `invoice`; `null` ohne Verknuepfung"},"relatedEntityId":{"description":"Kennung des verknuepften Datensatzes; `null` ohne Verknuepfung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Setzt NUR `POST /tickets/{id}/close` — ein Statuswechsel per PATCH laesst das Feld leer"},"resolution":{"description":"Loesungstext; `null`, solange offen"},"createdBy":{"description":"Benutzerkennung des Anlegenden; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"},"comments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Kommentars (UUID)"},"ticketId":{"type":"string","description":"Ticket, an dem der Kommentar haengt"},"authorId":{"type":["string","null"],"description":"Benutzerkennung des Verfassers; `null` bei System-Kommentaren ohne Benutzerkontext"},"authorName":{"type":["string","null"],"description":"Name des Verfassers. Bei Schliessen, Wiedereroeffnen und Loeschen steht hier `System`, wenn kein Benutzername vorlag"},"content":{"type":"string","description":"Der Kommentartext"},"isInternal":{"type":"boolean","description":"`true` = nur intern sichtbar. Alle vom Server selbst geschriebenen Kommentare sind intern; wer die Sichtbarkeit durchsetzt, ist die lesende Oberflaeche, nicht diese Route"},"createdAt":{"description":"Anlagezeitpunkt"}},"required":["id","ticketId","authorId","authorName","content","isInternal"],"additionalProperties":false},"description":"Alle Kommentare des Tickets, interne wie oeffentliche — ungefiltert und ohne Obergrenze"}},"required":["id","ticketNumber","title","status","priority","category","source","comments"],"additionalProperties":false},"example":{"id":"string","ticketNumber":"string","title":"string","status":"string","priority":"string","category":"string","source":"string","comments":[{"id":"string","ticketId":"string","authorId":"string","authorName":"string","content":"string","isInternal":true}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Ticket nicht gefunden"}},"operationId":"getApiV1TicketsById","tags":["tickets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ticket-Detail abrufen","description":"Ticket-Detail mit allen Kommentaren abrufen."},"patch":{"responses":{"200":{"description":"Das Ticket nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Tickets (UUID)"},"tenantId":{"description":"Mandant, zu dem das Ticket gehoert"},"ticketNumber":{"type":"string","description":"Fortlaufende Nummer in der Form `TKT-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"type":"string","description":"Betreff des Tickets"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"type":"string","description":"`open`, `in_progress`, `waiting` oder `closed`"},"priority":{"type":"string","description":"`low`, `medium`, `high` oder `critical`"},"category":{"type":"string","description":"`general`, `bug`, `feature`, `billing` oder `access`"},"source":{"type":"string","description":"Woher das Ticket kam: `internal`, `email`, `api` oder `customer_portal`"},"reporterName":{"description":"Name des Melders; `null`, wenn keiner erfasst wurde"},"reporterEmail":{"description":"E-Mail des Melders; `null`, wenn keine erfasst wurde"},"reporterId":{"description":"Benutzerkennung des Melders; `null` bei Meldungen von aussen"},"assigneeId":{"description":"Benutzerkennung des Bearbeiters; `null`, solange niemand zustaendig ist"},"assigneeName":{"description":"Name des Bearbeiters; `null`, solange niemand zustaendig ist"},"relatedEntityType":{"description":"Art eines verknuepften Datensatzes, z. B. `invoice`; `null` ohne Verknuepfung"},"relatedEntityId":{"description":"Kennung des verknuepften Datensatzes; `null` ohne Verknuepfung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Setzt NUR `POST /tickets/{id}/close` — ein Statuswechsel per PATCH laesst das Feld leer"},"resolution":{"description":"Loesungstext; `null`, solange offen"},"createdBy":{"description":"Benutzerkennung des Anlegenden; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","ticketNumber","title","status","priority","category","source"],"additionalProperties":false},"example":{"id":"string","ticketNumber":"string","title":"string","status":"string","priority":"string","category":"string","source":"string"}}}},"400":{"description":"ZWEI Formen unter demselben Code: das rohe Zod-Ergebnis bei unzulaessigem Rumpf, oder `{ \"error\": \"no_fields_to_update\" }`, wenn der Rumpf kein aenderbares Feld enthaelt.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]},{"type":"object","properties":{"error":{"type":"string","const":"no_fields_to_update","description":"Der Rumpf enthielt kein Feld, das geschrieben werden koennte"}},"required":["error"],"additionalProperties":false}]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Ticket mit dieser Kennung, oder es ist bereits geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"ticket_not_found","description":"Nicht vorhanden, bereits geloescht — oder beim Wiedereroeffnen: nicht mehr auffindbar"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Aenderung fehlgeschlagen (JSON nach dem Schema) — es wurde nichts geaendert. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1TicketsById","tags":["tickets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ticket-Felder aktualisieren","description":"Aendert einzelne Felder eines Tickets. Nur mitgesendete Felder werden geschrieben; `updatedAt` setzt der Server selbst. ACHTUNG: ein Wechsel auf `status: \"closed\"` ueber diesen Weg setzt WEDER den Loesungszeitpunkt noch einen Loesungstext und schreibt keinen System-Kommentar — dafuer gibt es `POST /tickets/{id}/close`. Ein so geschlossenes Ticket fehlt deshalb in `closedThisMonth`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","in_progress","waiting","closed"]},"priority":{"type":"string","enum":["low","medium","high","critical"]},"assigneeId":{"type":["string","null"]},"assigneeName":{"type":["string","null"],"maxLength":200},"title":{"type":"string","minLength":1,"maxLength":500},"description":{"type":"string"},"category":{"type":"string","enum":["general","bug","feature","billing","access"]}}},"example":{"status":"open","priority":"low","assigneeId":"string","assigneeName":"string","title":"string","description":"string","category":"general"}}}}},"delete":{"responses":{"200":{"description":"ZWEI Bauformen — `mode` sagt welche. Nur beim Soft-Delete kommt das Ticket mit","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"message":{"type":"string","description":"Deutscher Satz der Form `Ticket <id> gelöscht`"},"id":{"type":"string","description":"Kennung des geloeschten Tickets, unveraendert aus dem Pfad"},"mode":{"type":"string","const":"soft","description":"Es wurde nur `deleted_at` gesetzt — die Zeile bleibt vollstaendig erhalten"},"ticket":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Tickets (UUID)"},"tenantId":{"description":"Mandant, zu dem das Ticket gehoert"},"ticketNumber":{"type":"string","description":"Fortlaufende Nummer in der Form `TKT-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"type":"string","description":"Betreff des Tickets"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"type":"string","description":"`open`, `in_progress`, `waiting` oder `closed`"},"priority":{"type":"string","description":"`low`, `medium`, `high` oder `critical`"},"category":{"type":"string","description":"`general`, `bug`, `feature`, `billing` oder `access`"},"source":{"type":"string","description":"Woher das Ticket kam: `internal`, `email`, `api` oder `customer_portal`"},"reporterName":{"description":"Name des Melders; `null`, wenn keiner erfasst wurde"},"reporterEmail":{"description":"E-Mail des Melders; `null`, wenn keine erfasst wurde"},"reporterId":{"description":"Benutzerkennung des Melders; `null` bei Meldungen von aussen"},"assigneeId":{"description":"Benutzerkennung des Bearbeiters; `null`, solange niemand zustaendig ist"},"assigneeName":{"description":"Name des Bearbeiters; `null`, solange niemand zustaendig ist"},"relatedEntityType":{"description":"Art eines verknuepften Datensatzes, z. B. `invoice`; `null` ohne Verknuepfung"},"relatedEntityId":{"description":"Kennung des verknuepften Datensatzes; `null` ohne Verknuepfung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Setzt NUR `POST /tickets/{id}/close` — ein Statuswechsel per PATCH laesst das Feld leer"},"resolution":{"description":"Loesungstext; `null`, solange offen"},"createdBy":{"description":"Benutzerkennung des Anlegenden; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","ticketNumber","title","status","priority","category","source"],"additionalProperties":false,"description":"Das Ticket im Zustand nach dem Loeschen"}},"required":["message","id","mode","ticket"],"additionalProperties":false},{"type":"object","properties":{"message":{"type":"string","description":"Deutscher Satz der Form `Ticket <id> endgültig gelöscht`"},"id":{"type":"string","description":"Kennung des geloeschten Tickets, unveraendert aus dem Pfad"},"mode":{"type":"string","const":"hard","description":"Alle personenbezogenen Felder wurden geleert und die Kommentare entfernt. Die ZEILE bleibt — sie haelt die Ticketnummer belegt, damit keine Nummer ein zweites Mal vergeben wird. Deshalb kommt hier auch KEIN `ticket` mit"}},"required":["message","id","mode"],"additionalProperties":false}]},"example":{"message":"string","id":"string","mode":"soft","ticket":{"id":"string","ticketNumber":"string","title":"string","status":"string","priority":"string","category":"string","source":"string"}}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `manager`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle — hier `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Ticket mit dieser Kennung. Beim Soft-Delete auch dann, wenn es bereits geloescht war — beim harten Loeschen dagegen NICHT: ein schon geloeschtes Ticket laesst sich noch bereinigen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"ticket_not_found","description":"Nicht vorhanden, bereits geloescht — oder beim Wiedereroeffnen: nicht mehr auffindbar"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Loeschen fehlgeschlagen (JSON nach dem Schema) — die Transaktion wurde zurueckgerollt. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1TicketsById","tags":["tickets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ticket löschen","description":"Loescht ein Ticket. OHNE `?hard=true` wird nur `deleted_at` gesetzt und ein interner System-Kommentar mit dem mitgegebenen Grund geschrieben; die Zeile bleibt vollstaendig erhalten. MIT `?hard=true` werden alle personenbezogenen Felder geleert und die Kommentare wirklich entfernt — die ZEILE bleibt aber auch dann stehen, weil sie die Ticketnummer belegt haelt. Sonst bekaeme das naechste Ticket dieselbe Nummer, die beim Kunden schon in einer Mail steht. Nur genau `hard=true` zaehlt; jeder andere Wert bedeutet Soft-Delete. Der Rumpf ist freiwillig und darf `reason` tragen; ohne Rumpf gibt es kein 400. Der zweite Soft-Delete auf dasselbe Ticket antwortet ehrlich mit 404 statt mit einem zweiten Erfolg."}},"/api/v1/tickets/{id}/comments":{"post":{"responses":{"201":{"description":"Der angelegte Kommentar","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Kommentars (UUID)"},"ticketId":{"type":"string","description":"Ticket, an dem der Kommentar haengt"},"authorId":{"type":["string","null"],"description":"Benutzerkennung des Verfassers; `null` bei System-Kommentaren ohne Benutzerkontext"},"authorName":{"type":["string","null"],"description":"Name des Verfassers. Bei Schliessen, Wiedereroeffnen und Loeschen steht hier `System`, wenn kein Benutzername vorlag"},"content":{"type":"string","description":"Der Kommentartext"},"isInternal":{"type":"boolean","description":"`true` = nur intern sichtbar. Alle vom Server selbst geschriebenen Kommentare sind intern; wer die Sichtbarkeit durchsetzt, ist die lesende Oberflaeche, nicht diese Route"},"createdAt":{"description":"Anlagezeitpunkt"}},"required":["id","ticketId","authorId","authorName","content","isInternal"],"additionalProperties":false},"example":{"id":"string","ticketId":"string","authorId":"string","authorName":"string","content":"string","isInternal":true}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Ticket mit dieser Kennung, oder es ist bereits geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"ticket_not_found","description":"Nicht vorhanden, bereits geloescht — oder beim Wiedereroeffnen: nicht mehr auffindbar"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Anlegen fehlgeschlagen (JSON nach dem Schema) — die Transaktion wurde zurueckgerollt, es steht also weder ein Kommentar noch ein neuer Aenderungszeitpunkt. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1TicketsByIdComments","tags":["tickets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Kommentar zu einem Ticket hinzufügen","description":"Haengt einen Kommentar an ein Ticket und beruehrt dabei dessen Aenderungszeitpunkt — beides in EINER Transaktion, es gibt also keinen Kommentar an einem unveraenderten Ticket. `isInternal: true` markiert ihn als nur intern sichtbar; wer das durchsetzt, entscheidet die lesende Oberflaeche, nicht diese Route. Urheber ist der angemeldete Benutzer und laesst sich nicht mitgeben. Die Antwort ist der KOMMENTAR, nicht das Ticket.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","minLength":1,"maxLength":10000},"isInternal":{"type":"boolean","default":false}},"required":["content"]},"example":{"content":"string","isInternal":true}}}}}},"/api/v1/tickets/{id}/close":{"post":{"responses":{"200":{"description":"Das geschlossene Ticket","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Tickets (UUID)"},"tenantId":{"description":"Mandant, zu dem das Ticket gehoert"},"ticketNumber":{"type":"string","description":"Fortlaufende Nummer in der Form `TKT-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"type":"string","description":"Betreff des Tickets"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"type":"string","description":"`open`, `in_progress`, `waiting` oder `closed`"},"priority":{"type":"string","description":"`low`, `medium`, `high` oder `critical`"},"category":{"type":"string","description":"`general`, `bug`, `feature`, `billing` oder `access`"},"source":{"type":"string","description":"Woher das Ticket kam: `internal`, `email`, `api` oder `customer_portal`"},"reporterName":{"description":"Name des Melders; `null`, wenn keiner erfasst wurde"},"reporterEmail":{"description":"E-Mail des Melders; `null`, wenn keine erfasst wurde"},"reporterId":{"description":"Benutzerkennung des Melders; `null` bei Meldungen von aussen"},"assigneeId":{"description":"Benutzerkennung des Bearbeiters; `null`, solange niemand zustaendig ist"},"assigneeName":{"description":"Name des Bearbeiters; `null`, solange niemand zustaendig ist"},"relatedEntityType":{"description":"Art eines verknuepften Datensatzes, z. B. `invoice`; `null` ohne Verknuepfung"},"relatedEntityId":{"description":"Kennung des verknuepften Datensatzes; `null` ohne Verknuepfung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Setzt NUR `POST /tickets/{id}/close` — ein Statuswechsel per PATCH laesst das Feld leer"},"resolution":{"description":"Loesungstext; `null`, solange offen"},"createdBy":{"description":"Benutzerkennung des Anlegenden; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","ticketNumber","title","status","priority","category","source"],"additionalProperties":false},"example":{"id":"string","ticketNumber":"string","title":"string","status":"string","priority":"string","category":"string","source":"string"}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis) — etwa ein leerer Loesungstext","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Ticket mit dieser Kennung, oder es ist bereits geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"ticket_not_found","description":"Nicht vorhanden, bereits geloescht — oder beim Wiedereroeffnen: nicht mehr auffindbar"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Schliessen fehlgeschlagen (JSON nach dem Schema) — die Transaktion wurde zurueckgerollt, das Ticket ist also NICHT halb geschlossen. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1TicketsByIdClose","tags":["tickets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ticket schließen","description":"Schliesst ein Ticket: setzt Status, Loesungszeitpunkt und Loesungstext und schreibt einen internen System-Kommentar — alles in EINER Transaktion. Der Loesungstext ist Pflicht. Ein bereits geschlossenes Ticket laesst sich erneut schliessen; Zeitpunkt und Text werden dann ueberschrieben, und es entsteht ein zweiter System-Kommentar. Die Antwort ist das Ticket.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"resolution":{"type":"string","minLength":1,"maxLength":5000}},"required":["resolution"]},"example":{"resolution":"string"}}}}}},"/api/v1/tickets/{id}/reopen":{"post":{"responses":{"200":{"description":"Das wieder geoeffnete Ticket","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Tickets (UUID)"},"tenantId":{"description":"Mandant, zu dem das Ticket gehoert"},"ticketNumber":{"type":"string","description":"Fortlaufende Nummer in der Form `TKT-JJJJ-NNNN`, mandantenweit eindeutig"},"title":{"type":"string","description":"Betreff des Tickets"},"description":{"description":"Ausfuehrliche Beschreibung; `null`, wenn keine erfasst wurde"},"status":{"type":"string","description":"`open`, `in_progress`, `waiting` oder `closed`"},"priority":{"type":"string","description":"`low`, `medium`, `high` oder `critical`"},"category":{"type":"string","description":"`general`, `bug`, `feature`, `billing` oder `access`"},"source":{"type":"string","description":"Woher das Ticket kam: `internal`, `email`, `api` oder `customer_portal`"},"reporterName":{"description":"Name des Melders; `null`, wenn keiner erfasst wurde"},"reporterEmail":{"description":"E-Mail des Melders; `null`, wenn keine erfasst wurde"},"reporterId":{"description":"Benutzerkennung des Melders; `null` bei Meldungen von aussen"},"assigneeId":{"description":"Benutzerkennung des Bearbeiters; `null`, solange niemand zustaendig ist"},"assigneeName":{"description":"Name des Bearbeiters; `null`, solange niemand zustaendig ist"},"relatedEntityType":{"description":"Art eines verknuepften Datensatzes, z. B. `invoice`; `null` ohne Verknuepfung"},"relatedEntityId":{"description":"Kennung des verknuepften Datensatzes; `null` ohne Verknuepfung"},"resolvedAt":{"description":"Zeitpunkt der Loesung; `null`, solange offen. Setzt NUR `POST /tickets/{id}/close` — ein Statuswechsel per PATCH laesst das Feld leer"},"resolution":{"description":"Loesungstext; `null`, solange offen"},"createdBy":{"description":"Benutzerkennung des Anlegenden; `null`, wenn ohne Benutzerkontext angelegt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Zeitpunkt der letzten Aenderung"}},"required":["id","ticketNumber","title","status","priority","category","source"],"additionalProperties":false},"example":{"id":"string","ticketNumber":"string","title":"string","status":"string","priority":"string","category":"string","source":"string"}}}},"401":{"description":"Nicht angemeldet — Klartext, kein JSON"},"404":{"description":"Kein Ticket mit dieser Kennung, oder es ist bereits geloescht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"ticket_not_found","description":"Nicht vorhanden, bereits geloescht — oder beim Wiedereroeffnen: nicht mehr auffindbar"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Wiedereroeffnen fehlgeschlagen (JSON nach dem Schema) — die Transaktion wurde zurueckgerollt. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1TicketsByIdReopen","tags":["tickets"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Geschlossenes Ticket wieder öffnen","description":"Setzt den Status zurueck auf `open` und LOESCHT dabei Loesungszeitpunkt und Loesungstext — die bisherige Loesung ist danach weg und steht nur noch im System-Kommentar der Schliessung. Der Aufruf nimmt keinen Rumpf entgegen und prueft nicht, ob das Ticket ueberhaupt geschlossen war: ein offenes Ticket „wiederzuoeffnen\" ist erlaubt und schreibt trotzdem einen System-Kommentar."}},"/api/v1/notifications":{"get":{"responses":{"200":{"description":"Benachrichtigungen des angemeldeten Nutzers","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"userId":{},"type":{},"title":{},"message":{},"link":{},"readAt":{},"entityType":{},"entityId":{},"createdAt":{},"read":{"type":"boolean"}},"required":["read"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"]},"unreadCount":{"type":"integer"}},"required":["data","pagination","unreadCount"],"additionalProperties":false},"example":{"data":[{"read":true}],"pagination":{"limit":0,"offset":0,"total":0},"unreadCount":0}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Notifications","tags":["notifications"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"unread","schema":{"type":"string","enum":["true","false","1","0","yes","no","on","off"]}},{"in":"query","name":"type","schema":{"type":"string","enum":["info","success","warning","error"]}}],"summary":"Benachrichtigungen des angemeldeten Nutzers auflisten","description":"Liest `notifications` im Mandanten-Schema, gefiltert auf die eigene Benutzerkennung und sortiert nach Anlagezeitpunkt absteigend. `unread=true` zeigt nur Ungelesene, `unread=false` nur Gelesene, `type` grenzt auf info, success, warning oder error ein. `limit` (Vorgabe 20, hoechstens 100) und `offset` blaettern; `pagination.total` zaehlt alle Treffer des Filters, nicht nur die Seite. `unreadCount` zaehlt dagegen IMMER alle ungelesenen Benachrichtigungen des Nutzers, unabhaengig von Filter und Seite — die Glocke im Kopfbereich liest genau dieses Feld. Die Tabelle wird beim ersten Aufruf angelegt, ein neuer Mandant bekommt deshalb eine leere Liste statt eines Fehlers."}},"/api/v1/notifications/read-all":{"post":{"responses":{"200":{"description":"Anzahl der umgestellten Benachrichtigungen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"updated":{"type":"integer","minimum":0,"description":"Wie viele Benachrichtigungen tatsaechlich umgestellt wurden; 0 wenn nichts offen war"}},"required":["ok","updated"],"additionalProperties":false},"example":{"ok":true,"updated":0}}}},"401":{"description":"Unauthorized"}},"operationId":"postApiV1NotificationsRead-all","tags":["notifications"],"parameters":[],"summary":"Alle Benachrichtigungen als gelesen markieren","description":"Setzt `read_at` in EINER Anweisung auf alle noch ungelesenen Benachrichtigungen des aufrufenden Nutzers und meldet in `updated`, wie viele Zeilen das betraf. Bereits gelesene bleiben unberuehrt, ihr urspruenglicher Zeitpunkt geht also nicht verloren. Ist nichts offen, kommt trotzdem 200 mit `updated: 0` — der Aufruf ist gefahrlos wiederholbar. Fremde Benachrichtigungen sind nie betroffen, der Filter laeuft ueber `user_id`. Der Endpunkt nimmt keinen Rumpf entgegen."}},"/api/v1/notifications/{id}/read":{"post":{"responses":{"200":{"description":"Die Benachrichtigung nach dem Markieren","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"userId":{},"type":{},"title":{},"message":{},"link":{},"readAt":{},"entityType":{},"entityId":{},"createdAt":{},"read":{"type":"boolean"}},"required":["read"],"additionalProperties":false},"example":{"read":true}}}},"401":{"description":"Unauthorized"},"404":{"description":"Keine Benachrichtigung dieser Kennung beim aufrufenden Nutzer","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Notification not found"},"code":{"type":"string","const":"NOT_FOUND"}},"required":["error","code"],"additionalProperties":false}}}}},"operationId":"postApiV1NotificationsByIdRead","tags":["notifications"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Benachrichtigung als gelesen markieren","description":"Setzt `read_at` auf den aktuellen Zeitpunkt und gibt die vollstaendige Benachrichtigung im Zustand DANACH zurueck — dieselbe Form wie in der Liste, `read` ist dann true. Das Update greift nur, wenn die Zeile dem aufrufenden Nutzer gehoert; eine fremde oder unbekannte Kennung liefert 404, nicht 403. Ein zweiter Aufruf schadet nicht, verschiebt aber den Zeitstempel: gefiltert wird auf die Kennung, nicht auf „noch ungelesen\"."}},"/api/v1/notifications/{id}":{"delete":{"responses":{"200":{"description":"Geloescht — die Antwort traegt keine Nutzdaten","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"Unauthorized"},"404":{"description":"Keine Benachrichtigung dieser Kennung beim aufrufenden Nutzer","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Notification not found"},"code":{"type":"string","const":"NOT_FOUND"}},"required":["error","code"],"additionalProperties":false}}}}},"operationId":"deleteApiV1NotificationsById","tags":["notifications"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Benachrichtigung endgueltig loeschen","description":"Loescht die Zeile physisch aus `notifications`. Die Tabelle hat keine `deleted_at`-Spalte — es gibt also kein Zurueck und keinen Papierkorb. Vorher wird geprueft, ob die Benachrichtigung dem aufrufenden Nutzer gehoert; eine fremde oder unbekannte Kennung liefert 404, nicht 403. Kopien derselben Nachricht bei anderen Nutzern bleiben unberuehrt, weil jede Zeile genau einem `user_id` gehoert."}},"/api/v1/quotes":{"get":{"responses":{"200":{"description":"Liste der Angebote","content":{"application/json":{"schema":{"type":"object","properties":{"quotes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"quoteNumber":{"type":["string","null"]},"customerName":{"type":["string","null"]},"title":{"type":["string","null"]},"status":{"type":["string","null"]},"total":{"type":"number"},"validUntil":{"type":["string","null"]},"createdAt":{"type":"string"},"chanceId":{"type":["string","null"]},"customFields":{"type":"object","additionalProperties":{}}},"required":["id","quoteNumber","customerName","title","status","total","validUntil","createdAt","chanceId","customFields"],"additionalProperties":false}},"total":{"type":"integer"},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["limit","offset"]},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string"}},"required":["tenantId","source"]}},"required":["quotes","total","pagination","meta"],"additionalProperties":false},"example":{"quotes":[{"id":"string","quoteNumber":"string","customerName":"string","title":"string","status":"string","total":0,"validUntil":"string","createdAt":"string","chanceId":"string","customFields":{}}],"total":0,"pagination":{"limit":0,"offset":0},"meta":{"tenantId":"string","source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Quotes","tags":["quotes"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","sent","accepted","rejected","expired","cancelled"]}},{"in":"query","name":"projectId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"customerId","schema":{"type":"string"}}],"summary":"List quotes","description":"Listet alle Angebote des Mandanten mit Filter und Paginierung. Filter: status, projectId, customerId. Gelöschte Angebote (deleted_at gesetzt) erscheinen nie. Sortierung fest nach Anlagedatum, neueste zuerst."},"post":{"responses":{"201":{"description":"Angebot angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"quoteNumber":{},"customerId":{},"customerName":{},"projectId":{},"chanceId":{},"title":{},"status":{},"subtotal":{"type":"number"},"taxAmount":{"type":"number"},"total":{"type":"number"},"discount":{"type":["number","null"]},"validUntil":{},"paymentTermsDays":{"type":["number","null"]},"skontoDays":{"type":["number","null"]},"skontoPercent":{"type":["number","null"]},"notes":{},"customFields":{"type":"object","additionalProperties":{}},"introText":{"type":["string","null"]},"footerText":{"type":["string","null"]},"taxNote":{},"priceMode":{"type":"string"},"salutation":{"type":["string","null"]},"recipientName":{"type":["string","null"]},"recipientCompany":{"type":["string","null"]},"recipientStreet":{"type":["string","null"]},"recipientZip":{"type":["string","null"]},"recipientCity":{"type":["string","null"]},"recipientCountry":{"type":["string","null"]},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"leistungszeitraumVon":{"type":["string","null"]},"leistungszeitraumBis":{"type":["string","null"]},"lieferdatum":{"type":["string","null"]},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"]},"language":{"type":["string","null"]},"sourceDocumentId":{},"sourceDocumentType":{},"convertedToOrderId":{},"positions":{"type":"array","items":{}},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","taxAmount","total","discount","paymentTermsDays","skontoDays","skontoPercent","customFields","introText","footerText","priceMode","salutation","recipientName","recipientCompany","recipientStreet","recipientZip","recipientCity","recipientCountry","deliveryName","deliveryCompany","deliveryStreet","deliveryZip","deliveryCity","deliveryCountry","leistungszeitraumVon","leistungszeitraumBis","lieferdatum","leistungsTyp","leistungsdatum","language","positions"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"taxAmount":0,"total":0,"discount":0,"paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"customFields":{},"introText":"string","footerText":"string","priceMode":"string","salutation":"string","recipientName":"string","recipientCompany":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","leistungszeitraumVon":"string","leistungszeitraumBis":"string","lieferdatum":"string","leistungsTyp":"leistungsdatum","leistungsdatum":"string","language":"string","positions":[]}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"}},"operationId":"postApiV1Quotes","tags":["quotes"],"parameters":[],"summary":"Create quote","description":"Legt ein neues Angebot mit Positionen an. Der Status ist immer `draft` — er lässt sich beim Anlegen nicht setzen. Die Belegnummer vergibt der Nummernkreis, die Summen rechnet der Server aus den Positionen (mitgeschickte Summen zählen nicht). Eine verletzte Geschäftsregel wird mit 422 (`entity_rule_violation`) abgelehnt, bevor irgendetwas geschrieben wird.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerName":{"type":"string","minLength":1,"maxLength":255},"customerId":{"type":"string"},"projectId":{"type":["string","null"],"format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":255,"default":""},"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string","default":""},"quantity":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"discount":{"type":"number","minimum":0,"maximum":100},"lineType":{"type":"string","enum":["standard","section","note"],"default":"standard"},"optional":{"type":"boolean","default":false},"isAlternative":{"type":"boolean","default":false},"category":{"type":"string"},"sortOrder":{"type":"number"},"articleId":{"type":["string","null"],"format":"uuid"}},"required":["quantity","unitPrice"]},"maxItems":1000,"default":[]},"validUntil":{"type":"string","format":"date"},"paymentTermsDays":{"type":["integer","null"],"minimum":0,"maximum":365},"skontoDays":{"type":["integer","null"],"minimum":0,"maximum":365},"skontoPercent":{"type":["number","null"],"minimum":0,"maximum":100},"notes":{"type":"string"},"introText":{"type":"string"},"footerText":{"type":"string"},"priceMode":{"type":"string","enum":["net","gross"],"default":"net"},"salutation":{"type":"string"},"recipientName":{"type":"string"},"recipientCompany":{"type":"string"},"recipientStreet":{"type":"string"},"recipientZip":{"type":"string"},"recipientCity":{"type":"string"},"recipientCountry":{"type":"string"},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"leistungszeitraumVon":{"type":["string","null"],"format":"date"},"leistungszeitraumBis":{"type":["string","null"],"format":"date"},"lieferdatum":{"type":["string","null"],"format":"date"},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"],"format":"date"},"language":{"type":"string","enum":["de","en","fr","es","pl","nl","da","cs","zh"]},"customFields":{"type":"object","additionalProperties":{}}},"required":["customerName","title"]},"example":{"customerName":"string","customerId":"string","projectId":"00000000-0000-4000-8000-000000000000","title":"string","positions":[{"title":"string","description":"string","quantity":0,"unit":"string","unitPrice":0,"taxRate":0,"discount":0,"lineType":"standard","optional":true,"isAlternative":true,"category":"string","sortOrder":0,"articleId":"00000000-0000-4000-8000-000000000000"}],"validUntil":"2026-01-01","paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"notes":"string","introText":"string","footerText":"string","priceMode":"net","salutation":"string","recipientName":"string","recipientCompany":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","leistungszeitraumVon":"2026-01-01","leistungszeitraumBis":"2026-01-01","lieferdatum":"2026-01-01","leistungsTyp":"leistungsdatum","leistungsdatum":"2026-01-01","language":"de","customFields":{}}}}}}},"/api/v1/quotes/stats":{"get":{"responses":{"200":{"description":"Angebots-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer","description":"Angebote gesamt"},"draft":{"type":"integer","description":"Entwuerfe"},"sent":{"type":"integer","description":"Versendet"},"accepted":{"type":"integer","description":"Angenommen"},"rejected":{"type":"integer","description":"Abgelehnt"},"expired":{"type":"integer","description":"Abgelaufen"},"totalValue":{"type":"number","description":"Gesamtwert aller Angebote"},"conversionRate":{"type":"number","description":"Anteil angenommener Angebote"},"meta":{"type":"object","additionalProperties":{},"description":"Angaben zur Abfrage"}},"required":["total","draft","sent","accepted","rejected","expired","totalValue","conversionRate","meta"]},"example":{"total":0,"draft":0,"sent":0,"accepted":0,"rejected":0,"expired":0,"totalValue":0,"conversionRate":0,"meta":{}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1QuotesStats","tags":["quotes"],"parameters":[],"summary":"Get quote statistics","description":"Liefert Kennzahlen zu Angeboten (Anzahl je Status, Annahmequote, Gesamtwert). Gelöschte Angebote zählen nicht mit; der Gesamtwert lässt abgelehnte Angebote aus."}},"/api/v1/quotes/{id}":{"get":{"responses":{"200":{"description":"Angebots-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"quoteNumber":{},"customerId":{},"customerName":{},"projectId":{},"chanceId":{},"title":{},"status":{},"subtotal":{"type":"number"},"taxAmount":{"type":"number"},"total":{"type":"number"},"discount":{"type":["number","null"]},"validUntil":{},"paymentTermsDays":{"type":["number","null"]},"skontoDays":{"type":["number","null"]},"skontoPercent":{"type":["number","null"]},"notes":{},"customFields":{"type":"object","additionalProperties":{}},"introText":{"type":["string","null"]},"footerText":{"type":["string","null"]},"taxNote":{},"priceMode":{"type":"string"},"salutation":{"type":["string","null"]},"recipientName":{"type":["string","null"]},"recipientCompany":{"type":["string","null"]},"recipientStreet":{"type":["string","null"]},"recipientZip":{"type":["string","null"]},"recipientCity":{"type":["string","null"]},"recipientCountry":{"type":["string","null"]},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"leistungszeitraumVon":{"type":["string","null"]},"leistungszeitraumBis":{"type":["string","null"]},"lieferdatum":{"type":["string","null"]},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"]},"language":{"type":["string","null"]},"sourceDocumentId":{},"sourceDocumentType":{},"convertedToOrderId":{},"positions":{"type":"array","items":{}},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","taxAmount","total","discount","paymentTermsDays","skontoDays","skontoPercent","customFields","introText","footerText","priceMode","salutation","recipientName","recipientCompany","recipientStreet","recipientZip","recipientCity","recipientCountry","deliveryName","deliveryCompany","deliveryStreet","deliveryZip","deliveryCity","deliveryCountry","leistungszeitraumVon","leistungszeitraumBis","lieferdatum","leistungsTyp","leistungsdatum","language","positions"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"taxAmount":0,"total":0,"discount":0,"paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"customFields":{},"introText":"string","footerText":"string","priceMode":"string","salutation":"string","recipientName":"string","recipientCompany":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","leistungszeitraumVon":"string","leistungszeitraumBis":"string","lieferdatum":"string","leistungsTyp":"leistungsdatum","leistungsdatum":"string","language":"string","positions":[]}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Angebot nicht gefunden"}},"operationId":"getApiV1QuotesById","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get quote","description":"Liefert ein einzelnes Angebot inklusive Positionen. Eine Id, die keine UUID ist, und ein gelöschtes Angebot liefern beide 404."},"put":{"responses":{"200":{"description":"Angebot aktualisiert — vollständiger Angebots-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"quoteNumber":{},"customerId":{},"customerName":{},"projectId":{},"chanceId":{},"title":{},"status":{},"subtotal":{"type":"number"},"taxAmount":{"type":"number"},"total":{"type":"number"},"discount":{"type":["number","null"]},"validUntil":{},"paymentTermsDays":{"type":["number","null"]},"skontoDays":{"type":["number","null"]},"skontoPercent":{"type":["number","null"]},"notes":{},"customFields":{"type":"object","additionalProperties":{}},"introText":{"type":["string","null"]},"footerText":{"type":["string","null"]},"taxNote":{},"priceMode":{"type":"string"},"salutation":{"type":["string","null"]},"recipientName":{"type":["string","null"]},"recipientCompany":{"type":["string","null"]},"recipientStreet":{"type":["string","null"]},"recipientZip":{"type":["string","null"]},"recipientCity":{"type":["string","null"]},"recipientCountry":{"type":["string","null"]},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"leistungszeitraumVon":{"type":["string","null"]},"leistungszeitraumBis":{"type":["string","null"]},"lieferdatum":{"type":["string","null"]},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"]},"language":{"type":["string","null"]},"sourceDocumentId":{},"sourceDocumentType":{},"convertedToOrderId":{},"positions":{"type":"array","items":{}},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","taxAmount","total","discount","paymentTermsDays","skontoDays","skontoPercent","customFields","introText","footerText","priceMode","salutation","recipientName","recipientCompany","recipientStreet","recipientZip","recipientCity","recipientCountry","deliveryName","deliveryCompany","deliveryStreet","deliveryZip","deliveryCity","deliveryCountry","leistungszeitraumVon","leistungszeitraumBis","lieferdatum","leistungsTyp","leistungsdatum","language","positions"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"taxAmount":0,"total":0,"discount":0,"paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"customFields":{},"introText":"string","footerText":"string","priceMode":"string","salutation":"string","recipientName":"string","recipientCompany":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","leistungszeitraumVon":"string","leistungszeitraumBis":"string","lieferdatum":"string","leistungsTyp":"leistungsdatum","leistungsdatum":"string","language":"string","positions":[]}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"},"409":{"description":"`quote_locked` — Endzustand accepted/converted/cancelled (GoBD-Sperre)"},"422":{"description":"Verstoß gegen eine Geschäftsregel (entity_rule_violation)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1QuotesById","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace quote","description":"Ersetzt Inhalt und Positionen eines Angebots vollständig; die Summen werden aus den neuen Positionen neu gerechnet, ein gesetzter Beleg-Rabatt bleibt erhalten. GoBD-Sperre: ein angenommenes, umgewandeltes oder storniertes Angebot (accepted/converted/cancelled) wird mit 409 `quote_locked` abgelehnt — vor einem PUT also den Status prüfen. Ist das Angebot bereits versendet (`sent`), schreibt der Server vor der Änderung einen Versions-Snapshot.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerName":{"type":"string","minLength":1,"maxLength":255},"customerId":{"type":"string"},"projectId":{"type":["string","null"],"format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":255,"default":""},"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string","default":""},"quantity":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"discount":{"type":"number","minimum":0,"maximum":100},"lineType":{"type":"string","enum":["standard","section","note"],"default":"standard"},"optional":{"type":"boolean","default":false},"isAlternative":{"type":"boolean","default":false},"category":{"type":"string"},"sortOrder":{"type":"number"},"articleId":{"type":["string","null"],"format":"uuid"}},"required":["quantity","unitPrice"]},"maxItems":1000,"default":[]},"validUntil":{"type":"string","format":"date"},"paymentTermsDays":{"type":["integer","null"],"minimum":0,"maximum":365},"skontoDays":{"type":["integer","null"],"minimum":0,"maximum":365},"skontoPercent":{"type":["number","null"],"minimum":0,"maximum":100},"notes":{"type":"string"},"introText":{"type":"string"},"footerText":{"type":"string"},"priceMode":{"type":"string","enum":["net","gross"],"default":"net"},"salutation":{"type":"string"},"recipientName":{"type":"string"},"recipientCompany":{"type":"string"},"recipientStreet":{"type":"string"},"recipientZip":{"type":"string"},"recipientCity":{"type":"string"},"recipientCountry":{"type":"string"},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"leistungszeitraumVon":{"type":["string","null"],"format":"date"},"leistungszeitraumBis":{"type":["string","null"],"format":"date"},"lieferdatum":{"type":["string","null"],"format":"date"},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"],"format":"date"},"language":{"type":"string","enum":["de","en","fr","es","pl","nl","da","cs","zh"]},"customFields":{"type":"object","additionalProperties":{}}},"required":["customerName","title"]},"example":{"customerName":"string","customerId":"string","projectId":"00000000-0000-4000-8000-000000000000","title":"string","positions":[{"title":"string","description":"string","quantity":0,"unit":"string","unitPrice":0,"taxRate":0,"discount":0,"lineType":"standard","optional":true,"isAlternative":true,"category":"string","sortOrder":0,"articleId":"00000000-0000-4000-8000-000000000000"}],"validUntil":"2026-01-01","paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"notes":"string","introText":"string","footerText":"string","priceMode":"net","salutation":"string","recipientName":"string","recipientCompany":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","leistungszeitraumVon":"2026-01-01","leistungszeitraumBis":"2026-01-01","lieferdatum":"2026-01-01","leistungsTyp":"leistungsdatum","leistungsdatum":"2026-01-01","language":"de","customFields":{}}}}}},"patch":{"responses":{"200":{"description":"Angebot aktualisiert — vollständiger Angebots-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"quoteNumber":{},"customerId":{},"customerName":{},"projectId":{},"chanceId":{},"title":{},"status":{},"subtotal":{"type":"number"},"taxAmount":{"type":"number"},"total":{"type":"number"},"discount":{"type":["number","null"]},"validUntil":{},"paymentTermsDays":{"type":["number","null"]},"skontoDays":{"type":["number","null"]},"skontoPercent":{"type":["number","null"]},"notes":{},"customFields":{"type":"object","additionalProperties":{}},"introText":{"type":["string","null"]},"footerText":{"type":["string","null"]},"taxNote":{},"priceMode":{"type":"string"},"salutation":{"type":["string","null"]},"recipientName":{"type":["string","null"]},"recipientCompany":{"type":["string","null"]},"recipientStreet":{"type":["string","null"]},"recipientZip":{"type":["string","null"]},"recipientCity":{"type":["string","null"]},"recipientCountry":{"type":["string","null"]},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"leistungszeitraumVon":{"type":["string","null"]},"leistungszeitraumBis":{"type":["string","null"]},"lieferdatum":{"type":["string","null"]},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"]},"language":{"type":["string","null"]},"sourceDocumentId":{},"sourceDocumentType":{},"convertedToOrderId":{},"positions":{"type":"array","items":{}},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","taxAmount","total","discount","paymentTermsDays","skontoDays","skontoPercent","customFields","introText","footerText","priceMode","salutation","recipientName","recipientCompany","recipientStreet","recipientZip","recipientCity","recipientCountry","deliveryName","deliveryCompany","deliveryStreet","deliveryZip","deliveryCity","deliveryCountry","leistungszeitraumVon","leistungszeitraumBis","lieferdatum","leistungsTyp","leistungsdatum","language","positions"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"taxAmount":0,"total":0,"discount":0,"paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"customFields":{},"introText":"string","footerText":"string","priceMode":"string","salutation":"string","recipientName":"string","recipientCompany":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","leistungszeitraumVon":"string","leistungszeitraumBis":"string","lieferdatum":"string","leistungsTyp":"leistungsdatum","leistungsdatum":"string","language":"string","positions":[]}}}},"400":{"description":"Validierungsfehler oder keine änderbaren Felder im Body (no_fields_to_update)"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"},"409":{"description":"Ungültiger Status-Übergang, Angebot gesperrt (Endzustand) oder dokument-sichtbares Eigenes Feld bereits ausgegeben (GoBD)"},"422":{"description":"Verstoß gegen eine Geschäftsregel (entity_rule_violation)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"quotes.update","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update quote","description":"Ändert einzelne Felder eines Angebots (status, validUntil, notes, title, discount, customFields). Nicht mitgeschickte Felder bleiben unverändert; bei `customFields` gilt: fehlender Schlüssel = behalten, `null` = löschen, Wert = setzen. `discount` ist der Beleg-Rabatt in Prozent und rechnet die Summen neu. GoBD-Sperren: unzulässiger Status-Übergang, gesperrter Endstatus (accepted/converted/cancelled) und die Änderung eines dokument-sichtbaren Eigenen Feldes an einem bereits ausgegebenen Angebot enden alle in 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired","converted","cancelled"]},"validUntil":{"type":"string","format":"date"},"notes":{"type":"string"},"title":{"type":"string","minLength":1,"maxLength":255},"discount":{"type":"number","minimum":0,"maximum":100},"customFields":{"type":"object","additionalProperties":{}}}},"example":{"status":"draft","validUntil":"2026-01-01","notes":"string","title":"string","discount":0,"customFields":{}}}}}},"delete":{"responses":{"200":{"description":"Angebot gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn die Aktion ausgefuehrt wurde"}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"}},"operationId":"deleteApiV1QuotesById","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Soft-delete quote","description":"Setzt einen Löschstempel (`deleted_at`) — der Datensatz bleibt bestehen und lässt sich über POST /quotes/{id}/restore zurückholen. Aus Listen, Kennzahlen und Einzelabruf verschwindet das Angebot sofort. Löschbar sind nur Angebote im Status draft, rejected oder expired; alles andere endet in 409 `quote_not_deletable`."}},"/api/v1/quotes/{id}/positions":{"patch":{"responses":{"200":{"description":"Positionen aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"position_count":{"type":"integer","description":"Positionen nach der Aenderung"}},"required":["position_count"]},"example":{"position_count":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"},"409":{"description":"Angebot gesperrt (Endzustand)"}},"operationId":"patchApiV1QuotesByIdPositions","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace quote positions","description":"Ersetzt NUR die Positionsliste eines Angebots; Kunde, Texte und Adressen bleiben unangetastet, die Summen werden neu gerechnet. Die Liste wird ersetzt, nicht ergänzt. Dieselbe GoBD-Sperre wie beim PUT: accepted/converted/cancelled → 409 `quote_locked`. Bei `sent` entsteht vorher ein Versions-Snapshot (abschaltbar über `create_new_version_if_sent: false`).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"positions":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string","default":""},"quantity":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0},"taxRate":{"type":"number","minimum":0,"maximum":100,"default":19},"discount":{"type":"number","minimum":0,"maximum":100},"lineType":{"type":"string","enum":["standard","section","note"],"default":"standard"},"optional":{"type":"boolean","default":false},"isAlternative":{"type":"boolean","default":false},"category":{"type":"string"},"sortOrder":{"type":"number"},"articleId":{"type":["string","null"],"format":"uuid"}},"required":["quantity","unitPrice"]},"maxItems":1000,"default":[]},"create_new_version_if_sent":{"type":"boolean","default":true}}},"example":{"positions":[{"title":"string","description":"string","quantity":0,"unit":"string","unitPrice":0,"taxRate":0,"discount":0,"lineType":"standard","optional":true,"isAlternative":true,"category":"string","sortOrder":0,"articleId":"00000000-0000-4000-8000-000000000000"}],"create_new_version_if_sent":true}}}}}},"/api/v1/quotes/{id}/versions":{"get":{"responses":{"200":{"description":"Liste der Versionen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Versionen"}},"required":["data"]},"example":{"data":[{}]}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1QuotesByIdVersions","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"List quote versions","description":"Listet alle Versions-Snapshots eines Angebots (alt → neu)"},"post":{"responses":{"201":{"description":"Version erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"version":{"type":"integer","description":"Nummer der neuen Version"}},"required":["id","version"]},"example":{"id":"string","version":0}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"}},"operationId":"postApiV1QuotesByIdVersions","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Create quote version snapshot","description":"Erstellt manuell einen Versions-Snapshot des aktuellen Angebots — unabhängig vom Status. Das Angebot selbst ändert sich dabei nicht."}},"/api/v1/quotes/{id}/versions/{version}":{"get":{"responses":{"200":{"description":"Versions-Snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"version":{"type":"integer","description":"Fortlaufende Versionsnummer"},"snapshot":{"type":"object","additionalProperties":{},"description":"Der Stand zu diesem Zeitpunkt"},"createdAt":{"type":"string"},"createdBy":{"type":["string","null"]}},"required":["id","version","snapshot","createdAt","createdBy"]},"example":{"id":"string","version":0,"snapshot":{},"createdAt":"string","createdBy":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Version nicht gefunden"}},"operationId":"getApiV1QuotesByIdVersionsByVersion","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"version","required":true}],"summary":"Get quote version","description":"Liefert einen einzelnen Versions-Snapshot eines Angebots. `version` ist die fortlaufende Nummer ab 1; alles andere liefert 400 `invalid_version`."}},"/api/v1/quotes/{id}/versions/{version}/restore":{"post":{"responses":{"200":{"description":"Version wiederhergestellt — vollständiger Angebots-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"quoteNumber":{},"customerId":{},"customerName":{},"projectId":{},"chanceId":{},"title":{},"status":{},"subtotal":{"type":"number"},"taxAmount":{"type":"number"},"total":{"type":"number"},"discount":{"type":["number","null"]},"validUntil":{},"paymentTermsDays":{"type":["number","null"]},"skontoDays":{"type":["number","null"]},"skontoPercent":{"type":["number","null"]},"notes":{},"customFields":{"type":"object","additionalProperties":{}},"introText":{"type":["string","null"]},"footerText":{"type":["string","null"]},"taxNote":{},"priceMode":{"type":"string"},"salutation":{"type":["string","null"]},"recipientName":{"type":["string","null"]},"recipientCompany":{"type":["string","null"]},"recipientStreet":{"type":["string","null"]},"recipientZip":{"type":["string","null"]},"recipientCity":{"type":["string","null"]},"recipientCountry":{"type":["string","null"]},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"leistungszeitraumVon":{"type":["string","null"]},"leistungszeitraumBis":{"type":["string","null"]},"lieferdatum":{"type":["string","null"]},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"]},"language":{"type":["string","null"]},"sourceDocumentId":{},"sourceDocumentType":{},"convertedToOrderId":{},"positions":{"type":"array","items":{}},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","taxAmount","total","discount","paymentTermsDays","skontoDays","skontoPercent","customFields","introText","footerText","priceMode","salutation","recipientName","recipientCompany","recipientStreet","recipientZip","recipientCity","recipientCountry","deliveryName","deliveryCompany","deliveryStreet","deliveryZip","deliveryCity","deliveryCountry","leistungszeitraumVon","leistungszeitraumBis","lieferdatum","leistungsTyp","leistungsdatum","language","positions"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"taxAmount":0,"total":0,"discount":0,"paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"customFields":{},"introText":"string","footerText":"string","priceMode":"string","salutation":"string","recipientName":"string","recipientCompany":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","leistungszeitraumVon":"string","leistungszeitraumBis":"string","lieferdatum":"string","leistungsTyp":"leistungsdatum","leistungsdatum":"string","language":"string","positions":[]}}}},"400":{"description":"`invalid_version` — keine Zahl oder kleiner als 1"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"`version_not_found` oder `quote_not_found`"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1QuotesByIdVersionsByVersionRestore","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"version","required":true}],"summary":"Restore quote version","description":"Stellt einen Versions-Snapshot wieder her und sichert vorher den aktuellen Stand als neue Version (nichts geht verloren). Zurückgeschrieben werden Kunde, Titel, Positionen, Summen, Gültigkeit und Notizen — NICHT der Status. Antwortet mit 200, nicht mit 201."}},"/api/v1/quotes/{id}/status":{"patch":{"responses":{"200":{"description":"Status aktualisiert — vollständiger Angebots-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"quoteNumber":{},"customerId":{},"customerName":{},"projectId":{},"chanceId":{},"title":{},"status":{},"subtotal":{"type":"number"},"taxAmount":{"type":"number"},"total":{"type":"number"},"discount":{"type":["number","null"]},"validUntil":{},"paymentTermsDays":{"type":["number","null"]},"skontoDays":{"type":["number","null"]},"skontoPercent":{"type":["number","null"]},"notes":{},"customFields":{"type":"object","additionalProperties":{}},"introText":{"type":["string","null"]},"footerText":{"type":["string","null"]},"taxNote":{},"priceMode":{"type":"string"},"salutation":{"type":["string","null"]},"recipientName":{"type":["string","null"]},"recipientCompany":{"type":["string","null"]},"recipientStreet":{"type":["string","null"]},"recipientZip":{"type":["string","null"]},"recipientCity":{"type":["string","null"]},"recipientCountry":{"type":["string","null"]},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"leistungszeitraumVon":{"type":["string","null"]},"leistungszeitraumBis":{"type":["string","null"]},"lieferdatum":{"type":["string","null"]},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"]},"language":{"type":["string","null"]},"sourceDocumentId":{},"sourceDocumentType":{},"convertedToOrderId":{},"positions":{"type":"array","items":{}},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","taxAmount","total","discount","paymentTermsDays","skontoDays","skontoPercent","customFields","introText","footerText","priceMode","salutation","recipientName","recipientCompany","recipientStreet","recipientZip","recipientCity","recipientCountry","deliveryName","deliveryCompany","deliveryStreet","deliveryZip","deliveryCity","deliveryCountry","leistungszeitraumVon","leistungszeitraumBis","lieferdatum","leistungsTyp","leistungsdatum","language","positions"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"taxAmount":0,"total":0,"discount":0,"paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"customFields":{},"introText":"string","footerText":"string","priceMode":"string","salutation":"string","recipientName":"string","recipientCompany":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","leistungszeitraumVon":"string","leistungszeitraumBis":"string","lieferdatum":"string","leistungsTyp":"leistungsdatum","leistungsdatum":"string","language":"string","positions":[]}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"},"409":{"description":"Ungültiger Status-Übergang (Transition-Matrix, GoBD)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"quotes.updateStatus","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Change quote status","description":"Ändert den Status eines Angebots entlang der erlaubten Übergänge. draft → sent/accepted/rejected/expired/cancelled, sent → accepted/rejected/expired/cancelled, accepted → converted/rejected/expired/cancelled. converted, rejected, expired und cancelled sind Endzustände: von dort führt kein Übergang mehr weg (GoBD — sonst liesse sich ein Angebot ein zweites Mal umwandeln). Ein unzulässiger Übergang wird mit 409 `invalid_status_transition` abgelehnt und nennt die erlaubten Ziele.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","sent","accepted","rejected","expired","cancelled"]}},"required":["status"]},"example":{"status":"draft"}}}}}},"/api/v1/quotes/{id}/convert":{"post":{"responses":{"201":{"description":"Auftrag aus Angebot erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"quoteId":{"type":"string","description":"Das umgewandelte Angebot"},"order":{"type":"object","additionalProperties":{},"description":"Der erzeugte Auftrag"},"quote":{"type":"object","additionalProperties":{},"description":"Das Angebot, falls schon umgewandelt"},"message":{"type":"string","description":"Hinweis, falls nichts Neues entstand"}},"required":["ok","quoteId"]},"example":{"ok":true,"quoteId":"string","order":{},"quote":{},"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"}},"operationId":"postApiV1QuotesByIdConvert","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mark quote as accepted (no order is created)","description":"ACHTUNG — trotz des Pfadnamens entsteht hier KEIN Auftrag. Der Handler setzt das Angebot lediglich auf `accepted` und meldet „Auftrag kann jetzt erstellt werden\". Wer wirklich einen Auftrag will, nimmt POST /quotes/{id}/convert-to-order. Antwortet mit 200 (nicht 201) und liefert das Angebot, keinen Auftrag. Ein abgelehntes oder abgelaufenes Angebot wird mit 409 `quote_not_convertible` abgelehnt."}},"/api/v1/quotes/{id}/convert-to-order":{"post":{"responses":{"201":{"description":"Auftrag aus Angebot erstellt — `orderId` ist zugesagt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"quoteId":{"type":"string","description":"Das umgewandelte Angebot"},"orderId":{"type":"string","minLength":1,"description":"Der erzeugte Auftrag — zugesagt, nicht optional"},"order":{"type":"object","additionalProperties":{},"description":"Die vollstaendige Auftragszeile"}},"required":["ok","quoteId","orderId","order"]},"example":{"ok":true,"quoteId":"string","orderId":"string","order":{}}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"},"409":{"description":"Status erlaubt keine Umwandlung (bereits umgewandelt/abgelehnt/abgelaufen/storniert)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"quotes.convertToOrder","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert quote to order","description":"Legt aus dem Angebot einen Auftrag an (Status `confirmed`, eigene Nummer aus dem Auftrags-Nummernkreis) und setzt das Angebot auf den Endzustand `converted`. Beide Belege werden verknüpft (orders.source_document_id ↔ quotes.converted_to_order_id); Positionen, Summen, Netto-/Brutto-Modus, Steuerhinweis, Einleitungs-/Schlusstext und Beleg-Rabatt wandern mit. Ein zweiter Aufruf legt keinen zweiten Auftrag an: bereits umgewandelte, abgelehnte, abgelaufene und stornierte Angebote werden mit 409 `quote_not_convertible` abgewiesen (gegen eine gesperrte Zeile geprüft, also auch bei zwei gleichzeitigen Aufrufen)."}},"/api/v1/quotes/{id}/convert/order":{"post":{"responses":{"201":{"description":"Auftrag aus Angebot erstellt — `orderId` ist zugesagt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"quoteId":{"type":"string","description":"Das umgewandelte Angebot"},"orderId":{"type":"string","minLength":1,"description":"Der erzeugte Auftrag — zugesagt, nicht optional"},"order":{"type":"object","additionalProperties":{},"description":"Die vollstaendige Auftragszeile"}},"required":["ok","quoteId","orderId","order"]},"example":{"ok":true,"quoteId":"string","orderId":"string","order":{}}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"},"409":{"description":"Status erlaubt keine Umwandlung"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1QuotesByIdConvertOrder","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert quote to order (legacy alias)","description":"Alter Pfad für POST /quotes/{id}/convert-to-order — identischer Handler, identisches Verhalten. Für neue Integrationen den kebab-case-Pfad verwenden."}},"/api/v1/quotes/{id}/duplicate":{"post":{"responses":{"201":{"description":"Angebot dupliziert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"quoteNumber":{},"customerId":{},"customerName":{},"projectId":{},"chanceId":{},"title":{},"status":{},"subtotal":{"type":"number"},"taxAmount":{"type":"number"},"total":{"type":"number"},"discount":{"type":["number","null"]},"validUntil":{},"paymentTermsDays":{"type":["number","null"]},"skontoDays":{"type":["number","null"]},"skontoPercent":{"type":["number","null"]},"notes":{},"customFields":{"type":"object","additionalProperties":{}},"introText":{"type":["string","null"]},"footerText":{"type":["string","null"]},"taxNote":{},"priceMode":{"type":"string"},"salutation":{"type":["string","null"]},"recipientName":{"type":["string","null"]},"recipientCompany":{"type":["string","null"]},"recipientStreet":{"type":["string","null"]},"recipientZip":{"type":["string","null"]},"recipientCity":{"type":["string","null"]},"recipientCountry":{"type":["string","null"]},"deliveryName":{"type":["string","null"]},"deliveryCompany":{"type":["string","null"]},"deliveryStreet":{"type":["string","null"]},"deliveryZip":{"type":["string","null"]},"deliveryCity":{"type":["string","null"]},"deliveryCountry":{"type":["string","null"]},"leistungszeitraumVon":{"type":["string","null"]},"leistungszeitraumBis":{"type":["string","null"]},"lieferdatum":{"type":["string","null"]},"leistungsTyp":{"type":"string","enum":["leistungsdatum","leistungszeitraum","keine"]},"leistungsdatum":{"type":["string","null"]},"language":{"type":["string","null"]},"sourceDocumentId":{},"sourceDocumentType":{},"convertedToOrderId":{},"positions":{"type":"array","items":{}},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","taxAmount","total","discount","paymentTermsDays","skontoDays","skontoPercent","customFields","introText","footerText","priceMode","salutation","recipientName","recipientCompany","recipientStreet","recipientZip","recipientCity","recipientCountry","deliveryName","deliveryCompany","deliveryStreet","deliveryZip","deliveryCity","deliveryCountry","leistungszeitraumVon","leistungszeitraumBis","lieferdatum","leistungsTyp","leistungsdatum","language","positions"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"taxAmount":0,"total":0,"discount":0,"paymentTermsDays":0,"skontoDays":0,"skontoPercent":0,"customFields":{},"introText":"string","footerText":"string","priceMode":"string","salutation":"string","recipientName":"string","recipientCompany":"string","recipientStreet":"string","recipientZip":"string","recipientCity":"string","recipientCountry":"string","deliveryName":"string","deliveryCompany":"string","deliveryStreet":"string","deliveryZip":"string","deliveryCity":"string","deliveryCountry":"string","leistungszeitraumVon":"string","leistungszeitraumBis":"string","lieferdatum":"string","leistungsTyp":"leistungsdatum","leistungsdatum":"string","language":"string","positions":[]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"}},"operationId":"postApiV1QuotesByIdDuplicate","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Duplicate quote","description":"Kopiert ein Angebot als neuen Entwurf mit eigener Belegnummer. Positionen, Summen, Texte, Empfänger- und Lieferadresse werden 1:1 übernommen; es entsteht KEINE Verknüpfung zur Quelle (anders als beim Umwandeln). Die Quelle bleibt unverändert."}},"/api/v1/quotes/{id}/restore":{"post":{"responses":{"200":{"description":"Angebot wiederhergestellt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"id":{"type":"string"}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden oder nicht gelöscht"}},"operationId":"postApiV1QuotesByIdRestore","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Restore deleted quote","description":"Nimmt den Löschstempel zurück, das Angebot erscheint wieder in Listen und Kennzahlen. Ein Angebot, das nie gelöscht wurde, liefert 404 — der Aufruf kann also nichts wiederbeleben, was noch lebt."}},"/api/v1/quotes/{id}/pdf":{"get":{"responses":{"200":{"description":"PDF-Datei des Angebots (Cache-Treffer oder frisch gerendert)","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}},"headers":{"Content-Disposition":{"description":"attachment; filename=\"Angebot-<quote_number>.pdf\" — auf beiden Codepfaden gesetzt.","schema":{"type":"string"},"required":true},"X-PDF-Engine":{"description":"Render-Herkunft: 'cache' bei Cache-Treffer, sonst die Engine aus generateAngebotPdfDetailed.","schema":{"type":"string","enum":["cache","react-pdf","custom","fallback"]},"required":true},"X-PDF-Cache":{"description":"Cache-Status: Speicher-Tier (cached.source) bei Treffer, sonst 'miss'.","schema":{"type":"string","enum":["memory","s3","r2","local","miss"]},"required":true},"X-PDF-CustomFields":{"description":"Anzahl dokument-sichtbarer Eigener Felder im PDF (Zahl als Text). Nur beim frischen Render gesetzt, fehlt bei Cache-Treffer.","schema":{"type":"string"}},"ETag":{"description":"Cache-Schlüssel für If-None-Match. Bei Cache-Treffer immer gesetzt; beim frischen Render nur, wenn X-PDF-Engine ungleich \"fallback\" ist (ein Fallback-Render soll nicht per 304 einzementiert werden).","schema":{"type":"string"}}}},"304":{"description":"Not modified — ETag matches","headers":{"ETag":{"description":"Der unveränderte Cache-Schlüssel aus dem Request.","schema":{"type":"string"},"required":true}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Angebot nicht gefunden"},"500":{"description":"PDF-Erstellung fehlgeschlagen (pdf_generation_failed) — echter Renderfehler, Wiederholen hilft nicht"},"503":{"description":"Datenbank nicht erreichbar — voruebergehend, Wiederholen sinnvoll"}},"operationId":"quotes.pdf","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Download quote PDF","description":"Liefert das Angebot als PDF-Datei — rohe Bytes mit `Content-Type: application/pdf`, KEINE JSON-Antwort. Ergebnisse werden gecacht; `If-None-Match` mit dem ETag beantwortet der Server mit 304. Optional `?lang=` für die Belegsprache (jede Sprache wird getrennt gecacht). Die Kopfzeilen X-PDF-Engine und X-PDF-Cache sagen, woher die Bytes kommen."}},"/api/v1/quotes/{id}/send":{"post":{"responses":{"200":{"description":"Angebot versendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"messageId":{"type":["string","null"],"description":"Id beim Mailversender"},"recipients":{"type":"array","items":{"type":"string"},"description":"Tatsaechliche Empfaenger"},"sentAt":{"type":"string","description":"Zeitpunkt (ISO)"},"simuliert":{"type":"boolean","description":"true = nur simuliert, es ging keine Mail raus"}},"required":["ok","messageId","recipients","sentAt","simuliert"]},"example":{"ok":true,"messageId":"string","recipients":["string"],"sentAt":"string","simuliert":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden"},"502":{"description":"E-Mail-Versand fehlgeschlagen"}},"operationId":"postApiV1QuotesByIdSend","tags":["quotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Send quote by email","description":"Versendet ein Angebot per E-Mail an den/die Empfaenger, standardmaessig mit dem Angebots-PDF im Anhang (`attachPdf: false` laesst ihn weg). War das Angebot ein Entwurf, steht es danach auf `sent`. Enthaelt der Beleg eine Kunden-E-Mail, traegt die Mail zusaetzlich einen signierten Annahme-Link. WICHTIG: das Antwortfeld `simuliert` sagt, ob wirklich eine Mail hinausging — auf Umgebungen mit Mail-Attrappe ist es `true` und es wurde NICHTS versendet, obwohl die Antwort 200 mit `ok: true` lautet. Weitere Ausgaenge: 402 wenn das Mail-Kontingent des Mandanten erschoepft ist, 413 wenn die Zusatzanhaenge zusammen 10 MB ueberschreiten, 503 wenn der PDF-Renderer gestoert ist (dann geht bewusst gar nichts raus), 502 wenn der Mailversender ablehnt. Bei 413 und 502 wird das Kontingent zurueckgebucht, beim PDF-503 nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"cc":{"type":"array","items":{"type":"string","format":"email"}},"bcc":{"type":"array","items":{"type":"string","format":"email"}},"subject":{"type":"string","minLength":1,"maxLength":255},"message":{"type":"string","maxLength":4000},"attachPdf":{"type":"boolean"},"includePortalLink":{"type":"boolean","default":true},"lang":{"type":"string","enum":["de","en","fr","es","nl","da","pl","cs","zh"]},"template":{"type":"string","enum":["doc-quote","doc-order","doc-delivery","doc-invoice","dunning-level1","dunning-level2","dunning-level3"]},"extraAttachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"contentBase64":{"type":"string","minLength":1},"contentType":{"type":"string","maxLength":100}},"required":["filename","contentBase64"]},"maxItems":10},"attachmentMode":{"type":"string","enum":["separate","merge"]}},"required":["to"]},"example":{"to":["beispiel@example.com"],"cc":["beispiel@example.com"],"bcc":["beispiel@example.com"],"subject":"string","message":"string","attachPdf":true,"includePortalLink":true,"lang":"de","template":"doc-quote","extraAttachments":[{"filename":"string","contentBase64":"string","contentType":"string"}],"attachmentMode":"separate"}}}}}},"/api/v1/doc-templates":{"get":{"responses":{"200":{"description":"Liste der Templates","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung der Vorlage (UUID)"},"tenantId":{"description":"Mandant, dem die Vorlage gehoert"},"name":{"type":"string","description":"Name der Vorlage"},"description":{"description":"Beschreibung; null, wenn keine hinterlegt ist"},"docType":{"type":"string","description":"Belegart, fuer die die Vorlage gedacht ist: quote, order, delivery oder invoice"},"templateJsonb":{"anyOf":[{"type":"object","additionalProperties":{}},{"type":"array","items":{}}],"description":"Der gespeicherte Beleg-Schnappschuss; nie null, im Zweifel ein leeres Objekt"},"isPublic":{"type":"boolean","description":"Steht die Vorlage allen Benutzern des Mandanten offen?"},"createdBy":{"description":"Wer die Vorlage angelegt hat; null, wenn unbekannt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","name","docType","templateJsonb","isPublic"],"additionalProperties":false},"description":"Die Vorlagen dieser Seite, nach Namen sortiert"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer — steht OBEN, nicht in pagination"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"description":"Angewendete Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Datensaetze"}},"required":["limit","offset"],"additionalProperties":false,"description":"Seitenangaben — limit/offset, ohne total"}},"required":["data","total","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","docType":"string","templateJsonb":{},"isPublic":true}],"total":0,"pagination":{"limit":1,"offset":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Doc-templates","tags":["doc-templates"],"parameters":[{"in":"query","name":"doc_type","schema":{"type":"string","enum":["quote","order","delivery","invoice"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"description":"Listet Beleg-Templates (optional gefiltert nach doc_type). Gelesen wird `public.doc_templates` — eine gemeinsame Tabelle, in der die Trennung ueber `tenant_id` in der Bedingung laeuft, nicht ueber ein eigenes Schema. Sortiert alphabetisch nach Namen; `limit` liegt zwischen 1 und 200 (Vorgabe 50), `offset` beginnt bei 0. `total` zaehlt mit demselben Filter wie die Liste und steht OBEN neben `pagination`, nicht darin. Je Vorlage kommt der vollstaendige Inhalt in `templateJsonb` mit — die Liste ist also kein Auszug.","summary":"Listet Beleg-Templates (optional gefiltert nach doc_type)","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Vorlage angelegt — die neue Vorlage, inklusive vergebener Id","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung der Vorlage (UUID)"},"tenantId":{"description":"Mandant, dem die Vorlage gehoert"},"name":{"type":"string","description":"Name der Vorlage"},"description":{"description":"Beschreibung; null, wenn keine hinterlegt ist"},"docType":{"type":"string","description":"Belegart, fuer die die Vorlage gedacht ist: quote, order, delivery oder invoice"},"templateJsonb":{"anyOf":[{"type":"object","additionalProperties":{}},{"type":"array","items":{}}],"description":"Der gespeicherte Beleg-Schnappschuss; nie null, im Zweifel ein leeres Objekt"},"isPublic":{"type":"boolean","description":"Steht die Vorlage allen Benutzern des Mandanten offen?"},"createdBy":{"description":"Wer die Vorlage angelegt hat; null, wenn unbekannt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","name","docType","templateJsonb","isPublic"],"additionalProperties":false},"example":{"id":"string","name":"string","docType":"string","templateJsonb":{},"isPublic":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Doc-templates","tags":["doc-templates"],"parameters":[],"summary":"Create document template","description":"Legt eine Beleg-Vorlage an. Der Inhalt (templateJsonb) wird als Schnappschuss gespeichert und nicht geprüft — was darin steht, entscheidet erst das Anwenden. Namen dürfen sich wiederholen, es gibt keine Eindeutigkeit.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":2000},"docType":{"type":"string","enum":["quote","order","delivery","invoice"]},"templateJsonb":{"type":"object","additionalProperties":{}},"isPublic":{"type":"boolean","default":false}},"required":["name","docType","templateJsonb"]},"example":{"name":"string","description":"string","docType":"quote","templateJsonb":{},"isPublic":true}}}}}},"/api/v1/doc-templates/{id}":{"get":{"responses":{"200":{"description":"Die Vorlage — dieselbe Form wie in der Liste","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung der Vorlage (UUID)"},"tenantId":{"description":"Mandant, dem die Vorlage gehoert"},"name":{"type":"string","description":"Name der Vorlage"},"description":{"description":"Beschreibung; null, wenn keine hinterlegt ist"},"docType":{"type":"string","description":"Belegart, fuer die die Vorlage gedacht ist: quote, order, delivery oder invoice"},"templateJsonb":{"anyOf":[{"type":"object","additionalProperties":{}},{"type":"array","items":{}}],"description":"Der gespeicherte Beleg-Schnappschuss; nie null, im Zweifel ein leeres Objekt"},"isPublic":{"type":"boolean","description":"Steht die Vorlage allen Benutzern des Mandanten offen?"},"createdBy":{"description":"Wer die Vorlage angelegt hat; null, wenn unbekannt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","name","docType","templateJsonb","isPublic"],"additionalProperties":false},"example":{"id":"string","name":"string","docType":"string","templateJsonb":{},"isPublic":true}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Vorlage nicht gefunden — oder sie gehört einem anderen Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"template_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Doc-templatesById","tags":["doc-templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get document template","description":"Liefert eine einzelne Beleg-Vorlage. Die Vorlagen liegen in einer gemeinsamen Tabelle, die Abfrage ist aber auf den eigenen Mandanten eingeschränkt — die Vorlage eines anderen Mandanten ergibt 404, nicht 403."},"patch":{"responses":{"200":{"description":"Vorlage aktualisiert — die Vorlage nach der Änderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Technische Kennung der Vorlage (UUID)"},"tenantId":{"description":"Mandant, dem die Vorlage gehoert"},"name":{"type":"string","description":"Name der Vorlage"},"description":{"description":"Beschreibung; null, wenn keine hinterlegt ist"},"docType":{"type":"string","description":"Belegart, fuer die die Vorlage gedacht ist: quote, order, delivery oder invoice"},"templateJsonb":{"anyOf":[{"type":"object","additionalProperties":{}},{"type":"array","items":{}}],"description":"Der gespeicherte Beleg-Schnappschuss; nie null, im Zweifel ein leeres Objekt"},"isPublic":{"type":"boolean","description":"Steht die Vorlage allen Benutzern des Mandanten offen?"},"createdBy":{"description":"Wer die Vorlage angelegt hat; null, wenn unbekannt"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","name","docType","templateJsonb","isPublic"],"additionalProperties":false},"example":{"id":"string","name":"string","docType":"string","templateJsonb":{},"isPublic":true}}}},"400":{"description":"Leerer Körper — es gibt nichts zu ändern","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"no_fields_to_update","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Vorlage nicht gefunden — oder sie gehört einem anderen Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"template_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1Doc-templatesById","tags":["doc-templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update document template","description":"Ändert eine Beleg-Vorlage. Nur mitgeschickte Felder werden geschrieben. Ein leerer Körper ergibt 400 — er würde sonst nur den Änderungszeitpunkt hochsetzen. Wird templateJsonb mitgeschickt, ersetzt es den bisherigen Schnappschuss vollständig; es wird nicht zusammengeführt. Bereits erzeugte Belege bleiben unberührt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":["string","null"],"maxLength":2000},"docType":{"type":"string","enum":["quote","order","delivery","invoice"]},"templateJsonb":{"type":"object","additionalProperties":{}},"isPublic":{"type":"boolean"}}},"example":{"name":"string","description":"string","docType":"quote","templateJsonb":{},"isPublic":true}}}}},"delete":{"responses":{"204":{"description":"Gelöscht — ohne Rumpf"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Vorlage nicht gefunden — oder sie gehört einem anderen Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"template_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1Doc-templatesById","tags":["doc-templates"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete document template","description":"Löscht eine Beleg-Vorlage endgültig — kein Soft-Delete, kein Wiederherstellen. Belege, die aus der Vorlage entstanden sind, bleiben bestehen; sie tragen die Vorlagen-Id nur im Aktivitätsprotokoll."}},"/api/v1/doc-templates/{id}/apply":{"post":{"responses":{"201":{"description":"Neuer Beleg angelegt — nur Id, Nummer und Belegart, nicht der ganze Beleg","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des neu erzeugten Belegs"},"number":{"type":["string","null"],"description":"Belegnummer; null, wenn die Datenbank keine zurueckgab"},"docType":{"type":"string","enum":["quote","order","delivery","invoice"],"description":"Belegart, die erzeugt wurde"},"templateId":{"type":"string","format":"uuid","description":"Die verwendete Vorlage"}},"required":["id","number","docType","templateId"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","number":"string","docType":"quote","templateId":"00000000-0000-4000-8000-000000000000"}}}},"400":{"description":"Belegart weder in der Vorlage noch im Aufruf brauchbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_doc_type","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Vorlage nicht gefunden — oder sie gehört einem anderen Mandanten","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"template_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Der Beleg wurde eingefügt, kam aber ohne Kennung zurück — nichts Brauchbares entstanden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"apply_failed","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Doc-templatesByIdApply","tags":["doc-templates"],"parameters":[{"in":"query","name":"to_doc_type","schema":{"type":"string","enum":["quote","order","delivery","invoice"]}},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Apply document template","description":"Erzeugt einen neuen Beleg aus dem Vorlagen-Schnappschuss, immer im Status `draft`. Ohne `to_doc_type` gilt die Belegart der Vorlage. Aus dem Schnappschuss werden Kunde, Titel, Positionen und Notiz übernommen — Summen werden NICHT gerechnet, sie stehen auf 0 und der Beleg muss einmal gespeichert werden. Die Belegnummer vergibt dieser Aufruf selbst durch Zählen der vorhandenen Belege, NICHT über den Nummernkreis des Mandanten; bei gleichzeitigen Aufrufen kann sie deshalb kollidieren. Bei einer Rechnung wird ein Zahlungsziel von 14 Tagen gesetzt."}},"/api/v1/user-views":{"get":{"responses":{"200":{"description":"Liste der Views","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string"},"tenantId":{"type":"string"},"entity":{"type":"string"},"name":{"type":"string"},"filtersJsonb":{"type":"object","additionalProperties":{}},"sortJsonb":{"type":["object","null"],"additionalProperties":{}},"isDefault":{"type":"boolean"},"isShared":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","userId","tenantId","entity","name","filtersJsonb","sortJsonb","isDefault","isShared","createdAt","updatedAt"]}}},"required":["data"]},"example":{"data":[{"id":"string","userId":"string","tenantId":"string","entity":"string","name":"string","filtersJsonb":{},"sortJsonb":{},"isDefault":true,"isShared":true,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1User-views","tags":["user-views"],"parameters":[{"in":"query","name":"entity","schema":{"type":"string","minLength":1,"maxLength":64},"required":true}],"description":"Listet eigene und geteilte Ansichten zu einer Entity. Der Query-Parameter `entity` ist Pflicht. Geliefert werden die Zeilen aus public.user_views dieses Mandanten, die dem aufrufenden Nutzer gehoeren oder `is_shared = true` tragen, sortiert nach Standard-Ansicht zuerst und dann nach Name. Es wird nicht geblaettert und nichts begrenzt.","summary":"Listet eigene und geteilte Ansichten zu einer Entity","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"View angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string"},"tenantId":{"type":"string"},"entity":{"type":"string"},"name":{"type":"string"},"filtersJsonb":{"type":"object","additionalProperties":{}},"sortJsonb":{"type":["object","null"],"additionalProperties":{}},"isDefault":{"type":"boolean"},"isShared":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","userId","tenantId","entity","name","filtersJsonb","sortJsonb","isDefault","isShared","createdAt","updatedAt"]},"example":{"id":"string","userId":"string","tenantId":"string","entity":"string","name":"string","filtersJsonb":{},"sortJsonb":{},"isDefault":true,"isShared":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Name existiert"}},"operationId":"postApiV1User-views","tags":["user-views"],"parameters":[],"description":"Legt eine neue Ansicht fuer den aufrufenden Nutzer an. `user_id` und `tenant_id` kommen aus dem Kontext, nicht aus dem Rumpf. Ist `isDefault` gesetzt, verliert die bisherige Standard-Ansicht derselben Kombination aus Nutzer, Mandant und Entity vorher ihre Markierung. Ein je (Nutzer, Mandant, Entity) schon vergebener Name verletzt den eindeutigen Index und ergibt 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","minLength":1,"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":120},"filtersJsonb":{"type":"object","additionalProperties":{},"default":{}},"sortJsonb":{"type":"object","additionalProperties":{}},"isDefault":{"type":"boolean","default":false},"isShared":{"type":"boolean","default":false}},"required":["entity","name"]},"example":{"entity":"string","name":"string","filtersJsonb":{},"sortJsonb":{},"isDefault":true,"isShared":true}}}},"summary":"Legt eine neue Ansicht fuer den aufrufenden Nutzer an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/user-views/{id}":{"patch":{"responses":{"200":{"description":"View aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string"},"tenantId":{"type":"string"},"entity":{"type":"string"},"name":{"type":"string"},"filtersJsonb":{"type":"object","additionalProperties":{}},"sortJsonb":{"type":["object","null"],"additionalProperties":{}},"isDefault":{"type":"boolean"},"isShared":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","userId","tenantId","entity","name","filtersJsonb","sortJsonb","isDefault","isShared","createdAt","updatedAt"]},"example":{"id":"string","userId":"string","tenantId":"string","entity":"string","name":"string","filtersJsonb":{},"sortJsonb":{},"isDefault":true,"isShared":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"patchApiV1User-viewsById","tags":["user-views"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aktualisiert einzelne Felder einer eigenen Ansicht. Geschrieben wird nur, was der Rumpf nennt; ein leerer Rumpf ergibt 400. Die WHERE-Bedingung umfasst neben der Kennung immer `user_id` und `tenant_id`, fremde Ansichten sind darueber nicht erreichbar und ergeben 404. Mit `isDefault: true` verliert die bisherige Standard-Ansicht derselben Entity ihre Markierung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"filtersJsonb":{"type":"object","additionalProperties":{}},"sortJsonb":{"type":["object","null"],"additionalProperties":{}},"isDefault":{"type":"boolean"},"isShared":{"type":"boolean"}}},"example":{"name":"string","filtersJsonb":{},"sortJsonb":{},"isDefault":true,"isShared":true}}}},"summary":"Aktualisiert einzelne Felder einer eigenen Ansicht","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"204":{"description":"Geloescht, kein Rumpf"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"deleteApiV1User-viewsById","tags":["user-views"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Loescht eine eigene Ansicht endgueltig. Die Zeile verschwindet per SQL-DELETE aus public.user_views, es gibt kein Soft-Delete und kein Rueckgaengig. Die WHERE-Bedingung umfasst `user_id` und `tenant_id`, fremde Ansichten ergeben 404. Erfolg wird mit 204 und leerem Rumpf beantwortet.","summary":"Loescht eine eigene Ansicht endgueltig","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/user-views/{id}/set-default":{"post":{"responses":{"200":{"description":"Als Default markiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string"},"tenantId":{"type":"string"},"entity":{"type":"string"},"name":{"type":"string"},"filtersJsonb":{"type":"object","additionalProperties":{}},"sortJsonb":{"type":["object","null"],"additionalProperties":{}},"isDefault":{"type":"boolean"},"isShared":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","userId","tenantId","entity","name","filtersJsonb","sortJsonb","isDefault","isShared","createdAt","updatedAt"]},"example":{"id":"string","userId":"string","tenantId":"string","entity":"string","name":"string","filtersJsonb":{},"sortJsonb":{},"isDefault":true,"isShared":true,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"postApiV1User-viewsByIdSet-default","tags":["user-views"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Markiert eine Ansicht als Standard fuer ihre Entity. Die Entity wird aus der gespeicherten Zeile gelesen, nicht aus der Anfrage; danach verliert jede andere Ansicht des Nutzers zu dieser Entity ihre Markierung. Die Antwort ist die aktualisierte Ansicht. Fremde oder unbekannte Kennungen ergeben 404.","summary":"Markiert eine Ansicht als Standard fuer ihre Entity","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/signatures/request":{"post":{"responses":{"201":{"description":"Die Anfrage samt Merkmal und Link. Das Merkmal steht NUR hier — die API gibt es danach nicht mehr heraus.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"},"token":{"type":"string","description":"Das Merkmal im KLARTEXT — es kommt nur hier heraus und wird nie wieder ausgegeben"},"magicLink":{"type":"string","description":"Die fertige Adresse fuer den Empfaenger, inklusive Merkmal"},"expiresAt":{"type":"string","description":"ISO-Zeitpunkt, ab dem der Link nicht mehr gilt"}},"required":["ok","id","token","magicLink","expiresAt"]},"example":{"ok":true,"id":"string","token":"string","magicLink":"string","expiresAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1SignaturesRequest","tags":["signatures"],"parameters":[],"summary":"Magic-Link-Signatur-Anfrage anlegen","description":"Legt eine Signatur-Anfrage zu einem Beleg (`quote`, `order`, `delivery` oder `invoice`) an und erzeugt dazu ein einmaliges Merkmal samt fertigem Link. Der Link gilt `expiresInDays` Tage (1..90, Vorgabe 14). VERSCHICKT wird nichts — der Aufrufer bekommt Merkmal und Adresse zurueck und muss die Mail selbst auf den Weg bringen. Der Beleg wird nicht auf Existenz geprueft, und mehrere Anfragen zum selben Beleg schliessen einander nicht aus: jede erzeugt ein eigenes, gueltiges Merkmal. Ein Eintrag im Aktivitaetsverlauf entsteht nebenbei.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"docType":{"type":"string","enum":["quote","order","delivery","invoice"]},"docId":{"type":"string","minLength":1},"recipientEmail":{"type":"string","format":"email"},"expiresInDays":{"type":"integer","minimum":1,"maximum":90,"default":14}},"required":["docType","docId","recipientEmail"]},"example":{"docType":"quote","docId":"string","recipientEmail":"beispiel@example.com","expiresInDays":1}}}}}},"/api/v1/signatures/verify":{"get":{"responses":{"200":{"description":"Das Merkmal gilt. `alreadySigned` sagt, ob schon quittiert wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"signatureId":{"type":"string"},"docType":{"type":"string","description":"quote | order | delivery | invoice"},"docId":{"type":"string"},"recipientEmail":{"type":"string","description":"Die Adresse, an die der Link ging"},"expiresAt":{"type":"string"},"alreadySigned":{"type":"boolean"},"signedAt":{"type":["string","null"]}},"required":["ok","signatureId","docType","docId","recipientEmail","expiresAt","alreadySigned","signedAt"]},"example":{"ok":true,"signatureId":"string","docType":"string","docId":"string","recipientEmail":"string","expiresAt":"string","alreadySigned":true,"signedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein solches Merkmal (`invalid_token`)"},"410":{"description":"Das Merkmal ist abgelaufen (`token_expired`)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1SignaturesVerify","tags":["signatures"],"parameters":[{"in":"query","name":"token","schema":{"type":"string","minLength":16,"maxLength":128},"required":true}],"summary":"Magic-Link-Token validieren","description":"Prueft das Merkmal aus dem Link und liefert die Angaben, die das Kundenportal zur Anzeige braucht: Belegart, Beleg-Id, Empfaengeradresse, Gueltigkeit und ob bereits quittiert wurde. Der Aufruf braucht KEINE Anmeldung — das Merkmal allein weist aus. Er ist rein lesend: nichts wird quittiert und nichts vermerkt. Ein unbekanntes Merkmal ergibt 404, ein abgelaufenes 410 — bewusst unterschieden, damit die Oberflaeche „Link ungueltig\" von „Link abgelaufen\" trennen kann. Ein bereits quittiertes Merkmal bleibt gueltig und antwortet 200 mit `alreadySigned: true`."}},"/api/v1/signatures/accept":{"post":{"responses":{"200":{"description":"Quittiert. Steht `alreadySigned: true` dabei, hat dieser Aufruf NICHTS geaendert — `signedAt` ist dann der Zeitpunkt von damals.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"alreadySigned":{"type":"boolean","const":true},"signedAt":{"type":"string","description":"Beim ersten Mal der Zeitpunkt jetzt, sonst der von damals"}},"required":["ok","signedAt"]},"example":{"ok":true,"alreadySigned":true,"signedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein solches Merkmal (`invalid_token`)"},"410":{"description":"Das Merkmal ist abgelaufen (`token_expired`)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1SignaturesAccept","tags":["signatures"],"parameters":[],"summary":"Magic-Link-Akzeptanz quittieren","description":"Quittiert die Annahme ueber das Merkmal aus dem Link — ohne Anmeldung, das Merkmal allein weist aus. Festgehalten werden Zeitpunkt, IP und Browserkennung des Aufrufers; die Signaturart ist immer `click`. Bei einem Angebot wird zusaetzlich der Status auf `accepted` gesetzt, aber NUR aus `sent` oder `draft` heraus — scheitert das, bleibt die Quittung trotzdem stehen und die Antwort trotzdem 200. Ein zweiter Aufruf aendert nichts und antwortet mit `alreadySigned: true` samt dem urspruenglichen Zeitpunkt. Ein unbekanntes Merkmal ergibt 404, ein abgelaufenes 410.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":16,"maxLength":128}},"required":["token"]},"example":{"token":"stringxxxxxxxxxx"}}}}}},"/api/v1/provisionen/buchungen":{"get":{"responses":{"200":{"description":"Die gefilterten Buchungen samt Blaetterung.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"mitarbeiterId":{"type":["string","null"]},"regelId":{"type":["string","null"],"description":"Provisionsregel, aus der die Buchung entstand."},"basisDocTyp":{"type":["string","null"],"description":"Belegart, auf der die Provision beruht."},"basisDocId":{"type":["string","null"]},"basisBetrag":{"type":"number","description":"Bemessungsgrundlage in Euro."},"prozent":{"type":"number"},"betrag":{"type":"number","description":"Errechnete Provision in Euro."},"status":{"type":"string"},"teamSplit":{"type":"null","description":"Aufteilung im Team; Form nicht zugesagt."},"produktKategorie":{"type":["string","null"]},"buchungsDatum":{"type":["string","null"]},"freigegebenVon":{"type":["string","null"]},"freigegebenAm":{"type":["string","null"]},"bezahltAm":{"type":["string","null"]},"clawbackGrund":{"type":["string","null"],"description":"Grund einer Rueckforderung, falls vorhanden."},"clawbackOfId":{"type":["string","null"],"description":"Die Buchung, die zurueckgefordert wird."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","regelId","basisDocTyp","basisDocId","basisBetrag","prozent","betrag","status","produktKategorie","buchungsDatum","freigegebenVon","freigegebenAm","bezahltAm","clawbackGrund","clawbackOfId","notes","createdAt","updatedAt"]}},"total":{"type":"integer","description":"Treffer OHNE Limit — die ganze Filtermenge"},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["data","total","limit","offset"]},"example":{"data":[{"id":"string","mitarbeiterId":"string","regelId":"string","basisDocTyp":"string","basisDocId":"string","basisBetrag":0,"prozent":0,"betrag":0,"status":"string","teamSplit":null,"produktKategorie":"string","buchungsDatum":"string","freigegebenVon":"string","freigegebenAm":"string","bezahltAm":"string","clawbackGrund":"string","clawbackOfId":"string","notes":"string","createdAt":"string","updatedAt":"string"}],"total":0,"limit":0,"offset":0}}}},"401":{"description":"Auth"},"403":{"description":"Keine Manager-Rolle"}},"operationId":"getApiV1ProvisionenBuchungen","tags":["Provisionen"],"parameters":[{"in":"query","name":"mitarbeiter_id","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["accrued","approved","paid","disputed","clawback","cancelled"]}},{"in":"query","name":"from","schema":{"type":"string","format":"date"}},{"in":"query","name":"to","schema":{"type":"string","format":"date"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Provisionsbuchungen des Mandanten auflisten","description":"Listet die Provisionsbuchungen des Mandanten, neueste zuerst (nach Buchungsdatum, bei Gleichstand nach Anlagezeitpunkt). Eingrenzen ueber `mitarbeiter_id`, `status` sowie `from`/`to` auf das Buchungsdatum. Geblaettert wird ueber `limit` (1..200, Vorgabe 50) und `offset`; `total` zaehlt die Treffer OHNE Limit. ACHTUNG bei Summen: Rueckforderungen stehen als EIGENE Zeilen mit negativem Betrag darin, die zugehoerige Ursprungsbuchung bleibt unveraendert bestehen — die offene Provision eines Mitarbeiters ergibt sich erst aus der Summe beider. Ein Soft-Delete gibt es hier nicht; jede Zeile ist sichtbar. Ab Rolle `manager`."}},"/api/v1/provisionen/buchungen/{id}":{"get":{"responses":{"200":{"description":"Die Buchung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"mitarbeiterId":{"type":["string","null"]},"regelId":{"type":["string","null"],"description":"Provisionsregel, aus der die Buchung entstand."},"basisDocTyp":{"type":["string","null"],"description":"Belegart, auf der die Provision beruht."},"basisDocId":{"type":["string","null"]},"basisBetrag":{"type":"number","description":"Bemessungsgrundlage in Euro."},"prozent":{"type":"number"},"betrag":{"type":"number","description":"Errechnete Provision in Euro."},"status":{"type":"string"},"teamSplit":{"type":"null","description":"Aufteilung im Team; Form nicht zugesagt."},"produktKategorie":{"type":["string","null"]},"buchungsDatum":{"type":["string","null"]},"freigegebenVon":{"type":["string","null"]},"freigegebenAm":{"type":["string","null"]},"bezahltAm":{"type":["string","null"]},"clawbackGrund":{"type":["string","null"],"description":"Grund einer Rueckforderung, falls vorhanden."},"clawbackOfId":{"type":["string","null"],"description":"Die Buchung, die zurueckgefordert wird."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","regelId","basisDocTyp","basisDocId","basisBetrag","prozent","betrag","status","produktKategorie","buchungsDatum","freigegebenVon","freigegebenAm","bezahltAm","clawbackGrund","clawbackOfId","notes","createdAt","updatedAt"]},"example":{"id":"string","mitarbeiterId":"string","regelId":"string","basisDocTyp":"string","basisDocId":"string","basisBetrag":0,"prozent":0,"betrag":0,"status":"string","teamSplit":null,"produktKategorie":"string","buchungsDatum":"string","freigegebenVon":"string","freigegebenAm":"string","bezahltAm":"string","clawbackGrund":"string","clawbackOfId":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Buchung mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1ProvisionenBuchungenById","tags":["Provisionen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Provisionsbuchung lesen","description":"Liefert eine einzelne Buchung. Die Antwort ist die Buchung SELBST, nicht\nin einen Umschlag gepackt — anders als die Liste daneben, die\n`{ data, total }` zurueckgibt.\n\nDie Mandantentrennung laeuft ueber das Schema, nicht ueber eine\nBedingung in der Abfrage: eine fremde Kennung findet nichts und ergibt\n404 `buchung_not_found`.\n\n`basisBetrag`, `prozent` und `betrag` kommen als ZAHLEN heraus, obwohl\nsie als Dezimalspalten liegen — der Serialisierer wandelt um. Bei sehr\ngrossen Betraegen gilt damit die uebliche Genauigkeitsgrenze von\nGleitkommazahlen.\n\nAb Rolle `manager`, wie die uebrigen Routen dieses Bereichs."}},"/api/v1/provisionen/buchungen/{id}/approve":{"post":{"responses":{"200":{"description":"Die freigegebene Buchung, so wie sie jetzt in der Tabelle steht.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"mitarbeiterId":{"type":["string","null"]},"regelId":{"type":["string","null"],"description":"Provisionsregel, aus der die Buchung entstand."},"basisDocTyp":{"type":["string","null"],"description":"Belegart, auf der die Provision beruht."},"basisDocId":{"type":["string","null"]},"basisBetrag":{"type":"number","description":"Bemessungsgrundlage in Euro."},"prozent":{"type":"number"},"betrag":{"type":"number","description":"Errechnete Provision in Euro."},"status":{"type":"string"},"teamSplit":{"type":"null","description":"Aufteilung im Team; Form nicht zugesagt."},"produktKategorie":{"type":["string","null"]},"buchungsDatum":{"type":["string","null"]},"freigegebenVon":{"type":["string","null"]},"freigegebenAm":{"type":["string","null"]},"bezahltAm":{"type":["string","null"]},"clawbackGrund":{"type":["string","null"],"description":"Grund einer Rueckforderung, falls vorhanden."},"clawbackOfId":{"type":["string","null"],"description":"Die Buchung, die zurueckgefordert wird."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","regelId","basisDocTyp","basisDocId","basisBetrag","prozent","betrag","status","produktKategorie","buchungsDatum","freigegebenVon","freigegebenAm","bezahltAm","clawbackGrund","clawbackOfId","notes","createdAt","updatedAt"]},"example":{"id":"string","mitarbeiterId":"string","regelId":"string","basisDocTyp":"string","basisDocId":"string","basisBetrag":0,"prozent":0,"betrag":0,"status":"string","teamSplit":null,"produktKategorie":"string","buchungsDatum":"string","freigegebenVon":"string","freigegebenAm":"string","bezahltAm":"string","clawbackGrund":"string","clawbackOfId":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Buchung mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"buchung_not_found"}},"required":["error"]}}}},"409":{"description":"Die Buchung stand nicht auf `accrued`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_transition"},"from":{"type":"string","description":"Der Status, in dem die Buchung wirklich stand."},"to":{"type":"string","description":"Der Status, der verlangt wurde."}},"required":["error","from","to"]}}}}},"operationId":"postApiV1ProvisionenBuchungenByIdApprove","tags":["Provisionen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Provisionsbuchung freigeben","description":"Setzt die Buchung von `accrued` auf `approved` und haelt fest, WER sie\nfreigegeben hat (`freigegeben_von` aus dem Rumpf) und WANN (Serverzeit).\n\nES WIRD KEIN GELD BEWEGT. Die Freigabe ist ein Statuswechsel an EINER\nZeile — kein Zahlungsauftrag, keine Lohnabrechnung, keine Buchung in der\nFinanzbuchhaltung. Was sie bewirkt, ist allein die Reife der\nProvisionsbuchung: erst nach der Freigabe ist `POST /{id}/pay` moeglich.\n\nNICHT UMKEHRBAR. Es gibt keine Gegenroute, die auf `accrued`\nzurueckstellt, und keine, die die Zeile loescht. Eine falsche Freigabe\nwird ueber `POST /{id}/clawback` korrigiert — das legt eine NEUE\nGegenbuchung an und laesst die Ursprungsbuchung unveraendert stehen.\n\nNur aus `accrued`. Jeder andere Ausgangsstatus ergibt 409\n`invalid_transition`, und es wird nichts geaendert.\n\n`freigegeben_von` ist Pflicht, wird aber UNGEPRUEFT uebernommen: eine\nbeliebige nicht-leere Zeichenkette, kein Abgleich gegen einen Benutzer.\n\nDer Vorgang wird im Aktivitaetsprotokoll vermerkt. Scheitert das, bleibt\ndie Freigabe trotzdem bestehen — der Protokollfehler wird verschluckt.\n\nAb Rolle `manager`, wie der ganze Bereich.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"freigegeben_von":{"type":"string","minLength":1}},"required":["freigegeben_von"]},"example":{"freigegeben_von":"string"}}}}}},"/api/v1/provisionen/buchungen/{id}/pay":{"post":{"responses":{"200":{"description":"Die als bezahlt vermerkte Buchung.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"mitarbeiterId":{"type":["string","null"]},"regelId":{"type":["string","null"],"description":"Provisionsregel, aus der die Buchung entstand."},"basisDocTyp":{"type":["string","null"],"description":"Belegart, auf der die Provision beruht."},"basisDocId":{"type":["string","null"]},"basisBetrag":{"type":"number","description":"Bemessungsgrundlage in Euro."},"prozent":{"type":"number"},"betrag":{"type":"number","description":"Errechnete Provision in Euro."},"status":{"type":"string"},"teamSplit":{"type":"null","description":"Aufteilung im Team; Form nicht zugesagt."},"produktKategorie":{"type":["string","null"]},"buchungsDatum":{"type":["string","null"]},"freigegebenVon":{"type":["string","null"]},"freigegebenAm":{"type":["string","null"]},"bezahltAm":{"type":["string","null"]},"clawbackGrund":{"type":["string","null"],"description":"Grund einer Rueckforderung, falls vorhanden."},"clawbackOfId":{"type":["string","null"],"description":"Die Buchung, die zurueckgefordert wird."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","regelId","basisDocTyp","basisDocId","basisBetrag","prozent","betrag","status","produktKategorie","buchungsDatum","freigegebenVon","freigegebenAm","bezahltAm","clawbackGrund","clawbackOfId","notes","createdAt","updatedAt"]},"example":{"id":"string","mitarbeiterId":"string","regelId":"string","basisDocTyp":"string","basisDocId":"string","basisBetrag":0,"prozent":0,"betrag":0,"status":"string","teamSplit":null,"produktKategorie":"string","buchungsDatum":"string","freigegebenVon":"string","freigegebenAm":"string","bezahltAm":"string","clawbackGrund":"string","clawbackOfId":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Buchung mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"buchung_not_found"}},"required":["error"]}}}},"409":{"description":"Die Buchung stand nicht auf `approved`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_transition"},"from":{"type":"string","description":"Der Status, in dem die Buchung wirklich stand."},"to":{"type":"string","description":"Der Status, der verlangt wurde."}},"required":["error","from","to"]}}}}},"operationId":"postApiV1ProvisionenBuchungenByIdPay","tags":["Provisionen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine freigegebene Provisionsbuchung als bezahlt vermerken","description":"Setzt die Buchung von `approved` auf `paid` und traegt `bezahlt_am` ein.\n\nES WIRD KEINE ZAHLUNG AUSGELOEST. Kein Bankauftrag, kein Lohnlauf, keine\nFinanzbuchung — die Route haelt nur fest, DASS ausserhalb des Systems\ngezahlt wurde. Wer den Betrag tatsaechlich ueberweist, entscheidet sich\nanderswo.\n\nDas Datum ist frei waehlbar: ohne `bezahlt_am` gilt der Zeitpunkt des\nAufrufs, mit `bezahlt_am` der mitgegebene Zeitstempel. Er wird NICHT\ngegen die Freigabe oder gegen heute geprueft — ein Datum in der Zukunft\noder vor der Freigabe wird angenommen.\n\nNICHT UMKEHRBAR. Es gibt keinen Weg zurueck nach `approved` und keinen,\ndie Zeile zu loeschen. Die einzige Korrektur ist `POST /{id}/clawback`,\ndas eine NEUE Gegenbuchung erzeugt.\n\nNur aus `approved`. Eine noch nicht freigegebene Buchung (`accrued`)\nergibt 409 `invalid_transition` — die Reihenfolge accrued → approved →\npaid laesst sich nicht abkuerzen.\n\nAb Rolle `manager`, wie der ganze Bereich.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bezahlt_am":{"type":"string","format":"date-time"}}},"example":{"bezahlt_am":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/provisionen/buchungen/{id}/clawback":{"post":{"responses":{"201":{"description":"Beide Zeilen: `original` unveraendert, `clawback` die neu angelegte Gegenbuchung.","content":{"application/json":{"schema":{"type":"object","properties":{"original":{"type":"object","properties":{"id":{"type":"string"},"mitarbeiterId":{"type":["string","null"]},"regelId":{"type":["string","null"],"description":"Provisionsregel, aus der die Buchung entstand."},"basisDocTyp":{"type":["string","null"],"description":"Belegart, auf der die Provision beruht."},"basisDocId":{"type":["string","null"]},"basisBetrag":{"type":"number","description":"Bemessungsgrundlage in Euro."},"prozent":{"type":"number"},"betrag":{"type":"number","description":"Errechnete Provision in Euro."},"status":{"type":"string"},"teamSplit":{"type":"null","description":"Aufteilung im Team; Form nicht zugesagt."},"produktKategorie":{"type":["string","null"]},"buchungsDatum":{"type":["string","null"]},"freigegebenVon":{"type":["string","null"]},"freigegebenAm":{"type":["string","null"]},"bezahltAm":{"type":["string","null"]},"clawbackGrund":{"type":["string","null"],"description":"Grund einer Rueckforderung, falls vorhanden."},"clawbackOfId":{"type":["string","null"],"description":"Die Buchung, die zurueckgefordert wird."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","regelId","basisDocTyp","basisDocId","basisBetrag","prozent","betrag","status","produktKategorie","buchungsDatum","freigegebenVon","freigegebenAm","bezahltAm","clawbackGrund","clawbackOfId","notes","createdAt","updatedAt"]},"clawback":{"type":"object","properties":{"id":{"type":"string"},"mitarbeiterId":{"type":["string","null"]},"regelId":{"type":["string","null"],"description":"Provisionsregel, aus der die Buchung entstand."},"basisDocTyp":{"type":["string","null"],"description":"Belegart, auf der die Provision beruht."},"basisDocId":{"type":["string","null"]},"basisBetrag":{"type":"number","description":"Bemessungsgrundlage in Euro."},"prozent":{"type":"number"},"betrag":{"type":"number","description":"Errechnete Provision in Euro."},"status":{"type":"string"},"teamSplit":{"type":"null","description":"Aufteilung im Team; Form nicht zugesagt."},"produktKategorie":{"type":["string","null"]},"buchungsDatum":{"type":["string","null"]},"freigegebenVon":{"type":["string","null"]},"freigegebenAm":{"type":["string","null"]},"bezahltAm":{"type":["string","null"]},"clawbackGrund":{"type":["string","null"],"description":"Grund einer Rueckforderung, falls vorhanden."},"clawbackOfId":{"type":["string","null"],"description":"Die Buchung, die zurueckgefordert wird."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","regelId","basisDocTyp","basisDocId","basisBetrag","prozent","betrag","status","produktKategorie","buchungsDatum","freigegebenVon","freigegebenAm","bezahltAm","clawbackGrund","clawbackOfId","notes","createdAt","updatedAt"]}},"required":["original","clawback"]},"example":{"original":{"id":"string","mitarbeiterId":"string","regelId":"string","basisDocTyp":"string","basisDocId":"string","basisBetrag":0,"prozent":0,"betrag":0,"status":"string","teamSplit":null,"produktKategorie":"string","buchungsDatum":"string","freigegebenVon":"string","freigegebenAm":"string","bezahltAm":"string","clawbackGrund":"string","clawbackOfId":"string","notes":"string","createdAt":"string","updatedAt":"string"},"clawback":{"id":"string","mitarbeiterId":"string","regelId":"string","basisDocTyp":"string","basisDocId":"string","basisBetrag":0,"prozent":0,"betrag":0,"status":"string","teamSplit":null,"produktKategorie":"string","buchungsDatum":"string","freigegebenVon":"string","freigegebenAm":"string","bezahltAm":"string","clawbackGrund":"string","clawbackOfId":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Buchung mit dieser Kennung in diesem Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"buchung_not_found"}},"required":["error"]}}}},"409":{"description":"Die Buchung stand weder auf `approved` noch auf `paid`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_transition"},"from":{"type":"string","description":"Der Status, in dem die Buchung wirklich stand."},"to":{"type":"string","description":"Der Status, der verlangt wurde."}},"required":["error","from","to"]}}}}},"operationId":"postApiV1ProvisionenBuchungenByIdClawback","tags":["Provisionen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Provision zurueckfordern (Gegenbuchung anlegen)","description":"Legt eine NEUE Buchung mit dem NEGATIVEN Betrag der Ursprungsbuchung an,\nStatus `clawback`. Sie uebernimmt Mitarbeiter, Regel, Beleg, Bemessungs-\ngrundlage, Prozentsatz, Team-Aufteilung und Produktkategorie und zeigt\nueber `clawbackOfId` auf die Ursprungsbuchung; `clawbackGrund` traegt den\nmitgegebenen Grund.\n\nDIE URSPRUNGSBUCHUNG BLEIBT UNVERAENDERT. Ihr Status, ihr Betrag, ihre\nFreigabe und ihr Zahldatum stehen weiter da — es wird nichts storniert\nund nichts geloescht. Der Ausgleich entsteht erst in der SUMME beider\nZeilen. Wer die offene Provision eines Mitarbeiters berechnet, muss\ndeshalb ueber alle Zeilen summieren; die Ursprungsbuchung allein\nanzusehen ergibt weiter den vollen Betrag.\n\nMEHRFACH AUFRUFBAR, und das ist eine Folge davon: weil die\nUrsprungsbuchung ihren Status behaelt, legt jeder weitere Aufruf eine\nWEITERE Gegenbuchung an. Es wird nicht geprueft, ob es schon eine gibt.\n\nNur aus `approved` oder `paid` — was noch nicht freigegeben ist, kann\nnicht zurueckgefordert werden (409 `invalid_transition`).\n\n`grund` ist Pflicht und muss zwischen 5 und 500 Zeichen lang sein.\n\nAb Rolle `manager`, wie der ganze Bereich.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"grund":{"type":"string","minLength":5,"maxLength":500}},"required":["grund"]},"example":{"grund":"string"}}}}}},"/api/v1/provisionen/buchungen/bulk-approve":{"post":{"responses":{"200":{"description":"Zaehler und die geaenderten Buchungen. Auch bei 0 Treffern.","content":{"application/json":{"schema":{"type":"object","properties":{"approved":{"type":"integer","description":"Tatsaechlich geaenderte Zeilen."},"requested":{"type":"integer","description":"Anzahl der uebergebenen Kennungen."},"skipped":{"type":"integer","description":"requested minus approved — ohne Angabe des Grundes."},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"mitarbeiterId":{"type":["string","null"]},"regelId":{"type":["string","null"],"description":"Provisionsregel, aus der die Buchung entstand."},"basisDocTyp":{"type":["string","null"],"description":"Belegart, auf der die Provision beruht."},"basisDocId":{"type":["string","null"]},"basisBetrag":{"type":"number","description":"Bemessungsgrundlage in Euro."},"prozent":{"type":"number"},"betrag":{"type":"number","description":"Errechnete Provision in Euro."},"status":{"type":"string"},"teamSplit":{"type":"null","description":"Aufteilung im Team; Form nicht zugesagt."},"produktKategorie":{"type":["string","null"]},"buchungsDatum":{"type":["string","null"]},"freigegebenVon":{"type":["string","null"]},"freigegebenAm":{"type":["string","null"]},"bezahltAm":{"type":["string","null"]},"clawbackGrund":{"type":["string","null"],"description":"Grund einer Rueckforderung, falls vorhanden."},"clawbackOfId":{"type":["string","null"],"description":"Die Buchung, die zurueckgefordert wird."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","regelId","basisDocTyp","basisDocId","basisBetrag","prozent","betrag","status","produktKategorie","buchungsDatum","freigegebenVon","freigegebenAm","bezahltAm","clawbackGrund","clawbackOfId","notes","createdAt","updatedAt"]}}},"required":["approved","requested","skipped","data"]},"example":{"approved":0,"requested":0,"skipped":0,"data":[{"id":"string","mitarbeiterId":"string","regelId":"string","basisDocTyp":"string","basisDocId":"string","basisBetrag":0,"prozent":0,"betrag":0,"status":"string","teamSplit":null,"produktKategorie":"string","buchungsDatum":"string","freigegebenVon":"string","freigegebenAm":"string","bezahltAm":"string","clawbackGrund":"string","clawbackOfId":"string","notes":"string","createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ProvisionenBuchungenBulk-approve","tags":["Provisionen"],"parameters":[],"summary":"Mehrere Provisionsbuchungen auf einmal freigeben","description":"Setzt bis zu 100 Buchungen in EINER Anweisung von `accrued` auf\n`approved`. Wie bei der Einzelfreigabe wird KEIN GELD BEWEGT, und der\nSchritt ist NICHT UMKEHRBAR.\n\nDie Antwort ist IMMER 200 — auch wenn keine einzige Zeile getroffen\nwurde. `approved` zaehlt die tatsaechlich geaenderten Zeilen, `skipped`\nist die Differenz zu `requested`.\n\n`skipped` UNTERSCHEIDET NICHT, warum eine Kennung nicht durchkam: „gibt\nes nicht\", „gehoert einem anderen Mandanten\" und „stand nicht auf\n`accrued`\" zaehlen alle gleich. Wer den Grund braucht, holt die Buchung\neinzeln.\n\nWer freigegeben hat, wird in dieser Reihenfolge bestimmt:\n`freigegeben_von` aus dem Rumpf, ersatzweise die Kennung des\nangemeldeten Benutzers, ersatzweise woertlich `system`.\n\nIm Aktivitaetsprotokoll steht EIN Eintrag fuer den ganzen Aufruf, nicht\neiner je Buchung; als Kennung traegt er woertlich `bulk`.\n\nAb Rolle `manager`, wie der ganze Bereich.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"freigegeben_von":{"type":"string","minLength":1}},"required":["ids"]},"example":{"ids":["string"],"freigegeben_von":"string"}}}}}},"/api/v1/einkauf/mrp":{"get":{"responses":{"200":{"description":"Die Vorschlaege des Mandanten, zuletzt erzeugte zuerst, geblaettert.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Vorschlags (UUID als Text, vom Server vergeben)"},"artikelId":{"type":"string","minLength":1,"description":"Der zu beschaffende Artikel."},"lieferantId":{"type":["string","null"],"description":"Vorgesehener Lieferant. `null`, solange keiner gewaehlt wurde."},"vorgeschlageneMenge":{"type":"number","description":"Empfohlene Bestellmenge, bis zu vier Nachkommastellen. Beim Genehmigen ueberschreibbar."},"basis":{"type":"object","additionalProperties":{},"description":"Die Rechengrundlage des Vorschlags als freies Objekt — Bestand, Bedarf, Reichweite und, wenn bekannt, `articleName`. Leeres Objekt, wenn der Lauf nichts hinterlegt hat."},"status":{"type":"string","enum":["vorschlag","approved","bestellt","abgelehnt","verworfen"],"description":"Zustand: `vorschlag` frisch erzeugt, `approved` freigegeben, `bestellt` in eine Bestellung uebernommen, `abgelehnt` von Hand verworfen, `verworfen` von einem neuen Lauf ersetzt."},"bestellungId":{"type":["string","null"],"description":"Die Bestellung, in die der Vorschlag eingegangen ist. `null`, solange nicht bestellt."},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt des erzeugenden MRP-Laufs als ISO-8601-Zeitstempel in UTC."},"approvedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Entscheidung. Auch eine ABLEHNUNG setzt ihn — das Feld heisst nur so, es bedeutet nicht „genehmigt\"."},"approvedBy":{"type":["string","null"],"description":"Wer entschieden hat. `null` bei fehlendem Benutzerkontext."},"notes":{"type":["string","null"],"description":"Freitext. Beim Ablehnen steht hier die Begruendung — sie ERSETZT eine bisherige Notiz."}},"required":["id","artikelId","lieferantId","vorgeschlageneMenge","basis","status","bestellungId","generatedAt","approvedAt","approvedBy","notes"],"additionalProperties":false},"description":"Die Vorschlaege, zuletzt erzeugte zuerst."},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angeforderte Seitengroesse."},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege."},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer unter demselben Filter, unabhaengig von der Seite."}},"required":["limit","offset","total"],"additionalProperties":false,"description":"Seitenangaben zur Abfrage."}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","artikelId":"string","lieferantId":"string","vorgeschlageneMenge":0,"basis":{},"status":"vorschlag","bestellungId":"string","generatedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","approvedBy":"string","notes":"string"}],"pagination":{"limit":1,"offset":0,"total":0}}}}},"400":{"description":"Ungueltige Abfrageparameter.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer deutscher Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar oder das Anlegen der Tabellen schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1EinkaufMrp","tags":["einkauf"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["vorschlag","approved","bestellt","abgelehnt","verworfen"]}},{"in":"query","name":"artikelId","schema":{"type":"string"}}],"summary":"Bestellvorschlaege auflisten","description":"Liste der Bestellvorschläge"}},"/api/v1/einkauf/mrp/regenerate":{"post":{"responses":{"200":{"description":"Der Lauf ist durch. Ist keine Datenbank erreichbar, kommt trotzdem 200 mit `generated: 0, skipped: 0` — dieser Endpunkt unterscheidet „nichts zu tun\" nicht von „gar nicht gelaufen\".","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true`. Ein Fehlschlag kommt als 500."},"generated":{"type":"integer","minimum":0,"description":"Anzahl neu erzeugter Vorschlaege."},"skipped":{"type":"integer","minimum":0,"description":"Anzahl uebersprungener Artikel — meist, weil schon ein offener Vorschlag bestand."}},"required":["ok","generated","skipped"],"additionalProperties":false},"example":{"ok":true,"generated":0,"skipped":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle oder Modulrecht reichen nicht (Antwort der Rechte-Schicht)."},"500":{"description":"Der MRP-Lauf selbst ist gescheitert.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"mrp_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Die durchgereichte Fehlermeldung des Laufs."}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufMrpRegenerate","tags":["einkauf"],"parameters":[],"summary":"MRP-Lauf ausloesen","description":"Trigger MRP-Lauf für aktuellen Tenant. Laeuft synchron: die Antwort kommt erst, wenn der Lauf durch ist."}},"/api/v1/einkauf/mrp/bulk-approve":{"post":{"responses":{"200":{"description":"ACHTUNG: 200 heisst NICHT „alle genehmigt\". Jeder Vorschlag wird einzeln versucht; gescheiterte tragen ein `error` in ihrer Zeile, waehrend die uebrigen gebucht werden. `approved` nennt die Zahl der wirklich uebernommenen. Wer nur die Kennzahl prueft, uebersieht Teilausfaelle.","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Der bearbeitete Vorschlag."},"bestellungId":{"type":["string","null"],"description":"Die Bestellung, in die er einging. `null`, wenn er nicht uebernommen wurde."},"error":{"type":"string","description":"Fehlt bei Erfolg. Sonst `not_found`, `invalid_status:<Zustand>` oder die Meldung der Datenbank."}},"required":["id","bestellungId"],"additionalProperties":false},"description":"Ein Eintrag je uebergebener Kennung, in derselben Reihenfolge."},"approved":{"type":"integer","minimum":0,"description":"Wie viele davon wirklich uebernommen wurden."},"bestellungId":{"type":["string","null"],"description":"Die SAMMELBESTELLUNG, an die alle erfolgreichen Vorschlaege gehaengt wurden. `null`, wenn keiner durchkam."}},"required":["results","approved","bestellungId"],"additionalProperties":false},"example":{"results":[{"id":"string","bestellungId":"string","error":"string"}],"approved":0,"bestellungId":"string"}}}},"400":{"description":"Eingabe ungueltig — etwa eine leere Liste oder mehr als 100 Kennungen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer deutscher Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle oder Modulrecht reichen nicht (Antwort der Rechte-Schicht)."},"503":{"description":"Datenbank nicht erreichbar. Fehler EINZELNER Vorschlaege fuehren nicht hierher — die stehen in `results`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufMrpBulk-approve","tags":["einkauf"],"parameters":[],"summary":"Mehrere Vorschlaege gemeinsam genehmigen","description":"Genehmigt mehrere Vorschläge gleichzeitig → erzeugt PO. Alle erfolgreichen Vorschlaege landen in EINER Sammelbestellung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"lieferant_id":{"type":"string","minLength":1}},"required":["ids","lieferant_id"]},"example":{"ids":["string"],"lieferant_id":"string"}}}}}},"/api/v1/einkauf/mrp/{id}/approve":{"post":{"responses":{"201":{"description":"Der Vorschlag ist uebernommen. Bestellung und Zustandswechsel entstehen in EINER Transaktion — es kann keine Bestellposition ohne uebernommenen Vorschlag geben. IMMER 201, auch wenn nur an eine bestehende Bestellung angehaengt wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"vorschlag":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Vorschlags (UUID als Text, vom Server vergeben)"},"artikelId":{"type":"string","minLength":1,"description":"Der zu beschaffende Artikel."},"lieferantId":{"type":["string","null"],"description":"Vorgesehener Lieferant. `null`, solange keiner gewaehlt wurde."},"vorgeschlageneMenge":{"type":"number","description":"Empfohlene Bestellmenge, bis zu vier Nachkommastellen. Beim Genehmigen ueberschreibbar."},"basis":{"type":"object","additionalProperties":{},"description":"Die Rechengrundlage des Vorschlags als freies Objekt — Bestand, Bedarf, Reichweite und, wenn bekannt, `articleName`. Leeres Objekt, wenn der Lauf nichts hinterlegt hat."},"status":{"type":"string","enum":["vorschlag","approved","bestellt","abgelehnt","verworfen"],"description":"Zustand: `vorschlag` frisch erzeugt, `approved` freigegeben, `bestellt` in eine Bestellung uebernommen, `abgelehnt` von Hand verworfen, `verworfen` von einem neuen Lauf ersetzt."},"bestellungId":{"type":["string","null"],"description":"Die Bestellung, in die der Vorschlag eingegangen ist. `null`, solange nicht bestellt."},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt des erzeugenden MRP-Laufs als ISO-8601-Zeitstempel in UTC."},"approvedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Entscheidung. Auch eine ABLEHNUNG setzt ihn — das Feld heisst nur so, es bedeutet nicht „genehmigt\"."},"approvedBy":{"type":["string","null"],"description":"Wer entschieden hat. `null` bei fehlendem Benutzerkontext."},"notes":{"type":["string","null"],"description":"Freitext. Beim Ablehnen steht hier die Begruendung — sie ERSETZT eine bisherige Notiz."}},"required":["id","artikelId","lieferantId","vorgeschlageneMenge","basis","status","bestellungId","generatedAt","approvedAt","approvedBy","notes"],"additionalProperties":false,"description":"Der Vorschlag nach der Genehmigung — `status` ist `bestellt`."},"bestellungId":{"type":"string","minLength":1,"description":"Die Bestellung. Gab es vom selben Lieferanten schon einen Entwurf aus der letzten Stunde, wurde daran ANGEHAENGT statt eine neue anzulegen."}},"required":["vorschlag","bestellungId"],"additionalProperties":false},"example":{"vorschlag":{"id":"string","artikelId":"string","lieferantId":"string","vorgeschlageneMenge":0,"basis":{},"status":"vorschlag","bestellungId":"string","generatedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","approvedBy":"string","notes":"string"},"bestellungId":"string"}}}},"400":{"description":"Eingabe ungueltig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer deutscher Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle oder Modulrecht reichen nicht (Antwort der Rechte-Schicht)."},"404":{"description":"Kein Vorschlag mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"vorschlag_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Der Vorschlag steht weder auf `vorschlag` noch auf `approved` — der Ist-Zustand steht hinter dem Doppelpunkt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","pattern":"^invalid_status:","description":"Schluessel und Ist-Zustand in EINEM Feld, getrennt durch einen Doppelpunkt — z. B. `invalid_status:bestellt`."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Die Bestelltabelle wird bei Bedarf selbst angelegt, sie ist hier also KEIN eigener Fehlergrund mehr.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufMrpByIdApprove","tags":["einkauf"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Vorschlag genehmigen","description":"Genehmigt Vorschlag → erzeugt purchase_order (oder hängt an). Gibt es vom selben Lieferanten schon einen Bestellentwurf aus der letzten Stunde, wird die Position dort angehaengt statt eine zweite Bestellung anzulegen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lieferant_id":{"type":"string","minLength":1},"menge":{"type":"number","exclusiveMinimum":0},"notes":{"type":"string","maxLength":1000}},"required":["lieferant_id"]},"example":{"lieferant_id":"string","menge":1,"notes":"string"}}}}}},"/api/v1/einkauf/mrp/{id}/reject":{"post":{"responses":{"200":{"description":"Der abgelehnte Vorschlag, unverpackt — kein `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Vorschlags (UUID als Text, vom Server vergeben)"},"artikelId":{"type":"string","minLength":1,"description":"Der zu beschaffende Artikel."},"lieferantId":{"type":["string","null"],"description":"Vorgesehener Lieferant. `null`, solange keiner gewaehlt wurde."},"vorgeschlageneMenge":{"type":"number","description":"Empfohlene Bestellmenge, bis zu vier Nachkommastellen. Beim Genehmigen ueberschreibbar."},"basis":{"type":"object","additionalProperties":{},"description":"Die Rechengrundlage des Vorschlags als freies Objekt — Bestand, Bedarf, Reichweite und, wenn bekannt, `articleName`. Leeres Objekt, wenn der Lauf nichts hinterlegt hat."},"status":{"type":"string","enum":["vorschlag","approved","bestellt","abgelehnt","verworfen"],"description":"Zustand: `vorschlag` frisch erzeugt, `approved` freigegeben, `bestellt` in eine Bestellung uebernommen, `abgelehnt` von Hand verworfen, `verworfen` von einem neuen Lauf ersetzt."},"bestellungId":{"type":["string","null"],"description":"Die Bestellung, in die der Vorschlag eingegangen ist. `null`, solange nicht bestellt."},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt des erzeugenden MRP-Laufs als ISO-8601-Zeitstempel in UTC."},"approvedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Entscheidung. Auch eine ABLEHNUNG setzt ihn — das Feld heisst nur so, es bedeutet nicht „genehmigt\"."},"approvedBy":{"type":["string","null"],"description":"Wer entschieden hat. `null` bei fehlendem Benutzerkontext."},"notes":{"type":["string","null"],"description":"Freitext. Beim Ablehnen steht hier die Begruendung — sie ERSETZT eine bisherige Notiz."}},"required":["id","artikelId","lieferantId","vorgeschlageneMenge","basis","status","bestellungId","generatedAt","approvedAt","approvedBy","notes"],"additionalProperties":false},"example":{"id":"string","artikelId":"string","lieferantId":"string","vorgeschlageneMenge":0,"basis":{},"status":"vorschlag","bestellungId":"string","generatedAt":"2026-01-01T12:00:00.000Z","approvedAt":"2026-01-01T12:00:00.000Z","approvedBy":"string","notes":"string"}}}},"400":{"description":"Begruendung fehlt, ist leer oder laenger als 1000 Zeichen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer deutscher Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle oder Modulrecht reichen nicht (Antwort der Rechte-Schicht)."},"404":{"description":"Kein Vorschlag mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"vorschlag_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das UPDATE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufMrpByIdReject","tags":["einkauf"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Vorschlag ablehnen","description":"Lehnt einen Vorschlag ab. Die Begruendung wird in `notes` gespeichert und ERSETZT dabei eine bisherige Notiz. Der Zustand wird NICHT geprueft — auch ein bereits bestellter Vorschlag laesst sich so auf `abgelehnt` setzen, ohne dass die Bestellung zurueckgeht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","minLength":1,"maxLength":1000}},"required":["reason"]},"example":{"reason":"string"}}}}}},"/api/v1/einkauf/rfq":{"get":{"responses":{"200":{"description":"Die Anfragen des Mandanten, neueste zuerst, geblaettert.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Anfrage (UUID als Text, vom Server vergeben)"},"rfqNr":{"type":"string","minLength":1,"description":"Sprechende Nummer der Anfrage, vom Server vergeben."},"artikelId":{"type":"string","minLength":1,"description":"Angefragter Artikel."},"menge":{"type":"number","description":"Angefragte Menge, bis zu vier Nachkommastellen."},"lieferanten":{"type":"array","items":{"type":"string"},"description":"Die angefragten Lieferanten. Mindestens einer, beim Anlegen festgelegt."},"deadline":{"type":"string","minLength":10,"description":"Frist fuer die Angebote. In der Datenbank ein reiner Kalendertag (Spaltentyp DATE); in der Antwort steht die Zeichenkette, die der Treiber daraus macht."},"status":{"type":"string","enum":["offen","abgeschlossen","vergeben","abgesagt"],"description":"Zustand: `offen` nimmt Angebote an, `abgeschlossen` nicht mehr (kann aber noch vergeben werden), `vergeben` hat eine Bestellung erzeugt, `abgesagt` ist beendet."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"vergebenAn":{"type":["string","null"],"description":"Lieferant, der den Zuschlag bekam. `null`, solange nicht vergeben."},"vergebenAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Vergabe. `null`, solange nicht vergeben."},"bestellungId":{"type":["string","null"],"description":"Die bei der Vergabe erzeugte Bestellung. `null`, solange nicht vergeben."}},"required":["id","rfqNr","artikelId","menge","lieferanten","deadline","status","createdAt","vergebenAn","vergebenAm","bestellungId"],"additionalProperties":false},"description":"Die Anfragen, neueste zuerst. OHNE ihre Angebote."},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angeforderte Seitengroesse."},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege."},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer unter demselben Filter, unabhaengig von der Seite."}},"required":["limit","offset","total"],"additionalProperties":false,"description":"Seitenangaben zur Abfrage."}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","rfqNr":"string","artikelId":"string","menge":0,"lieferanten":["string"],"deadline":"stringxxxx","status":"offen","createdAt":"2026-01-01T12:00:00.000Z","vergebenAn":"string","vergebenAm":"2026-01-01T12:00:00.000Z","bestellungId":"string"}],"pagination":{"limit":1,"offset":0,"total":0}}}}},"400":{"description":"Ungueltige Abfrageparameter.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer deutscher Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar oder das Anlegen der Tabellen schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1EinkaufRfq","tags":["einkauf"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["offen","abgeschlossen","vergeben","abgesagt"]}}],"summary":"Anfragen auflisten","description":"Liste der RFQs"},"post":{"responses":{"201":{"description":"Die angelegte Anfrage, unverpackt. `rfqNr` und `status: \"offen\"` setzt der Server.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Anfrage (UUID als Text, vom Server vergeben)"},"rfqNr":{"type":"string","minLength":1,"description":"Sprechende Nummer der Anfrage, vom Server vergeben."},"artikelId":{"type":"string","minLength":1,"description":"Angefragter Artikel."},"menge":{"type":"number","description":"Angefragte Menge, bis zu vier Nachkommastellen."},"lieferanten":{"type":"array","items":{"type":"string"},"description":"Die angefragten Lieferanten. Mindestens einer, beim Anlegen festgelegt."},"deadline":{"type":"string","minLength":10,"description":"Frist fuer die Angebote. In der Datenbank ein reiner Kalendertag (Spaltentyp DATE); in der Antwort steht die Zeichenkette, die der Treiber daraus macht."},"status":{"type":"string","enum":["offen","abgeschlossen","vergeben","abgesagt"],"description":"Zustand: `offen` nimmt Angebote an, `abgeschlossen` nicht mehr (kann aber noch vergeben werden), `vergeben` hat eine Bestellung erzeugt, `abgesagt` ist beendet."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"vergebenAn":{"type":["string","null"],"description":"Lieferant, der den Zuschlag bekam. `null`, solange nicht vergeben."},"vergebenAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Vergabe. `null`, solange nicht vergeben."},"bestellungId":{"type":["string","null"],"description":"Die bei der Vergabe erzeugte Bestellung. `null`, solange nicht vergeben."}},"required":["id","rfqNr","artikelId","menge","lieferanten","deadline","status","createdAt","vergebenAn","vergebenAm","bestellungId"],"additionalProperties":false},"example":{"id":"string","rfqNr":"string","artikelId":"string","menge":0,"lieferanten":["string"],"deadline":"stringxxxx","status":"offen","createdAt":"2026-01-01T12:00:00.000Z","vergebenAn":"string","vergebenAm":"2026-01-01T12:00:00.000Z","bestellungId":"string"}}}},"400":{"description":"Eingabe ungueltig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer deutscher Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle oder Modulrecht reichen nicht (Antwort der Rechte-Schicht)."},"503":{"description":"Datenbank nicht erreichbar oder das INSERT schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufRfq","tags":["einkauf"],"parameters":[],"summary":"Anfrage anlegen","description":"Erzeugt neue RFQ","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"artikel_id":{"type":"string","minLength":1},"menge":{"type":"number","exclusiveMinimum":0},"lieferanten":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"deadline":{"type":"string","format":"date"}},"required":["artikel_id","menge","lieferanten","deadline"]},"example":{"artikel_id":"string","menge":1,"lieferanten":["string"],"deadline":"2026-01-01"}}}}}},"/api/v1/einkauf/rfq/{id}":{"get":{"responses":{"200":{"description":"Die Anfrage mit ihren Angeboten. Die Angebote stehen unter `angebote` DIREKT im Anfrage-Objekt, nicht in einem eigenen Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Anfrage (UUID als Text, vom Server vergeben)"},"rfqNr":{"type":"string","minLength":1,"description":"Sprechende Nummer der Anfrage, vom Server vergeben."},"artikelId":{"type":"string","minLength":1,"description":"Angefragter Artikel."},"menge":{"type":"number","description":"Angefragte Menge, bis zu vier Nachkommastellen."},"lieferanten":{"type":"array","items":{"type":"string"},"description":"Die angefragten Lieferanten. Mindestens einer, beim Anlegen festgelegt."},"deadline":{"type":"string","minLength":10,"description":"Frist fuer die Angebote. In der Datenbank ein reiner Kalendertag (Spaltentyp DATE); in der Antwort steht die Zeichenkette, die der Treiber daraus macht."},"status":{"type":"string","enum":["offen","abgeschlossen","vergeben","abgesagt"],"description":"Zustand: `offen` nimmt Angebote an, `abgeschlossen` nicht mehr (kann aber noch vergeben werden), `vergeben` hat eine Bestellung erzeugt, `abgesagt` ist beendet."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"vergebenAn":{"type":["string","null"],"description":"Lieferant, der den Zuschlag bekam. `null`, solange nicht vergeben."},"vergebenAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Vergabe. `null`, solange nicht vergeben."},"bestellungId":{"type":["string","null"],"description":"Die bei der Vergabe erzeugte Bestellung. `null`, solange nicht vergeben."},"angebote":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Angebots (UUID als Text, vom Server vergeben)"},"rfqId":{"type":"string","minLength":1,"description":"Anfrage, zu der das Angebot gehoert."},"lieferantId":{"type":"string","minLength":1,"description":"Der bietende Lieferant. Je Anfrage ist nur ein Angebot pro Lieferant erlaubt."},"preis":{"type":"number","description":"Gebotener Preis, bis zu vier Nachkommastellen."},"lieferzeitTage":{"type":["integer","null"],"description":"Zugesagte Lieferzeit in Tagen. `null`, wenn keine genannt wurde."},"gueltigBis":{"type":["string","null"],"description":"Bindefrist des Angebots als Kalendertag. `null`, wenn keine genannt wurde."},"notes":{"type":["string","null"],"description":"Freitext des Lieferanten. `null`, wenn keiner erfasst ist."},"eingegangenAm":{"type":"string","format":"date-time","description":"Zeitpunkt des Eingangs als ISO-8601-Zeitstempel in UTC."}},"required":["id","rfqId","lieferantId","preis","lieferzeitTage","gueltigBis","notes","eingegangenAm"],"additionalProperties":false},"description":"Die eingegangenen Angebote, guenstigstes zuerst, bei Gleichstand aelteres zuerst."}},"required":["id","rfqNr","artikelId","menge","lieferanten","deadline","status","createdAt","vergebenAn","vergebenAm","bestellungId","angebote"],"additionalProperties":false},"example":{"id":"string","rfqNr":"string","artikelId":"string","menge":0,"lieferanten":["string"],"deadline":"stringxxxx","status":"offen","createdAt":"2026-01-01T12:00:00.000Z","vergebenAn":"string","vergebenAm":"2026-01-01T12:00:00.000Z","bestellungId":"string","angebote":[{"id":"string","rfqId":"string","lieferantId":"string","preis":0,"lieferzeitTage":0,"gueltigBis":"string","notes":"string","eingegangenAm":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Keine Anfrage mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rfq_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das Anlegen der Tabellen schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1EinkaufRfqById","tags":["einkauf"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Anfrage mit Angeboten lesen","description":"Detail einer RFQ inkl. Angebote"}},"/api/v1/einkauf/rfq/{id}/angebote":{"post":{"responses":{"201":{"description":"Das erfasste Angebot, unverpackt.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Angebots (UUID als Text, vom Server vergeben)"},"rfqId":{"type":"string","minLength":1,"description":"Anfrage, zu der das Angebot gehoert."},"lieferantId":{"type":"string","minLength":1,"description":"Der bietende Lieferant. Je Anfrage ist nur ein Angebot pro Lieferant erlaubt."},"preis":{"type":"number","description":"Gebotener Preis, bis zu vier Nachkommastellen."},"lieferzeitTage":{"type":["integer","null"],"description":"Zugesagte Lieferzeit in Tagen. `null`, wenn keine genannt wurde."},"gueltigBis":{"type":["string","null"],"description":"Bindefrist des Angebots als Kalendertag. `null`, wenn keine genannt wurde."},"notes":{"type":["string","null"],"description":"Freitext des Lieferanten. `null`, wenn keiner erfasst ist."},"eingegangenAm":{"type":"string","format":"date-time","description":"Zeitpunkt des Eingangs als ISO-8601-Zeitstempel in UTC."}},"required":["id","rfqId","lieferantId","preis","lieferzeitTage","gueltigBis","notes","eingegangenAm"],"additionalProperties":false},"example":{"id":"string","rfqId":"string","lieferantId":"string","preis":0,"lieferzeitTage":0,"gueltigBis":"string","notes":"string","eingegangenAm":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Eingabe ungueltig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer deutscher Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle oder Modulrecht reichen nicht (Antwort der Rechte-Schicht)."},"404":{"description":"Keine Anfrage mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rfq_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"ZWEI FORMEN: steht die Anfrage nicht auf `offen`, kommt `invalid_status:<Zustand>`. Hat derselbe Lieferant schon geboten, kommt `angebot_already_exists`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","pattern":"^invalid_status:","description":"Schluessel und Ist-Zustand in EINEM Feld, getrennt durch einen Doppelpunkt — z. B. `invalid_status:vergeben`."}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"angebot_already_exists","description":"Fester Fehlerschluessel. Je Anfrage ist ein Angebot pro Lieferant erlaubt."}},"required":["error"],"additionalProperties":false}]}}}},"503":{"description":"Datenbank nicht erreichbar oder das INSERT schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufRfqByIdAngebote","tags":["einkauf"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Angebot zu einer Anfrage erfassen","description":"Fügt ein Angebot zur RFQ hinzu","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lieferant_id":{"type":"string","minLength":1},"preis":{"type":"number","exclusiveMinimum":0},"lieferzeit_tage":{"type":"integer","minimum":0},"gueltig_bis":{"type":"string","format":"date"},"notes":{"type":"string","maxLength":1000}},"required":["lieferant_id","preis"]},"example":{"lieferant_id":"string","preis":1,"lieferzeit_tage":0,"gueltig_bis":"2026-01-01","notes":"string"}}}}}},"/api/v1/einkauf/rfq/{id}/award":{"post":{"responses":{"201":{"description":"Der Zuschlag ist erteilt: die Anfrage steht auf `vergeben`, und es gibt eine neue Bestellung im Zustand `draft`. Beides entsteht in EINER Transaktion — es kann keine Bestellung ohne vergebene Anfrage geben.","content":{"application/json":{"schema":{"type":"object","properties":{"rfq":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Anfrage (UUID als Text, vom Server vergeben)"},"rfqNr":{"type":"string","minLength":1,"description":"Sprechende Nummer der Anfrage, vom Server vergeben."},"artikelId":{"type":"string","minLength":1,"description":"Angefragter Artikel."},"menge":{"type":"number","description":"Angefragte Menge, bis zu vier Nachkommastellen."},"lieferanten":{"type":"array","items":{"type":"string"},"description":"Die angefragten Lieferanten. Mindestens einer, beim Anlegen festgelegt."},"deadline":{"type":"string","minLength":10,"description":"Frist fuer die Angebote. In der Datenbank ein reiner Kalendertag (Spaltentyp DATE); in der Antwort steht die Zeichenkette, die der Treiber daraus macht."},"status":{"type":"string","enum":["offen","abgeschlossen","vergeben","abgesagt"],"description":"Zustand: `offen` nimmt Angebote an, `abgeschlossen` nicht mehr (kann aber noch vergeben werden), `vergeben` hat eine Bestellung erzeugt, `abgesagt` ist beendet."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"vergebenAn":{"type":["string","null"],"description":"Lieferant, der den Zuschlag bekam. `null`, solange nicht vergeben."},"vergebenAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Vergabe. `null`, solange nicht vergeben."},"bestellungId":{"type":["string","null"],"description":"Die bei der Vergabe erzeugte Bestellung. `null`, solange nicht vergeben."}},"required":["id","rfqNr","artikelId","menge","lieferanten","deadline","status","createdAt","vergebenAn","vergebenAm","bestellungId"],"additionalProperties":false,"description":"Die Anfrage nach der Vergabe — `status` steht jetzt auf `vergeben`."},"bestellungId":{"type":"string","minLength":1,"description":"Kennung der erzeugten Bestellung."},"orderNumber":{"type":"string","pattern":"^PO-\\d{4}-\\d{4}$","description":"Nummer der erzeugten Bestellung im Muster `PO-JAHR-NNNN`."},"gewinner":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Angebots (UUID als Text, vom Server vergeben)"},"rfqId":{"type":"string","minLength":1,"description":"Anfrage, zu der das Angebot gehoert."},"lieferantId":{"type":"string","minLength":1,"description":"Der bietende Lieferant. Je Anfrage ist nur ein Angebot pro Lieferant erlaubt."},"preis":{"type":"number","description":"Gebotener Preis, bis zu vier Nachkommastellen."},"lieferzeitTage":{"type":["integer","null"],"description":"Zugesagte Lieferzeit in Tagen. `null`, wenn keine genannt wurde."},"gueltigBis":{"type":["string","null"],"description":"Bindefrist des Angebots als Kalendertag. `null`, wenn keine genannt wurde."},"notes":{"type":["string","null"],"description":"Freitext des Lieferanten. `null`, wenn keiner erfasst ist."},"eingegangenAm":{"type":"string","format":"date-time","description":"Zeitpunkt des Eingangs als ISO-8601-Zeitstempel in UTC."}},"required":["id","rfqId","lieferantId","preis","lieferzeitTage","gueltigBis","notes","eingegangenAm"],"additionalProperties":false,"description":"Das Angebot, das den Zuschlag bekam."}},"required":["rfq","bestellungId","orderNumber","gewinner"],"additionalProperties":false}}}},"400":{"description":"Eingabe ungueltig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer deutscher Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle oder Modulrecht reichen nicht (Antwort der Rechte-Schicht)."},"404":{"description":"ZWEI FORMEN: `rfq_not_found`, wenn es die Anfrage nicht gibt; `angebot_not_found`, wenn es sie gibt, das Angebot aber nicht zu ihr gehoert.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"rfq_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"angebot_not_found","description":"Fester Fehlerschluessel. Das Angebot gehoert nicht zu dieser Anfrage oder fehlt."}},"required":["error"],"additionalProperties":false}]}}}},"409":{"description":"Die Anfrage steht weder auf `offen` noch auf `abgeschlossen` — der Ist-Zustand steht hinter dem Doppelpunkt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","pattern":"^invalid_status:","description":"Schluessel und Ist-Zustand in EINEM Feld, getrennt durch einen Doppelpunkt — z. B. `invalid_status:vergeben`."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"ZWEI FORMEN: fehlt dem Mandanten die Bestelltabelle, kommt `purchase_orders_table_missing` MIT Klartext. Jeder andere Ausfall kommt als `database_unavailable` MIT Wartezeit.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"purchase_orders_table_missing","description":"Fester Fehlerschluessel. Ohne Bestelltabelle kann keine Bestellung entstehen."},"message":{"type":"string","minLength":1,"description":"Deutscher Klartext fuer die Oberflaeche."}},"required":["error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"postApiV1EinkaufRfqByIdAward","tags":["einkauf"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Anfrage vergeben","description":"Vergibt RFQ → erzeugt purchase_order","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"angebot_id":{"type":"string","minLength":1},"notes":{"type":"string","maxLength":1000}},"required":["angebot_id"]},"example":{"angebot_id":"string","notes":"string"}}}}}},"/api/v1/einkauf/rfq/{id}/cancel":{"post":{"responses":{"200":{"description":"Die abgesagte Anfrage samt zurueckgegebener Begruendung. Die Begruendung wird NICHT gespeichert — sie steht nur in dieser einen Antwort.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Anfrage (UUID als Text, vom Server vergeben)"},"rfqNr":{"type":"string","minLength":1,"description":"Sprechende Nummer der Anfrage, vom Server vergeben."},"artikelId":{"type":"string","minLength":1,"description":"Angefragter Artikel."},"menge":{"type":"number","description":"Angefragte Menge, bis zu vier Nachkommastellen."},"lieferanten":{"type":"array","items":{"type":"string"},"description":"Die angefragten Lieferanten. Mindestens einer, beim Anlegen festgelegt."},"deadline":{"type":"string","minLength":10,"description":"Frist fuer die Angebote. In der Datenbank ein reiner Kalendertag (Spaltentyp DATE); in der Antwort steht die Zeichenkette, die der Treiber daraus macht."},"status":{"type":"string","enum":["offen","abgeschlossen","vergeben","abgesagt"],"description":"Zustand: `offen` nimmt Angebote an, `abgeschlossen` nicht mehr (kann aber noch vergeben werden), `vergeben` hat eine Bestellung erzeugt, `abgesagt` ist beendet."},"createdAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Anlage als ISO-8601-Zeitstempel in UTC."},"vergebenAn":{"type":["string","null"],"description":"Lieferant, der den Zuschlag bekam. `null`, solange nicht vergeben."},"vergebenAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Vergabe. `null`, solange nicht vergeben."},"bestellungId":{"type":["string","null"],"description":"Die bei der Vergabe erzeugte Bestellung. `null`, solange nicht vergeben."},"reason":{"type":"string","minLength":1,"description":"Die mitgegebene Begruendung, unveraendert zurueckgegeben. ACHTUNG: sie wird NICHT gespeichert — beim naechsten Lesen der Anfrage ist sie weg."}},"required":["id","rfqNr","artikelId","menge","lieferanten","deadline","status","createdAt","vergebenAn","vergebenAm","bestellungId","reason"],"additionalProperties":false},"example":{"id":"string","rfqNr":"string","artikelId":"string","menge":0,"lieferanten":["string"],"deadline":"stringxxxx","status":"offen","createdAt":"2026-01-01T12:00:00.000Z","vergebenAn":"string","vergebenAm":"2026-01-01T12:00:00.000Z","bestellungId":"string","reason":"string"}}}},"400":{"description":"Begruendung fehlt, ist leer oder laenger als 1000 Zeichen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"message":{"type":"string","description":"Kurzer deutscher Klartext. Er nennt bewusst keine Eingabewerte."},"fields":{"type":"array","items":{"type":"string"},"description":"Die beanstandeten Feldpfade. Der Wurzelfehler steht als `_root`."}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle oder Modulrecht reichen nicht (Antwort der Rechte-Schicht)."},"404":{"description":"Die Anfrage gibt es nicht, ODER sie ist bereits vergeben — beides unter demselben Schluessel. Eine vergebene Anfrage laesst sich nicht mehr absagen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rfq_not_found_or_already_awarded","description":"Fester Fehlerschluessel fuer zwei Faelle: die Anfrage gibt es nicht, ODER sie ist schon vergeben. Welcher davon zutrifft, sagt die Antwort nicht."}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar oder das UPDATE schlug fehl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel zum Auswerten im Programm."},"retryAfter":{"type":"number","const":5,"description":"Vorgeschlagene Wartezeit in Sekunden bis zum naechsten Versuch."}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1EinkaufRfqByIdCancel","tags":["einkauf"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Anfrage absagen","description":"Storniert RFQ","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","minLength":1,"maxLength":1000}},"required":["reason"]},"example":{"reason":"string"}}}}}},"/api/v1/provisionen/statement":{"get":{"responses":{"200":{"description":"Das Abrechnungs-PDF. `Content-Disposition: attachment; filename=\"Provisions-Abrechnung-<Name>-<from>-bis-<to>.pdf\"`. `X-PDF-Engine` nennt den erzeugenden Weg (`cache` bei einem Treffer im Zwischenspeicher, sonst die Render-Engine bzw. `fallback`), `X-PDF-Cache` die Herkunft (`miss` bei frischem Render). Ein ETag liegt nur bei, wenn nicht über den Rückfall gerendert wurde.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"304":{"description":"Not modified — ETag matches"},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Mitarbeiter nicht gefunden — die ID steht nicht in `employees` des Mandanten-Schemas.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"mitarbeiter_not_found"}},"required":["error"]}}}},"500":{"description":"Aufbau des PDF gescheitert; `message` trägt die interne Fehlermeldung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"statement_generation_failed"},"message":{"type":"string"}},"required":["error","message"]}}}},"503":{"description":"Keine Datenbankverbindung. Antwort mit `Retry-After: 5`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1ProvisionenStatement","tags":["Provisionen"],"parameters":[{"in":"query","name":"mitarbeiter_id","schema":{"type":"string","format":"uuid"},"required":true},{"in":"query","name":"from","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"required":true},{"in":"query","name":"to","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"required":true},{"in":"query","name":"label","schema":{"type":"string","maxLength":64},"required":false}],"summary":"Liefert das Provisions-Abrechnungs-PDF eines Mitarbeiters","description":"Liest die Buchungen aus `provisionen_buchungen` des Mandanten-Schemas für einen Mitarbeiter zwischen `from` und `to` (`deleted_at IS NULL`), summiert sie je Status — accrued, approved, paid, clawback — und rechnet daraus die Netto-Auszahlung `approved + paid − clawback`. Alle vier Parameter außer `label` sind Pflicht; `label` überschreibt nur die Zeitraum-Überschrift auf dem Beleg, sonst steht dort der Monatsname oder `von – bis`. Rein lesend: es wird nichts gebucht und nichts am Status geändert.\n\nAusgeliefert wird ein PDF, kein JSON. Das Ergebnis wird zwischengespeichert (`getCachedPdf`/`putCachedPdf`) und trägt ein schwaches ETag; ein passendes `If-None-Match` beantwortet die Route mit 304 und leerem Rumpf. Ein Rückfall-Render (`X-PDF-Engine: fallback`) wird bewusst WEDER gespeichert NOCH mit ETag ausgeliefert, damit ein vorübergehend kaputtes PDF nicht dauerhaft festgeschrieben wird.\n\nMindestrolle `manager`."}},"/api/v1/provisionen/export/datev":{"get":{"responses":{"200":{"description":"Die CSV als Datei, Dateiname `provisionen-<von>-<bis>.csv`. Die Kopfzeilen `X-Provisionen-Rows` und `X-Provisionen-Status` nennen Zeilenzahl und ausgewerteten Status.","content":{"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine HR-Manager-Rolle"},"500":{"description":"Export fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"payroll_export_failed"},"message":{"type":"string"}},"required":["error","message"]}}}},"503":{"description":"Keine Datenbankverbindung — `Retry-After: 5` ist gesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1ProvisionenExportDatev","tags":["Provisionen","datev"],"parameters":[{"in":"query","name":"periode_from","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"required":true},{"in":"query","name":"periode_to","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"required":true},{"in":"query","name":"status","schema":{"type":"string","enum":["paid","approved"],"default":"paid"},"required":false},{"in":"query","name":"lohnart_code","schema":{"type":"string","maxLength":8,"default":"910"},"required":false},{"in":"query","name":"lohnart_label","schema":{"type":"string","maxLength":80},"required":false}],"summary":"Exportiert Provisionen als Lohn-DATEV-CSV","description":"Fasst die Provisionsbuchungen des Zeitraums je Mitarbeiter zu EINER Zeile zusammen (Summe der Betraege, Anzahl der Buchungen in der Bemerkung) und liefert sie als semikolongetrennte CSV mit UTF-8-BOM zum Herunterladen. Gelesen wird nur, es wird nichts geschrieben. Beruecksichtigt werden Buchungen ohne `deleted_at` im Status `paid` oder `approved`.\n\nKEIN JSON im Erfolgsfall — nur die Fehlerfaelle antworten in JSON. Fehlt die Personalnummer, stehen die ersten acht Zeichen der Mitarbeiter-Kennung in der Spalte; fehlt der Name, steht dort \"Mitarbeiter\"."}},"/api/v1/accounting/sachkonten":{"get":{"responses":{"200":{"description":"Liste der Sachkonten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"kontoNr":{},"bezeichnung":{},"kategorie":{},"steuerkennzeichen":{},"istBuchungskonto":{},"saldo":{"type":"number"},"parentKontoNr":{},"isActive":{},"createdAt":{},"updatedAt":{}},"required":["saldo"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"saldo":0}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AccountingSachkonten","tags":["accounting"],"parameters":[{"in":"query","name":"kategorie","schema":{"type":"string","enum":["aktiv","passiv","aufwand","ertrag","neutral"]}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":500,"default":200}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List Sachkonten","description":"Listet die Sachkonten (Kontenrahmen) des Mandanten. Filterbar nach Kategorie und Suchbegriff (Kontonummer oder Bezeichnung)."},"post":{"responses":{"201":{"description":"Sachkonto angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"kontoNr":{},"bezeichnung":{},"kategorie":{},"steuerkennzeichen":{},"istBuchungskonto":{},"saldo":{"type":"number"},"parentKontoNr":{},"isActive":{},"createdAt":{},"updatedAt":{}},"required":["saldo"],"additionalProperties":false},"example":{"saldo":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"409":{"description":"Kontonummer existiert bereits","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"kontoNr":{},"blockers":{"type":"array","items":{}},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1AccountingSachkonten","tags":["accounting"],"parameters":[],"summary":"Create Sachkonto","description":"Legt ein neues Sachkonto im Kontenrahmen an. Eine bereits vergebene Kontonummer wird mit 409 abgelehnt — es wird nichts angelegt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kontoNr":{"type":"string","minLength":1,"maxLength":20},"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"kategorie":{"type":"string","enum":["aktiv","passiv","aufwand","ertrag","neutral"]},"steuerkennzeichen":{"type":"string","maxLength":10},"istBuchungskonto":{"type":"boolean","default":true},"parentKontoNr":{"type":"string","maxLength":20}},"required":["kontoNr","bezeichnung","kategorie"]},"example":{"kontoNr":"string","bezeichnung":"string","kategorie":"aktiv","steuerkennzeichen":"string","istBuchungskonto":true,"parentKontoNr":"string"}}}}}},"/api/v1/accounting/sachkonten/{id}":{"put":{"responses":{"200":{"description":"Sachkonto aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"kontoNr":{},"bezeichnung":{},"kategorie":{},"steuerkennzeichen":{},"istBuchungskonto":{},"saldo":{"type":"number"},"parentKontoNr":{},"isActive":{},"createdAt":{},"updatedAt":{}},"required":["saldo"],"additionalProperties":false},"example":{"saldo":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Sachkonto nicht gefunden"}},"operationId":"putApiV1AccountingSachkontenById","tags":["accounting"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update Sachkonto","description":"Aktualisiert ein bestehendes Sachkonto. Teil-Update: nur die übergebenen Felder werden geschrieben. Enthält der Rumpf kein einziges Feld, antwortet die Route mit 400 (`no_fields_to_update`).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"kategorie":{"type":"string","enum":["aktiv","passiv","aufwand","ertrag","neutral"]},"steuerkennzeichen":{"type":["string","null"],"maxLength":10},"istBuchungskonto":{"type":"boolean"},"parentKontoNr":{"type":["string","null"],"maxLength":20},"isActive":{"type":"boolean"}}},"example":{"bezeichnung":"string","kategorie":"aktiv","steuerkennzeichen":"string","istBuchungskonto":true,"parentKontoNr":"string","isActive":true}}}}},"delete":{"responses":{"200":{"description":"Sachkonto gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Sachkonto nicht gefunden"},"409":{"description":"Auf dem Konto liegen Buchungen — nicht löschbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"kontoNr":{},"blockers":{"type":"array","items":{}},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1AccountingSachkontenById","tags":["accounting"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete Sachkonto","description":"Löscht ein Sachkonto endgültig (kein Soft-Delete). Liegt mindestens eine Buchung auf dem Konto — als Soll- oder als Habenkonto —, wird mit 409 abgelehnt und nichts gelöscht."}},"/api/v1/accounting/sachkonten/seed":{"post":{"responses":{"201":{"description":"Kontenrahmen angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"inserted":{"type":"number"},"skipped":{"type":"number"},"total":{"type":"number"}},"required":["message","inserted","skipped","total"],"additionalProperties":false},"example":{"message":"string","inserted":0,"skipped":0,"total":0}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"postApiV1AccountingSachkontenSeed","tags":["accounting"],"parameters":[],"summary":"Seed SKR03 Kontenrahmen","description":"Legt die SKR03-Standardkonten im Mandanten an. Bereits vorhandene Kontonummern bleiben unverändert und zählen als `skipped`. Achtung: der Handler antwortet mit HTTP 200, nicht mit 201."}},"/api/v1/accounting/buchungen":{"get":{"responses":{"200":{"description":"Liste der Buchungen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"buchungsnr":{},"buchungsdatum":{},"belegdatum":{},"belegnr":{},"buchungstext":{},"sollKonto":{},"habenKonto":{},"betrag":{"type":"number"},"steuerbetrag":{"type":"number"},"steuerkennzeichen":{},"waehrung":{},"referenceTyp":{},"referenceId":{},"storniert":{},"createdBy":{},"createdAt":{}},"required":["betrag","steuerbetrag"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"betrag":0,"steuerbetrag":0}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AccountingBuchungen","tags":["accounting"],"parameters":[{"in":"query","name":"konto","schema":{"type":"string"}},{"in":"query","name":"dateFrom","schema":{"type":"string","format":"date"}},{"in":"query","name":"dateTo","schema":{"type":"string","format":"date"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":500,"default":100}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List Buchungen","description":"Listet die manuellen Buchungen des Mandanten, neueste zuerst. Filterbar nach Konto (Soll oder Haben) und Buchungsdatum-Bereich."},"post":{"responses":{"201":{"description":"Buchung angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"buchungsnr":{},"buchungsdatum":{},"belegdatum":{},"belegnr":{},"buchungstext":{},"sollKonto":{},"habenKonto":{},"betrag":{"type":"number"},"steuerbetrag":{"type":"number"},"steuerkennzeichen":{},"waehrung":{},"referenceTyp":{},"referenceId":{},"storniert":{},"createdBy":{},"createdAt":{}},"required":["betrag","steuerbetrag"],"additionalProperties":false},"example":{"betrag":0,"steuerbetrag":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"423":{"description":"Buchungsperiode geschlossen — nichts gebucht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"kontoNr":{},"blockers":{"type":"array","items":{}},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1AccountingBuchungen","tags":["accounting"],"parameters":[],"summary":"Create Buchung","description":"Erfasst eine neue Buchung (Soll/Haben) und schreibt die Salden beider Konten in derselben Transaktion fort. Unbekanntes Soll- oder Habenkonto → 400. Anschließend wird der Satz ins Universal-Journal gespiegelt: Ist die Buchungsperiode geschlossen, antwortet die Route mit 423 — die Buchung selbst ist zu diesem Zeitpunkt bereits geschrieben. Scheitert die Spiegelung aus einem anderen Grund, antwortet die Route mit 201, ohne dass ein Journaleintrag entstanden ist.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"buchungsdatum":{"type":"string","format":"date"},"belegdatum":{"type":"string","format":"date"},"belegnr":{"type":"string","maxLength":50},"buchungstext":{"type":"string","minLength":1,"maxLength":500},"sollKonto":{"type":"string","minLength":1,"maxLength":20},"habenKonto":{"type":"string","minLength":1,"maxLength":20},"betrag":{"type":"number","exclusiveMinimum":0},"steuerbetrag":{"type":"number","minimum":0,"default":0},"steuerkennzeichen":{"type":"string","maxLength":10},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"referenceTyp":{"type":"string","enum":["invoice","eingangsrechnung","kasse","manuell"]},"referenceId":{"type":"string"}},"required":["buchungstext","sollKonto","habenKonto","betrag"]},"example":{"buchungsdatum":"2026-01-01","belegdatum":"2026-01-01","belegnr":"string","buchungstext":"string","sollKonto":"string","habenKonto":"string","betrag":1,"steuerbetrag":0,"steuerkennzeichen":"string","waehrung":"str","referenceTyp":"invoice","referenceId":"string"}}}}}},"/api/v1/accounting/buchungen/summary":{"get":{"responses":{"200":{"description":"Buchungs-Summary","content":{"application/json":{"schema":{"type":"object","properties":{"year":{"type":"number"},"expenseYear":{"type":"number"},"revenueYear":{"type":"number"},"countMonth":{"type":"number"},"countToday":{"type":"number"},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["year","expenseYear","revenueYear","countMonth","countToday","meta"],"additionalProperties":false},"example":{"year":0,"expenseYear":0,"revenueYear":0,"countMonth":0,"countToday":0,"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AccountingBuchungenSummary","tags":["accounting"],"parameters":[{"in":"query","name":"year","schema":{"type":"integer","minimum":2000,"maximum":2999}}],"summary":"Get Buchungen summary","description":"Aggregierte Hauptbuch-KPIs (Aufwand/Ertrag Jahr, Buchungen Monat) über ALLE Buchungen der Periode — nicht über die paginierte Liste. Quelle ist das Universal-Journal, die Klassifizierung als Aufwand oder Ertrag kommt aus der Kategorie des Sachkontos."}},"/api/v1/accounting/buchungen/{id}/storno":{"post":{"responses":{"201":{"description":"Storno-Buchung angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"buchungsnr":{},"buchungsdatum":{},"belegdatum":{},"belegnr":{},"buchungstext":{},"sollKonto":{},"habenKonto":{},"betrag":{"type":"number"},"steuerbetrag":{"type":"number"},"steuerkennzeichen":{},"waehrung":{},"referenceTyp":{},"referenceId":{},"storniert":{},"createdBy":{},"createdAt":{}},"required":["betrag","steuerbetrag"],"additionalProperties":false},"example":{"betrag":0,"steuerbetrag":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Buchung nicht gefunden"},"409":{"description":"Buchung ist bereits storniert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"kontoNr":{},"blockers":{"type":"array","items":{}},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1AccountingBuchungenByIdStorno","tags":["accounting"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Storno a Buchung","description":"Erstellt eine Storno-Buchung zur ursprünglichen Buchung. GoBD: die ursprüngliche Buchung wird weder geändert noch gelöscht — sie wird als storniert markiert, die Gegenbuchung (Soll und Haben getauscht) entsteht als neuer Satz, und beide Salden werden zurückgedreht. Eine bereits stornierte Buchung wird mit 409 abgelehnt."}},"/api/v1/accounting/journal":{"get":{"responses":{"200":{"description":"Journal-Einträge","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"buchungsnr":{},"buchungsdatum":{},"belegdatum":{},"belegnr":{},"buchungstext":{},"sollKonto":{},"habenKonto":{},"betrag":{"type":"number"},"steuerbetrag":{"type":"number"},"steuerkennzeichen":{},"waehrung":{},"referenceTyp":{},"referenceId":{},"storniert":{},"createdBy":{},"createdAt":{}},"required":["betrag","steuerbetrag"],"additionalProperties":false}},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"betrag":0,"steuerbetrag":0}],"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AccountingJournal","tags":["accounting"],"parameters":[{"in":"query","name":"konto","schema":{"type":"string"}},{"in":"query","name":"dateFrom","schema":{"type":"string","format":"date"}},{"in":"query","name":"dateTo","schema":{"type":"string","format":"date"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":500,"default":100}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List journal entries","description":"Liefert das Buchungsjournal chronologisch aufsteigend, filterbar nach Datumsbereich. Quelle ist die Tabelle der manuellen Buchungen, nicht das Universal-Journal. `limit`/`offset` wirken, die Antwort trägt aber keinen `pagination`-Block."}},"/api/v1/accounting/hauptbuch/{kontoNr}":{"get":{"responses":{"200":{"description":"Hauptbuch-Daten","content":{"application/json":{"schema":{"type":"object","properties":{"sachkonto":{"type":"object","properties":{"id":{},"tenantId":{},"kontoNr":{},"bezeichnung":{},"kategorie":{},"steuerkennzeichen":{},"istBuchungskonto":{},"saldo":{"type":"number"},"parentKontoNr":{},"isActive":{},"createdAt":{},"updatedAt":{}},"required":["saldo"],"additionalProperties":false},"eintraege":{"type":"array","items":{"type":"object","additionalProperties":{}}},"endsaldo":{"type":"number"},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["sachkonto","eintraege","endsaldo","meta"],"additionalProperties":false},"example":{"sachkonto":{"saldo":0},"eintraege":[{}],"endsaldo":0,"meta":{"source":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Konto nicht gefunden"}},"operationId":"getApiV1AccountingHauptbuchByKontoNr","tags":["accounting"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"kontoNr","required":true}],"summary":"Get Hauptbuch for one Sachkonto","description":"Kontoauszug eines Sachkontos: alle Buchungen chronologisch, je Zeile die Seite (soll/haben) und der laufende Saldo, dazu der Endsaldo."}},"/api/v1/accounting/bankkonten":{"get":{"responses":{"200":{"description":"Liste der Bankkonten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"bezeichnung":{},"iban":{},"bic":{},"bankName":{},"sachkontoNr":{},"saldo":{"type":"number"},"waehrung":{},"isActive":{},"createdAt":{}},"required":["saldo"],"additionalProperties":false}},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"saldo":0}],"meta":{"source":"string"}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AccountingBankkonten","tags":["accounting"],"parameters":[],"summary":"List Bankkonten","description":"Listet die aktiven Bankkonten des Mandanten. Deaktivierte Konten erscheinen nicht; die Antwort trägt keinen `pagination`-Block."},"post":{"responses":{"201":{"description":"Bankkonto angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"bezeichnung":{},"iban":{},"bic":{},"bankName":{},"sachkontoNr":{},"saldo":{"type":"number"},"waehrung":{},"isActive":{},"createdAt":{}},"required":["saldo"],"additionalProperties":false},"example":{"saldo":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"}},"operationId":"postApiV1AccountingBankkonten","tags":["accounting"],"parameters":[],"summary":"Create Bankkonto","description":"Legt ein neues Bankkonto an und verknüpft es optional mit einem Sachkonto.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"iban":{"type":"string","maxLength":34},"bic":{"type":"string","maxLength":11},"bankName":{"type":"string","maxLength":255},"sachkontoNr":{"type":"string","maxLength":20},"saldo":{"type":"number","default":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"}},"required":["bezeichnung"]},"example":{"bezeichnung":"string","iban":"string","bic":"string","bankName":"string","sachkontoNr":"string","saldo":0,"waehrung":"str"}}}}}},"/api/v1/accounting/bankkonten/{id}":{"put":{"responses":{"200":{"description":"Bankkonto aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"bezeichnung":{},"iban":{},"bic":{},"bankName":{},"sachkontoNr":{},"saldo":{"type":"number"},"waehrung":{},"isActive":{},"createdAt":{}},"required":["saldo"],"additionalProperties":false},"example":{"saldo":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Bankkonto nicht gefunden"}},"operationId":"putApiV1AccountingBankkontenById","tags":["accounting"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update Bankkonto","description":"Aktualisiert ein bestehendes Bankkonto. Teil-Update: nur die übergebenen Felder werden geschrieben. Enthält der Rumpf kein einziges Feld, antwortet die Route mit 400 (`no_fields_to_update`).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"iban":{"type":"string","maxLength":34},"bic":{"type":"string","maxLength":11},"bankName":{"type":"string","maxLength":255},"sachkontoNr":{"type":"string","maxLength":20},"saldo":{"type":"number"},"waehrung":{"type":"string","minLength":3,"maxLength":3},"isActive":{"type":"boolean"}}},"example":{"bezeichnung":"string","iban":"string","bic":"string","bankName":"string","sachkontoNr":"string","saldo":0,"waehrung":"str","isActive":true}}}}},"delete":{"responses":{"200":{"description":"Bankkonto deaktiviert","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}},"required":["deleted","id"],"additionalProperties":false},"example":{"deleted":true,"id":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Bankkonto nicht gefunden"}},"operationId":"deleteApiV1AccountingBankkontenById","tags":["accounting"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Deactivate Bankkonto","description":"Deaktiviert ein Bankkonto (Soft-Delete, is_active=false — GoBD-erhaltend). Der Datensatz bleibt bestehen und referenzierbar, verschwindet aber aus der Liste."}},"/api/v1/accounting/stats":{"get":{"responses":{"200":{"description":"Buchhaltungs-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"sachkontenCount":{"type":"number"},"buchungenThisMonth":{"type":"number"},"gesamtAufwandThisYear":{"type":"number"},"gesamtErtragThisYear":{"type":"number"},"gewinnThisYear":{"type":"number"},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["sachkontenCount","buchungenThisMonth","gesamtAufwandThisYear","gesamtErtragThisYear","gewinnThisYear","meta"],"additionalProperties":false},"example":{"sachkontenCount":0,"buchungenThisMonth":0,"gesamtAufwandThisYear":0,"gesamtErtragThisYear":0,"gewinnThisYear":0,"meta":{"source":"string"}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AccountingStats","tags":["accounting"],"parameters":[],"summary":"Get accounting stats","description":"KPIs der Buchhaltung: Anzahl aktiver Sachkonten, manuelle Buchungen des laufenden Monats sowie Aufwand, Ertrag und Gewinn des laufenden Jahres. Die Jahreswerte kommen aus dem Universal-Journal, die Monatszahl aus den manuellen Buchungen."}},"/api/v1/accounting/periods/{id}/close-checklist":{"get":{"responses":{"200":{"description":"Checklist items","content":{"application/json":{"schema":{"type":"object","properties":{"period_id":{"type":"string"},"ready_to_close":{"type":"boolean"},"items":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["period_id","ready_to_close","items"],"additionalProperties":false},"example":{"period_id":"string","ready_to_close":true,"items":[{}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Period not found"}},"operationId":"getApiV1AccountingPeriodsByIdClose-checklist","tags":["accounting"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get period close checklist","description":"Returns the pre-close checklist status for a period (UI preview). Checks whose table or column is absent in this tenant are reported as `ok` with a reason — `ready_to_close` can therefore be true because a check could not run, not because it passed."}},"/api/v1/accounting/periods/{id}/close-month":{"post":{"responses":{"200":{"description":"Period closed","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"period_id":{"type":"string"},"closed_at":{"type":"string"}},"required":["message","period_id","closed_at"],"additionalProperties":false},"example":{"message":"string","period_id":"string","closed_at":"string"}}}},"400":{"description":"Missing confirm flag"},"401":{"description":"Unauthorized"},"404":{"description":"Period not found"},"412":{"description":"Vorbedingungen nicht erfüllt — `blockers` nennt sie","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"kontoNr":{},"blockers":{"type":"array","items":{}},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1AccountingPeriodsByIdClose-month","tags":["accounting"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Close accounting period","description":"Closes an accounting period (Periodenabschluss) after passing all pre-close checks. Answers HTTP 200, not 201. A single failed check blocks the close with 412 and names the blockers — nothing is written in that case. Once closed the period is locked: later postings into it are refused with 423. The audit-log entry is best-effort and its failure never rolls the close back.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"confirm":{"type":"boolean","const":true}},"required":["confirm"]},"example":{"confirm":true}}}}}},"/api/v1/accounting/opos":{"get":{"responses":{"200":{"description":"Offene Posten zum Stichtag. ACHTUNG: die Positionen haben je nach `type` eine andere Form — Forderungen fuehren `invoice_number` und `belegart`, Verbindlichkeiten stattdessen `rechnungsnummer`. `stichtag` ist ein reines Datum, `faellig_am` ein Zeitstempel. `hint` erscheint nur, wenn die Liste unvollstaendig sein koennte.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"forderungen"},"stichtag":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"total_offen":{"type":"number"},"anzahl_belege":{"type":"number"},"by_partner":{"type":"array","items":{"type":"object","properties":{"partner_id":{"type":["string","null"]},"name":{"type":"null"},"total":{"type":"number"},"anzahl":{"type":"number"}},"required":["partner_id","name","total","anzahl"],"additionalProperties":false}},"hint":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"partner_id":{"type":["string","null"]},"faellig_am":{"type":["string","null"],"format":"date-time"},"betrag_brutto":{"type":"number"},"betrag_offen":{"type":"number"},"days_overdue":{"type":"number"},"invoice_number":{"type":"string"},"belegart":{"type":"string","enum":["rechnung","korrektur"]}},"required":["id","partner_id","faellig_am","betrag_brutto","betrag_offen","days_overdue","invoice_number","belegart"],"additionalProperties":false}}},"required":["type","stichtag","total_offen","anzahl_belege","by_partner","items"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"verbindlichkeiten"},"stichtag":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"total_offen":{"type":"number"},"anzahl_belege":{"type":"number"},"by_partner":{"type":"array","items":{"type":"object","properties":{"partner_id":{"type":["string","null"]},"name":{"type":"null"},"total":{"type":"number"},"anzahl":{"type":"number"}},"required":["partner_id","name","total","anzahl"],"additionalProperties":false}},"hint":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"partner_id":{"type":["string","null"]},"faellig_am":{"type":["string","null"],"format":"date-time"},"betrag_brutto":{"type":"number"},"betrag_offen":{"type":"number"},"days_overdue":{"type":"number"},"rechnungsnummer":{"type":"string"},"quelle":{"type":"string","enum":["eingangsrechnung","dokument","korrektur"]},"partner_name":{"type":["string","null"]}},"required":["id","partner_id","faellig_am","betrag_brutto","betrag_offen","days_overdue","rechnungsnummer","quelle","partner_name"],"additionalProperties":false}}},"required":["type","stichtag","total_offen","anzahl_belege","by_partner","items"],"additionalProperties":false}]},"example":{"type":"forderungen","stichtag":"2026-01-01","total_offen":0,"anzahl_belege":0,"by_partner":[{"partner_id":"string","name":null,"total":0,"anzahl":0}],"hint":"string","items":[{"id":"string","partner_id":"string","faellig_am":"2026-01-01T12:00:00.000Z","betrag_brutto":0,"betrag_offen":0,"days_overdue":0,"invoice_number":"string","belegart":"rechnung"}]}}}},"400":{"description":"Invalid query params"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1AccountingOpos","tags":["accounting"],"parameters":[{"in":"query","name":"type","schema":{"type":"string","enum":["forderungen","verbindlichkeiten"],"default":"forderungen"}},{"in":"query","name":"stichtag","schema":{"type":"string","format":"date"}},{"in":"query","name":"kunde_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"lieferant_id","schema":{"type":"string","format":"uuid"}}],"summary":"List OPOS (offene Posten)","description":"OPOS list — open receivables (Forderungen) or payables (Verbindlichkeiten) as of a cut-off date, with per-partner totals. Receivables additionally carry still-open quotes as their own rows (`belegart: \"angebot\"`). If the underlying table (invoices / eingangsrechnungen) does not exist in this tenant, the route answers 200 with an empty list and a `hint` — nothing failed and nothing was found; the same `hint` appears when the quotes branch could not be read."}},"/api/v1/accounting/datev-export-v2":{"get":{"responses":{"200":{"description":"DATEV-Export. Je nach Aufruf rohes CSV (text/csv) oder dieser JSON-Umschlag mit `csv_base64` und den Steuerschlüssel-Warnungen.","content":{"application/json":{"schema":{"type":"object","properties":{"meta":{"type":"object","properties":{"format":{"type":"string"},"vom":{},"bis":{},"rows":{"type":"number"},"generated_at":{"type":"string"}},"required":["format","rows","generated_at"]},"steuerschluessel_warnings":{"type":"array","items":{}},"csv_base64":{"type":"string"}},"required":["meta","steuerschluessel_warnings","csv_base64"],"additionalProperties":false},"example":{"meta":{"format":"string","rows":0,"generated_at":"string"},"steuerschluessel_warnings":[],"csv_base64":"string"}}}},"400":{"description":"Invalid query params"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1AccountingDatev-export-v2","tags":["accounting"],"parameters":[{"in":"query","name":"vom","schema":{"type":"string","format":"date"}},{"in":"query","name":"bis","schema":{"type":"string","format":"date"}},{"in":"query","name":"konto","schema":{"type":"string"}}],"summary":"Export DATEV Buchungsstapel","description":"DATEV Buchungsstapel-Export v2 — cost centers, tax-key validation, EXTF format 7.0. With `?raw=1` the route returns the CSV bytes themselves (`text/csv`, Content-Disposition attachment) instead of a JSON body; row and warning counts are then in the `X-DATEV-Rows` and `X-DATEV-Warnings` headers. Without it the same CSV comes back base64-encoded inside the JSON envelope. Stornierte Buchungen are excluded."}},"/api/v1/accounting/abstimmung":{"get":{"responses":{"200":{"description":"Reconciliation result","content":{"application/json":{"schema":{"type":"object","properties":{"konto":{},"periode":{},"summe_soll":{"type":"number"},"summe_haben":{"type":"number"},"saldo":{"type":"number"},"saldo_anfang":{"type":"number"},"saldo_ende":{"type":"number"},"abstimmungs_status":{},"differenz_betrag":{"type":"number"},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["summe_soll","summe_haben","saldo","saldo_anfang","saldo_ende","differenz_betrag","meta"],"additionalProperties":false},"example":{"summe_soll":0,"summe_haben":0,"saldo":0,"saldo_anfang":0,"saldo_ende":0,"differenz_betrag":0,"meta":{"source":"string"}}}}},"400":{"description":"Invalid query params"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1AccountingAbstimmung","tags":["accounting"],"parameters":[{"in":"query","name":"konto","schema":{"type":"string","minLength":1},"required":true},{"in":"query","name":"periode","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$"},"required":true}],"summary":"Reconcile a Sachkonto","description":"Account reconciliation (Abstimmung) for one Sachkonto and one month: Soll/Haben sums, period saldo, opening and closing balance. The recomputed closing balance is compared against the running `sachkonten.saldo`; a deviation of more than one cent is reported as `abstimmungs_status: \"differenz\"` with the amount in `differenz_betrag`."}},"/api/v1/finance/overview":{"get":{"responses":{"200":{"description":"Kennzahlen, Rohaggregate, die letzten Rechnungen und die Umsatzverlaeufe. Ein frischer Mandant ohne Rechnungstabelle bekommt dieselbe Form mit leeren Listen.","content":{"application/json":{"schema":{"type":"object","properties":{"kpis":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"value":{"type":"string"},"change":{"type":"number"},"tone":{"type":"string","enum":["default","danger","success","warning"]},"comparison":{"type":"string"},"trailing":{"type":"string"}},"required":["label","value"],"additionalProperties":false}},"aggregates":{"type":"object","properties":{"openSum":{"type":"number"},"overdueSum":{"type":"number"},"openCount":{"type":"number"},"overdueCount":{"type":"number"},"totalCount":{"type":"number"},"revenueMonth":{"type":"number"},"revenuePrevMonth":{"type":"number"},"paidMonth":{"type":"number"}},"required":["openSum","overdueSum","openCount","overdueCount","totalCount","revenueMonth","revenuePrevMonth","paidMonth"],"additionalProperties":false},"invoices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"customer":{"type":"string"},"amount":{"type":"number"},"dueDate":{"type":"string"},"status":{"type":"string","enum":["paid","open","overdue"]}},"required":["id","number","customer","amount","dueDate","status"],"additionalProperties":false}},"trends":{"type":"object","properties":{"today":{"type":"array","items":{"type":"number"}},"week":{"type":"array","items":{"type":"number"}},"month":{"type":"array","items":{"type":"number"}},"year":{"type":"array","items":{"type":"number"}}},"required":["today","week","month","year"],"additionalProperties":false}},"required":["kpis","aggregates","invoices","trends"],"additionalProperties":false},"example":{"kpis":[{"label":"string","value":"string","change":0,"tone":"default","comparison":"string","trailing":"string"}],"aggregates":{"openSum":0,"overdueSum":0,"openCount":0,"overdueCount":0,"totalCount":0,"revenueMonth":0,"revenuePrevMonth":0,"paidMonth":0},"invoices":[{"id":"string","number":"string","customer":"string","amount":0,"dueDate":"string","status":"paid"}],"trends":{"today":[0],"week":[0],"month":[0],"year":[0]}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Database unavailable"}},"operationId":"getApiV1FinanceOverview","tags":["Finance"],"parameters":[],"description":"Die Uebersichtsseite der Finanzen in einem Aufruf: fertig beschriftete Kennzahl-Kacheln, dieselben Zahlen roh unter `aggregates`, die letzten ZEHN Rechnungen und die Umsatzverlaeufe. WICHTIG fuer eigene Kacheln: `aggregates` rechnet ueber ALLE Rechnungen des Mandanten, `invoices` ist nur ein Ausschnitt von zehn — aus dieser Liste zu summieren ergibt eine andere, falsche Zahl. Entwuerfe und stornierte Belege zaehlen nirgends in den Umsatz. Der Status je Zeile ist NICHT der Belegstatus, sondern auf `paid`, `open` oder `overdue` verdichtet — dabei gilt ein offener Beleg mit vergangener Faelligkeit als ueberfaellig. Die Verlaeufe decken das LAUFENDE Jahr ab, je Periode mit eigenem Raster (24 Stunden, 7 Tage, Tage des Monats, 12 Monate) und in UTC gebildet. Ein Mandant ohne Rechnungstabelle bekommt dieselbe Form mit Nullen und leeren Listen — kein Fehler. Es gibt keine Parameter; Zeitraum und Anzahl sind fest.","summary":"Die Uebersichtsseite der Finanzen in einem Aufruf","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/accounting/perioden":{"get":{"responses":{"200":{"description":"Liste der zwoelf Perioden des Jahres","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":["string","null"],"format":"uuid","description":"Kennung der Periode; null, wenn fuer diesen Monat noch keine Zeile existiert"},"jahr":{"type":"integer","minimum":2000,"maximum":2100,"description":"Kalenderjahr der Periode"},"monat":{"type":"integer","minimum":1,"maximum":12,"description":"Monat der Periode"},"status":{"type":"string","enum":["open","closing","closed"],"description":"Offen, im Abschluss oder geschlossen — auf geschlossene Perioden wird nicht gebucht"},"closedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Schliessens; null wenn nie geschlossen"},"closedBy":{"type":["string","null"],"minLength":1,"description":"Benutzer, der geschlossen hat; null wenn nie geschlossen"},"reopenedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Wiederoeffnens; null wenn nie wieder geoeffnet"},"reopenedBy":{"type":["string","null"],"minLength":1,"description":"Benutzer, der wieder geoeffnet hat; null wenn nie wieder geoeffnet"},"notes":{"type":["string","null"],"maxLength":2000,"description":"Vermerk zum Schliessen oder Oeffnen; null wenn keiner erfasst"}},"required":["id","jahr","monat","status","closedAt","closedBy","reopenedAt","reopenedBy","notes"],"description":"Eine Buchungsperiode (Monat) mit ihrem Sperrzustand"},"minItems":12,"maxItems":12,"description":"Immer alle zwoelf Monate — Monate ohne Zeile werden als offen ergaenzt"},"jahr":{"type":"integer","minimum":2000,"maximum":2100,"description":"Das abgefragte Jahr; ohne Angabe das laufende"}},"required":["data","jahr"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"}],"jahr":2000}}}},"400":{"description":"Ungueltige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AccountingPerioden","tags":["accounting-periods"],"parameters":[{"in":"query","name":"jahr","schema":{"type":"integer","minimum":2000,"maximum":2100}}],"summary":"Liste der zwoelf Buchungsperioden eines Jahres","description":"Liste der 12 Buchungsperioden eines Jahres (auto-fill open falls keine Eintraege)"}},"/api/v1/accounting/perioden/{jahr}/{monat}":{"get":{"responses":{"200":{"description":"Periode — gespeicherter Stand oder ergaenzte offene Periode","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":["string","null"],"format":"uuid","description":"Kennung der Periode; null, wenn fuer diesen Monat noch keine Zeile existiert"},"jahr":{"type":"integer","minimum":2000,"maximum":2100,"description":"Kalenderjahr der Periode"},"monat":{"type":"integer","minimum":1,"maximum":12,"description":"Monat der Periode"},"status":{"type":"string","enum":["open","closing","closed"],"description":"Offen, im Abschluss oder geschlossen — auf geschlossene Perioden wird nicht gebucht"},"closedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Schliessens; null wenn nie geschlossen"},"closedBy":{"type":["string","null"],"minLength":1,"description":"Benutzer, der geschlossen hat; null wenn nie geschlossen"},"reopenedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Wiederoeffnens; null wenn nie wieder geoeffnet"},"reopenedBy":{"type":["string","null"],"minLength":1,"description":"Benutzer, der wieder geoeffnet hat; null wenn nie wieder geoeffnet"},"notes":{"type":["string","null"],"maxLength":2000,"description":"Vermerk zum Schliessen oder Oeffnen; null wenn keiner erfasst"}},"required":["id","jahr","monat","status","closedAt","closedBy","reopenedAt","reopenedBy","notes"],"description":"Die abgefragte Periode"}},"required":["data"]},"example":{"data":{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AccountingPeriodenByJahrByMonat","tags":["accounting-periods"],"parameters":[{"in":"path","name":"jahr","schema":{"type":"integer","minimum":2000,"maximum":2100},"required":true},{"in":"path","name":"monat","schema":{"type":"integer","minimum":1,"maximum":12},"required":true}],"description":"Detail einer Buchungsperiode. Existiert fuer den Monat keine Zeile, kommt trotzdem 200 — mit einer offenen Periode, deren `id` null ist.","summary":"Detail einer Buchungsperiode","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/accounting/perioden/{jahr}/{monat}/close":{"post":{"responses":{"200":{"description":"Periode geschlossen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":["string","null"],"format":"uuid","description":"Kennung der Periode; null, wenn fuer diesen Monat noch keine Zeile existiert"},"jahr":{"type":"integer","minimum":2000,"maximum":2100,"description":"Kalenderjahr der Periode"},"monat":{"type":"integer","minimum":1,"maximum":12,"description":"Monat der Periode"},"status":{"type":"string","enum":["open","closing","closed"],"description":"Offen, im Abschluss oder geschlossen — auf geschlossene Perioden wird nicht gebucht"},"closedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Schliessens; null wenn nie geschlossen"},"closedBy":{"type":["string","null"],"minLength":1,"description":"Benutzer, der geschlossen hat; null wenn nie geschlossen"},"reopenedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Wiederoeffnens; null wenn nie wieder geoeffnet"},"reopenedBy":{"type":["string","null"],"minLength":1,"description":"Benutzer, der wieder geoeffnet hat; null wenn nie wieder geoeffnet"},"notes":{"type":["string","null"],"maxLength":2000,"description":"Vermerk zum Schliessen oder Oeffnen; null wenn keiner erfasst"}},"required":["id","jahr","monat","status","closedAt","closedBy","reopenedAt","reopenedBy","notes"],"description":"Die Periode nach der Aenderung"},"message":{"type":"string","minLength":1,"description":"Ergebnis im Klartext fuer die Oberflaeche"}},"required":["data","message"]},"example":{"data":{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Periode ist bereits geschlossen (Klartext)"}},"operationId":"postApiV1AccountingPeriodenByJahrByMonatClose","tags":["accounting-periods"],"parameters":[{"in":"path","name":"jahr","schema":{"type":"integer","minimum":2000,"maximum":2100},"required":true},{"in":"path","name":"monat","schema":{"type":"integer","minimum":1,"maximum":12},"required":true}],"summary":"Buchungsperiode schliessen (status -> closed)","description":"Legt die Periode an oder aktualisiert die vorhandene (Konflikt auf Jahr und Monat), setzt `status` auf `closed` und haelt Zeitpunkt und Benutzer fest. Eine bereits geschlossene Periode wird mit 409 abgewiesen — dann wird nichts geschrieben. Eine mitgeschickte Notiz ersetzt die bestehende; ohne Notiz bleibt die alte stehen. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string","maxLength":2000}}},"example":{"notes":"string"}}}}}},"/api/v1/accounting/perioden/{jahr}/{monat}/reopen":{"post":{"responses":{"200":{"description":"Periode wieder geoeffnet","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":["string","null"],"format":"uuid","description":"Kennung der Periode; null, wenn fuer diesen Monat noch keine Zeile existiert"},"jahr":{"type":"integer","minimum":2000,"maximum":2100,"description":"Kalenderjahr der Periode"},"monat":{"type":"integer","minimum":1,"maximum":12,"description":"Monat der Periode"},"status":{"type":"string","enum":["open","closing","closed"],"description":"Offen, im Abschluss oder geschlossen — auf geschlossene Perioden wird nicht gebucht"},"closedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Schliessens; null wenn nie geschlossen"},"closedBy":{"type":["string","null"],"minLength":1,"description":"Benutzer, der geschlossen hat; null wenn nie geschlossen"},"reopenedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Wiederoeffnens; null wenn nie wieder geoeffnet"},"reopenedBy":{"type":["string","null"],"minLength":1,"description":"Benutzer, der wieder geoeffnet hat; null wenn nie wieder geoeffnet"},"notes":{"type":["string","null"],"maxLength":2000,"description":"Vermerk zum Schliessen oder Oeffnen; null wenn keiner erfasst"}},"required":["id","jahr","monat","status","closedAt","closedBy","reopenedAt","reopenedBy","notes"],"description":"Die Periode nach der Aenderung"},"message":{"type":"string","minLength":1,"description":"Ergebnis im Klartext fuer die Oberflaeche"}},"required":["data","message"]},"example":{"data":{"id":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"status":"open","closedAt":"2026-01-01T12:00:00.000Z","closedBy":"string","reopenedAt":"2026-01-01T12:00:00.000Z","reopenedBy":"string","notes":"string"},"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Periode ist nicht geschlossen — auch dann, wenn es sie gar nicht gibt (Klartext)"}},"operationId":"postApiV1AccountingPeriodenByJahrByMonatReopen","tags":["accounting-periods"],"parameters":[{"in":"path","name":"jahr","schema":{"type":"integer","minimum":2000,"maximum":2100},"required":true},{"in":"path","name":"monat","schema":{"type":"integer","minimum":1,"maximum":12},"required":true}],"summary":"Buchungsperiode wieder oeffnen (status -> open) — nur Admin","description":"Setzt eine geschlossene Periode zurueck auf `open` und haelt Zeitpunkt und Benutzer der Wiederoeffnung fest; `closedAt` und `closedBy` bleiben dabei stehen. Gibt es die Zeile gar nicht, ist das ebenfalls 409 und nicht 404 — die Begruendung lautet dann, sie sei nie geschlossen gewesen. Dasselbe gilt fuer jede Periode mit einem anderen Status als `closed`. Eine mitgeschickte Notiz ersetzt die bestehende. Erfordert die Rolle Admin.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string","maxLength":2000}}},"example":{"notes":"string"}}}}}},"/api/v1/accounting/susa":{"get":{"responses":{"200":{"description":"SuSa-Daten. Mit format=json (Vorgabe) JSON, mit format=csv eine semikolongetrennte Datei als Anhang.","content":{"application/json":{"schema":{"type":"object","properties":{"vom":{"type":"string","description":"Erster Tag des Zeitraums (YYYY-MM-DD)"},"bis":{"type":"string","description":"Letzter Tag des Zeitraums (YYYY-MM-DD)"},"konten":{"type":"array","items":{"type":"object","properties":{"kontoNr":{"type":"string"},"bezeichnung":{"type":"string"},"kategorie":{"type":"string","description":"aktiv, passiv, aufwand, ertrag oder neutral"},"av":{"type":"number","description":"Anfangsbestand vor dem Zeitraum"},"sollPeriode":{"type":"number"},"habenPeriode":{"type":"number"},"eb":{"type":"number","description":"Endbestand = av + sollPeriode - habenPeriode"}},"required":["kontoNr","bezeichnung","kategorie","av","sollPeriode","habenPeriode","eb"]},"description":"Alle Sachkonten, auch die ohne Bewegung"},"summe":{"type":"object","properties":{"av":{"type":"number"},"soll":{"type":"number"},"haben":{"type":"number"},"eb":{"type":"number"}},"required":["av","soll","haben","eb"]}},"required":["vom","bis","konten","summe"]},"example":{"vom":"string","bis":"string","konten":[{"kontoNr":"string","bezeichnung":"string","kategorie":"string","av":0,"sollPeriode":0,"habenPeriode":0,"eb":0}],"summe":{"av":0,"soll":0,"haben":0,"eb":0}}},"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Forbidden"},"501":{"description":"format=pdf ist nicht umgesetzt — CSV verwenden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1AccountingSusa","tags":["accounting"],"parameters":[{"in":"query","name":"vom","schema":{"type":"string","format":"date","description":"Start of the period. Defaults to 1 January of the current year."}},{"in":"query","name":"bis","schema":{"type":"string","format":"date","description":"End of the period. Defaults to today."}},{"in":"query","name":"format","schema":{"type":"string","enum":["json","csv","pdf"],"default":"json"}},{"in":"query","name":"kategorie","schema":{"type":"string","enum":["aktiv","passiv","aufwand","ertrag","neutral"]}},{"in":"query","name":"dimension_1","schema":{"type":"string","maxLength":40}},{"in":"query","name":"dimension_2","schema":{"type":"string","maxLength":40}}],"summary":"Trial balance per account: opening, period debit/credit and closing","description":"Summen-Salden-Liste: Anfangsbestand, Soll/Haben-Periode und Endbestand je Sachkonto."}},"/api/v1/accounting/susa/export.csv":{"get":{"responses":{"200":{"description":"CSV-Datei als Anhang","content":{"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"Kein Mandantenkontext"},"403":{"description":"Forbidden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1AccountingSusaExport.csv","tags":["accounting"],"parameters":[{"in":"query","name":"vom","schema":{"type":"string","format":"date","description":"Start of the period. Defaults to 1 January of the current year."}},{"in":"query","name":"bis","schema":{"type":"string","format":"date","description":"End of the period. Defaults to today."}},{"in":"query","name":"format","schema":{"type":"string","enum":["json","csv","pdf"],"default":"json"}},{"in":"query","name":"kategorie","schema":{"type":"string","enum":["aktiv","passiv","aufwand","ertrag","neutral"]}},{"in":"query","name":"dimension_1","schema":{"type":"string","maxLength":40}},{"in":"query","name":"dimension_2","schema":{"type":"string","maxLength":40}}],"description":"Convenience-Alias: SuSa als CSV-Download (identisch mit GET /?format=csv). Braucht mindestens die Rolle accountant. Die Antwort ist text/csv als Anhang: semikolongetrennt, mit CRLF-Zeilenenden und deutschen Dezimalkommas, und die letzte Zeile ist die Summenzeile. Der Parameter format wird hier ignoriert — diese Route liefert immer CSV. Ohne vom und bis gilt der 1. Januar des laufenden Jahres bis heute.","summary":"Convenience-Alias: SuSa als CSV-Download (identisch mit GET /?format=csv)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/journal-entries":{"get":{"responses":{"200":{"description":"Liste der Journal-Einträge mit Gesamtzahl und Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Buchung"},"tenant_id":{"type":"string","minLength":1,"description":"Mandant, dem die Buchung gehoert"},"buchungsdatum":{"type":"string","format":"date-time","description":"Buchungsdatum; DATE-Spalte, kommt als ISO-Zeitstempel"},"buchungsnummer":{"type":"string","minLength":1,"description":"Fortlaufende Buchungsnummer der Periode"},"belegdatum":{"type":"string","format":"date-time","description":"Belegdatum; Schluessel FEHLT, wenn keins erfasst ist"},"belegnummer":{"type":"string","maxLength":100,"description":"Belegnummer; Schluessel FEHLT, wenn keine erfasst ist"},"konto_soll":{"type":"string","maxLength":20,"description":"Sollkonto; leerer String, wenn die Spalte null ist"},"konto_haben":{"type":"string","maxLength":20,"description":"Habenkonto; leerer String, wenn die Spalte null ist"},"betrag":{"type":"number","description":"Buchungsbetrag in Belegwaehrung"},"waehrung":{"type":"string","minLength":3,"maxLength":3,"description":"Belegwaehrung nach ISO 4217"},"betrag_eur":{"type":"number","description":"Betrag in EUR; Schluessel FEHLT bei EUR-Buchungen"},"kurs":{"type":"number","exclusiveMinimum":0,"description":"Umrechnungskurs zur Belegwaehrung; 1 bei EUR"},"text":{"type":"string","maxLength":500,"description":"Buchungstext; Schluessel FEHLT, wenn keiner erfasst ist"},"ust_satz":{"type":"number","minimum":0,"maximum":100,"description":"Umsatzsteuersatz in Prozent; Schluessel FEHLT wenn ohne"},"ust_betrag":{"type":"number","minimum":0,"description":"Umsatzsteuerbetrag; Schluessel FEHLT wenn ohne"},"kostenstelle_id":{"type":"string","format":"uuid","description":"Kostenstelle; Schluessel FEHLT wenn nicht zugeordnet"},"kostentraeger_id":{"type":"string","format":"uuid","description":"Kostentraeger; Schluessel FEHLT wenn nicht zugeordnet"},"projekt_id":{"type":"string","format":"uuid","description":"Projekt; Schluessel FEHLT wenn nicht zugeordnet"},"dimension_1":{"type":"string","maxLength":100,"description":"Freie Dimension 1; Schluessel FEHLT wenn leer"},"dimension_2":{"type":"string","maxLength":100,"description":"Freie Dimension 2; Schluessel FEHLT wenn leer"},"dimension_3":{"type":"string","maxLength":100,"description":"Freie Dimension 3; Schluessel FEHLT wenn leer"},"source_module":{"type":"string","enum":["invoice","eingangsrechnung","payroll","inventur","anlage","bank","manual","credit-note","kostenrechnung"],"description":"Modul, aus dem die Buchung stammt; \"manual\" bei Handbuchungen"},"source_document_id":{"type":["string","null"],"description":"Kennung des Ursprungsbelegs; null bei Handbuchungen"},"source_document_no":{"type":["string","null"],"description":"Nummer des Ursprungsbelegs; null bei Handbuchungen"},"posting_group_id":{"type":"string","format":"uuid","description":"Klammer ueber zusammengehoerige Buchungen; Schluessel FEHLT wenn ohne"},"periode_jahr":{"type":"integer","minimum":2000,"maximum":2999,"description":"Jahr der Buchungsperiode"},"periode_monat":{"type":"integer","minimum":1,"maximum":12,"description":"Monat der Buchungsperiode"},"is_storno":{"type":"boolean","description":"true, wenn diese Buchung selbst eine Stornobuchung ist"},"stornierte_buchung_id":{"type":["string","null"],"format":"uuid","description":"Die stornierte Buchung; null bei allen anderen Buchungen"},"created_at":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Buchung"},"created_by":{"type":"string","description":"Benutzer, der gebucht hat; Schluessel FEHLT bei Systembuchungen"}},"required":["id","tenant_id","buchungsdatum","buchungsnummer","konto_soll","konto_haben","betrag","waehrung","kurs","source_module","source_document_id","source_document_no","periode_jahr","periode_monat","is_storno","stornierte_buchung_id","created_at"],"description":"Eine Zeile des Universal-Hauptbuchs"},"description":"Die Buchungen der aktuellen Seite, neueste zuerst"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Buchungen, die dem Filter entsprechen"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":500,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"}},"required":["limit","offset"],"description":"Seitenangaben"}},"required":["entries","total","pagination"]},"example":{"entries":[{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"string","buchungsdatum":"2026-01-01T12:00:00.000Z","buchungsnummer":"string","belegdatum":"2026-01-01T12:00:00.000Z","belegnummer":"string","konto_soll":"string","konto_haben":"string","betrag":0,"waehrung":"str","betrag_eur":0,"kurs":1,"text":"string","ust_satz":0,"ust_betrag":0,"kostenstelle_id":"00000000-0000-4000-8000-000000000000","kostentraeger_id":"00000000-0000-4000-8000-000000000000","projekt_id":"00000000-0000-4000-8000-000000000000","dimension_1":"string","dimension_2":"string","dimension_3":"string","source_module":"invoice","source_document_id":"string","source_document_no":"string","posting_group_id":"00000000-0000-4000-8000-000000000000","periode_jahr":2000,"periode_monat":1,"is_storno":true,"stornierte_buchung_id":"00000000-0000-4000-8000-000000000000","created_at":"2026-01-01T12:00:00.000Z","created_by":"string"}],"total":0,"pagination":{"limit":1,"offset":0}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Journal-entries","tags":["journal-entries"],"parameters":[{"in":"query","name":"konto","schema":{"type":"string"}},{"in":"query","name":"periode_jahr","schema":{"type":"integer","minimum":2000,"maximum":2999}},{"in":"query","name":"periode_monat","schema":{"type":"integer","minimum":1,"maximum":12}},{"in":"query","name":"source_module","schema":{"type":"string","enum":["invoice","eingangsrechnung","payroll","inventur","anlage","bank","manual","credit-note","kostenrechnung"]}},{"in":"query","name":"kostenstelle_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"datum_von","schema":{"type":"string","format":"date"}},{"in":"query","name":"datum_bis","schema":{"type":"string","format":"date"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":500,"default":100}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Listet Journal-Einträge mit Filter und Paginierung","description":"Liest `journal_entries` des Mandanten, sortiert nach Buchungsdatum absteigend und bei Gleichstand nach Anlagezeitpunkt. `konto` trifft Soll ODER Haben; daneben filtern `periode_jahr`, `periode_monat`, `source_module`, `kostenstelle_id` sowie `datum_von` und `datum_bis` auf das Buchungsdatum. `search` sucht als Teiltreffer in Buchungstext, Buchungsnummer, beiden Konten und der Belegnummer der Quelle. Geblättert wird über `limit` (1-500, Standard 100) und `offset`; `total` zählt alle Treffer des Filters, nicht nur die Seite. Der Umschlag heißt `entries`, nicht `data`."},"post":{"responses":{"201":{"description":"Journal-Eintrag angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Buchung"},"tenant_id":{"type":"string","minLength":1,"description":"Mandant, dem die Buchung gehoert"},"buchungsdatum":{"type":"string","format":"date-time","description":"Buchungsdatum; DATE-Spalte, kommt als ISO-Zeitstempel"},"buchungsnummer":{"type":"string","minLength":1,"description":"Fortlaufende Buchungsnummer der Periode"},"belegdatum":{"type":"string","format":"date-time","description":"Belegdatum; Schluessel FEHLT, wenn keins erfasst ist"},"belegnummer":{"type":"string","maxLength":100,"description":"Belegnummer; Schluessel FEHLT, wenn keine erfasst ist"},"konto_soll":{"type":"string","maxLength":20,"description":"Sollkonto; leerer String, wenn die Spalte null ist"},"konto_haben":{"type":"string","maxLength":20,"description":"Habenkonto; leerer String, wenn die Spalte null ist"},"betrag":{"type":"number","description":"Buchungsbetrag in Belegwaehrung"},"waehrung":{"type":"string","minLength":3,"maxLength":3,"description":"Belegwaehrung nach ISO 4217"},"betrag_eur":{"type":"number","description":"Betrag in EUR; Schluessel FEHLT bei EUR-Buchungen"},"kurs":{"type":"number","exclusiveMinimum":0,"description":"Umrechnungskurs zur Belegwaehrung; 1 bei EUR"},"text":{"type":"string","maxLength":500,"description":"Buchungstext; Schluessel FEHLT, wenn keiner erfasst ist"},"ust_satz":{"type":"number","minimum":0,"maximum":100,"description":"Umsatzsteuersatz in Prozent; Schluessel FEHLT wenn ohne"},"ust_betrag":{"type":"number","minimum":0,"description":"Umsatzsteuerbetrag; Schluessel FEHLT wenn ohne"},"kostenstelle_id":{"type":"string","format":"uuid","description":"Kostenstelle; Schluessel FEHLT wenn nicht zugeordnet"},"kostentraeger_id":{"type":"string","format":"uuid","description":"Kostentraeger; Schluessel FEHLT wenn nicht zugeordnet"},"projekt_id":{"type":"string","format":"uuid","description":"Projekt; Schluessel FEHLT wenn nicht zugeordnet"},"dimension_1":{"type":"string","maxLength":100,"description":"Freie Dimension 1; Schluessel FEHLT wenn leer"},"dimension_2":{"type":"string","maxLength":100,"description":"Freie Dimension 2; Schluessel FEHLT wenn leer"},"dimension_3":{"type":"string","maxLength":100,"description":"Freie Dimension 3; Schluessel FEHLT wenn leer"},"source_module":{"type":"string","enum":["invoice","eingangsrechnung","payroll","inventur","anlage","bank","manual","credit-note","kostenrechnung"],"description":"Modul, aus dem die Buchung stammt; \"manual\" bei Handbuchungen"},"source_document_id":{"type":["string","null"],"description":"Kennung des Ursprungsbelegs; null bei Handbuchungen"},"source_document_no":{"type":["string","null"],"description":"Nummer des Ursprungsbelegs; null bei Handbuchungen"},"posting_group_id":{"type":"string","format":"uuid","description":"Klammer ueber zusammengehoerige Buchungen; Schluessel FEHLT wenn ohne"},"periode_jahr":{"type":"integer","minimum":2000,"maximum":2999,"description":"Jahr der Buchungsperiode"},"periode_monat":{"type":"integer","minimum":1,"maximum":12,"description":"Monat der Buchungsperiode"},"is_storno":{"type":"boolean","description":"true, wenn diese Buchung selbst eine Stornobuchung ist"},"stornierte_buchung_id":{"type":["string","null"],"format":"uuid","description":"Die stornierte Buchung; null bei allen anderen Buchungen"},"created_at":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Buchung"},"created_by":{"type":"string","description":"Benutzer, der gebucht hat; Schluessel FEHLT bei Systembuchungen"}},"required":["id","tenant_id","buchungsdatum","buchungsnummer","konto_soll","konto_haben","betrag","waehrung","kurs","source_module","source_document_id","source_document_no","periode_jahr","periode_monat","is_storno","stornierte_buchung_id","created_at"],"description":"Eine Zeile des Universal-Hauptbuchs"},"example":{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"string","buchungsdatum":"2026-01-01T12:00:00.000Z","buchungsnummer":"string","belegdatum":"2026-01-01T12:00:00.000Z","belegnummer":"string","konto_soll":"string","konto_haben":"string","betrag":0,"waehrung":"str","betrag_eur":0,"kurs":1,"text":"string","ust_satz":0,"ust_betrag":0,"kostenstelle_id":"00000000-0000-4000-8000-000000000000","kostentraeger_id":"00000000-0000-4000-8000-000000000000","projekt_id":"00000000-0000-4000-8000-000000000000","dimension_1":"string","dimension_2":"string","dimension_3":"string","source_module":"invoice","source_document_id":"string","source_document_no":"string","posting_group_id":"00000000-0000-4000-8000-000000000000","periode_jahr":2000,"periode_monat":1,"is_storno":true,"stornierte_buchung_id":"00000000-0000-4000-8000-000000000000","created_at":"2026-01-01T12:00:00.000Z","created_by":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"423":{"description":"Buchungsperiode gesperrt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"period_closed","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext mit der betroffenen Periode"}},"required":["error","message"]}}}},"503":{"description":"Nicht gebucht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Journal-entries","tags":["journal-entries"],"parameters":[],"description":"Manuelle Journal-Buchung erfassen. Die Antwort ist der Eintrag selbst, ohne Umschlag.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"buchungsdatum":{"type":"string","format":"date"},"belegdatum":{"type":"string","format":"date"},"belegnummer":{"type":"string","maxLength":100},"konto_soll":{"type":"string","minLength":1,"maxLength":20},"konto_haben":{"type":"string","minLength":1,"maxLength":20},"betrag":{"type":"number","exclusiveMinimum":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"betrag_eur":{"type":"number","exclusiveMinimum":0},"kurs":{"type":"number","exclusiveMinimum":0,"default":1},"text":{"type":"string","maxLength":500},"ust_satz":{"type":"number","minimum":0,"maximum":100},"ust_betrag":{"type":"number","minimum":0},"kostenstelle_id":{"type":"string","format":"uuid"},"kostentraeger_id":{"type":"string","format":"uuid"},"projekt_id":{"type":"string","format":"uuid"},"dimension_1":{"type":"string","maxLength":100},"dimension_2":{"type":"string","maxLength":100},"dimension_3":{"type":"string","maxLength":100},"posting_group_id":{"type":"string","format":"uuid"}},"required":["buchungsdatum","konto_soll","konto_haben","betrag"]},"example":{"buchungsdatum":"2026-01-01","belegdatum":"2026-01-01","belegnummer":"string","konto_soll":"string","konto_haben":"string","betrag":1,"waehrung":"str","betrag_eur":1,"kurs":1,"text":"string","ust_satz":0,"ust_betrag":0,"kostenstelle_id":"00000000-0000-4000-8000-000000000000","kostentraeger_id":"00000000-0000-4000-8000-000000000000","projekt_id":"00000000-0000-4000-8000-000000000000","dimension_1":"string","dimension_2":"string","dimension_3":"string","posting_group_id":"00000000-0000-4000-8000-000000000000"}}}},"summary":"Manuelle Journal-Buchung erfassen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/journal-entries/summary":{"get":{"responses":{"200":{"description":"Monats-Summary — gezählt über alle Buchungen, nicht über eine Seite","content":{"application/json":{"schema":{"type":"object","properties":{"countMonth":{"type":"integer","minimum":0,"description":"Anzahl Buchungen im laufenden Kalendermonat"},"sumMonth":{"type":"number","description":"Summe der Betraege im laufenden Kalendermonat, Storni eingerechnet"},"countToday":{"type":"integer","minimum":0,"description":"Anzahl Buchungen mit heutigem Buchungsdatum"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, fuer den gezaehlt wurde"},"source":{"type":"string","const":"db","description":"Herkunft der Zahlen — immer direkt aus der Datenbank"}},"required":["tenantId","source"],"description":"Angaben zur Abfrage"}},"required":["countMonth","sumMonth","countToday","meta"]},"example":{"countMonth":0,"sumMonth":0,"countToday":0,"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Journal-entriesSummary","tags":["journal-entries"],"parameters":[],"summary":"Monats-Kennzahlen ueber alle Journal-Eintraege","description":"Aggregierte Monats-KPIs (Anzahl + Summe) über ALLE Journal-Einträge des aktuellen Monats — nicht über die paginierte Liste"}},"/api/v1/journal-entries/{id}":{"get":{"responses":{"200":{"description":"Journal-Eintrag","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Buchung"},"tenant_id":{"type":"string","minLength":1,"description":"Mandant, dem die Buchung gehoert"},"buchungsdatum":{"type":"string","format":"date-time","description":"Buchungsdatum; DATE-Spalte, kommt als ISO-Zeitstempel"},"buchungsnummer":{"type":"string","minLength":1,"description":"Fortlaufende Buchungsnummer der Periode"},"belegdatum":{"type":"string","format":"date-time","description":"Belegdatum; Schluessel FEHLT, wenn keins erfasst ist"},"belegnummer":{"type":"string","maxLength":100,"description":"Belegnummer; Schluessel FEHLT, wenn keine erfasst ist"},"konto_soll":{"type":"string","maxLength":20,"description":"Sollkonto; leerer String, wenn die Spalte null ist"},"konto_haben":{"type":"string","maxLength":20,"description":"Habenkonto; leerer String, wenn die Spalte null ist"},"betrag":{"type":"number","description":"Buchungsbetrag in Belegwaehrung"},"waehrung":{"type":"string","minLength":3,"maxLength":3,"description":"Belegwaehrung nach ISO 4217"},"betrag_eur":{"type":"number","description":"Betrag in EUR; Schluessel FEHLT bei EUR-Buchungen"},"kurs":{"type":"number","exclusiveMinimum":0,"description":"Umrechnungskurs zur Belegwaehrung; 1 bei EUR"},"text":{"type":"string","maxLength":500,"description":"Buchungstext; Schluessel FEHLT, wenn keiner erfasst ist"},"ust_satz":{"type":"number","minimum":0,"maximum":100,"description":"Umsatzsteuersatz in Prozent; Schluessel FEHLT wenn ohne"},"ust_betrag":{"type":"number","minimum":0,"description":"Umsatzsteuerbetrag; Schluessel FEHLT wenn ohne"},"kostenstelle_id":{"type":"string","format":"uuid","description":"Kostenstelle; Schluessel FEHLT wenn nicht zugeordnet"},"kostentraeger_id":{"type":"string","format":"uuid","description":"Kostentraeger; Schluessel FEHLT wenn nicht zugeordnet"},"projekt_id":{"type":"string","format":"uuid","description":"Projekt; Schluessel FEHLT wenn nicht zugeordnet"},"dimension_1":{"type":"string","maxLength":100,"description":"Freie Dimension 1; Schluessel FEHLT wenn leer"},"dimension_2":{"type":"string","maxLength":100,"description":"Freie Dimension 2; Schluessel FEHLT wenn leer"},"dimension_3":{"type":"string","maxLength":100,"description":"Freie Dimension 3; Schluessel FEHLT wenn leer"},"source_module":{"type":"string","enum":["invoice","eingangsrechnung","payroll","inventur","anlage","bank","manual","credit-note","kostenrechnung"],"description":"Modul, aus dem die Buchung stammt; \"manual\" bei Handbuchungen"},"source_document_id":{"type":["string","null"],"description":"Kennung des Ursprungsbelegs; null bei Handbuchungen"},"source_document_no":{"type":["string","null"],"description":"Nummer des Ursprungsbelegs; null bei Handbuchungen"},"posting_group_id":{"type":"string","format":"uuid","description":"Klammer ueber zusammengehoerige Buchungen; Schluessel FEHLT wenn ohne"},"periode_jahr":{"type":"integer","minimum":2000,"maximum":2999,"description":"Jahr der Buchungsperiode"},"periode_monat":{"type":"integer","minimum":1,"maximum":12,"description":"Monat der Buchungsperiode"},"is_storno":{"type":"boolean","description":"true, wenn diese Buchung selbst eine Stornobuchung ist"},"stornierte_buchung_id":{"type":["string","null"],"format":"uuid","description":"Die stornierte Buchung; null bei allen anderen Buchungen"},"created_at":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Buchung"},"created_by":{"type":"string","description":"Benutzer, der gebucht hat; Schluessel FEHLT bei Systembuchungen"}},"required":["id","tenant_id","buchungsdatum","buchungsnummer","konto_soll","konto_haben","betrag","waehrung","kurs","source_module","source_document_id","source_document_no","periode_jahr","periode_monat","is_storno","stornierte_buchung_id","created_at"],"description":"Eine Zeile des Universal-Hauptbuchs"},"example":{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"string","buchungsdatum":"2026-01-01T12:00:00.000Z","buchungsnummer":"string","belegdatum":"2026-01-01T12:00:00.000Z","belegnummer":"string","konto_soll":"string","konto_haben":"string","betrag":0,"waehrung":"str","betrag_eur":0,"kurs":1,"text":"string","ust_satz":0,"ust_betrag":0,"kostenstelle_id":"00000000-0000-4000-8000-000000000000","kostentraeger_id":"00000000-0000-4000-8000-000000000000","projekt_id":"00000000-0000-4000-8000-000000000000","dimension_1":"string","dimension_2":"string","dimension_3":"string","source_module":"invoice","source_document_id":"string","source_document_no":"string","posting_group_id":"00000000-0000-4000-8000-000000000000","periode_jahr":2000,"periode_monat":1,"is_storno":true,"stornierte_buchung_id":"00000000-0000-4000-8000-000000000000","created_at":"2026-01-01T12:00:00.000Z","created_by":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"journal_entry_not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Journal-entriesById","tags":["journal-entries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Einzelner Journal-Eintrag. Die Antwort ist der Eintrag selbst, ohne Umschlag.","summary":"Einzelner Journal-Eintrag","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/journal-entries/{id}/storno":{"post":{"responses":{"201":{"description":"Storno-Buchung angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Buchung"},"tenant_id":{"type":"string","minLength":1,"description":"Mandant, dem die Buchung gehoert"},"buchungsdatum":{"type":"string","format":"date-time","description":"Buchungsdatum; DATE-Spalte, kommt als ISO-Zeitstempel"},"buchungsnummer":{"type":"string","minLength":1,"description":"Fortlaufende Buchungsnummer der Periode"},"belegdatum":{"type":"string","format":"date-time","description":"Belegdatum; Schluessel FEHLT, wenn keins erfasst ist"},"belegnummer":{"type":"string","maxLength":100,"description":"Belegnummer; Schluessel FEHLT, wenn keine erfasst ist"},"konto_soll":{"type":"string","maxLength":20,"description":"Sollkonto; leerer String, wenn die Spalte null ist"},"konto_haben":{"type":"string","maxLength":20,"description":"Habenkonto; leerer String, wenn die Spalte null ist"},"betrag":{"type":"number","description":"Buchungsbetrag in Belegwaehrung"},"waehrung":{"type":"string","minLength":3,"maxLength":3,"description":"Belegwaehrung nach ISO 4217"},"betrag_eur":{"type":"number","description":"Betrag in EUR; Schluessel FEHLT bei EUR-Buchungen"},"kurs":{"type":"number","exclusiveMinimum":0,"description":"Umrechnungskurs zur Belegwaehrung; 1 bei EUR"},"text":{"type":"string","maxLength":500,"description":"Buchungstext; Schluessel FEHLT, wenn keiner erfasst ist"},"ust_satz":{"type":"number","minimum":0,"maximum":100,"description":"Umsatzsteuersatz in Prozent; Schluessel FEHLT wenn ohne"},"ust_betrag":{"type":"number","minimum":0,"description":"Umsatzsteuerbetrag; Schluessel FEHLT wenn ohne"},"kostenstelle_id":{"type":"string","format":"uuid","description":"Kostenstelle; Schluessel FEHLT wenn nicht zugeordnet"},"kostentraeger_id":{"type":"string","format":"uuid","description":"Kostentraeger; Schluessel FEHLT wenn nicht zugeordnet"},"projekt_id":{"type":"string","format":"uuid","description":"Projekt; Schluessel FEHLT wenn nicht zugeordnet"},"dimension_1":{"type":"string","maxLength":100,"description":"Freie Dimension 1; Schluessel FEHLT wenn leer"},"dimension_2":{"type":"string","maxLength":100,"description":"Freie Dimension 2; Schluessel FEHLT wenn leer"},"dimension_3":{"type":"string","maxLength":100,"description":"Freie Dimension 3; Schluessel FEHLT wenn leer"},"source_module":{"type":"string","enum":["invoice","eingangsrechnung","payroll","inventur","anlage","bank","manual","credit-note","kostenrechnung"],"description":"Modul, aus dem die Buchung stammt; \"manual\" bei Handbuchungen"},"source_document_id":{"type":["string","null"],"description":"Kennung des Ursprungsbelegs; null bei Handbuchungen"},"source_document_no":{"type":["string","null"],"description":"Nummer des Ursprungsbelegs; null bei Handbuchungen"},"posting_group_id":{"type":"string","format":"uuid","description":"Klammer ueber zusammengehoerige Buchungen; Schluessel FEHLT wenn ohne"},"periode_jahr":{"type":"integer","minimum":2000,"maximum":2999,"description":"Jahr der Buchungsperiode"},"periode_monat":{"type":"integer","minimum":1,"maximum":12,"description":"Monat der Buchungsperiode"},"is_storno":{"type":"boolean","description":"true, wenn diese Buchung selbst eine Stornobuchung ist"},"stornierte_buchung_id":{"type":["string","null"],"format":"uuid","description":"Die stornierte Buchung; null bei allen anderen Buchungen"},"created_at":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Buchung"},"created_by":{"type":"string","description":"Benutzer, der gebucht hat; Schluessel FEHLT bei Systembuchungen"}},"required":["id","tenant_id","buchungsdatum","buchungsnummer","konto_soll","konto_haben","betrag","waehrung","kurs","source_module","source_document_id","source_document_no","periode_jahr","periode_monat","is_storno","stornierte_buchung_id","created_at"],"description":"Eine Zeile des Universal-Hauptbuchs"},"example":{"id":"00000000-0000-4000-8000-000000000000","tenant_id":"string","buchungsdatum":"2026-01-01T12:00:00.000Z","buchungsnummer":"string","belegdatum":"2026-01-01T12:00:00.000Z","belegnummer":"string","konto_soll":"string","konto_haben":"string","betrag":0,"waehrung":"str","betrag_eur":0,"kurs":1,"text":"string","ust_satz":0,"ust_betrag":0,"kostenstelle_id":"00000000-0000-4000-8000-000000000000","kostentraeger_id":"00000000-0000-4000-8000-000000000000","projekt_id":"00000000-0000-4000-8000-000000000000","dimension_1":"string","dimension_2":"string","dimension_3":"string","source_module":"invoice","source_document_id":"string","source_document_no":"string","posting_group_id":"00000000-0000-4000-8000-000000000000","periode_jahr":2000,"periode_monat":1,"is_storno":true,"stornierte_buchung_id":"00000000-0000-4000-8000-000000000000","created_at":"2026-01-01T12:00:00.000Z","created_by":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Journal-Eintrag nicht gefunden (Klartext)"},"409":{"description":"Storno bereits vorhanden — oder die Buchung ist selbst schon ein Storno","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["already_storno","storno_exists"],"description":"already_storno = die Buchung IST ein Storno; storno_exists = es gibt schon eines dazu"},"message":{"type":"string","minLength":1,"description":"Klartext fuer die Oberflaeche"}},"required":["error","message"]}}}},"423":{"description":"Buchungsperiode gesperrt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"period_closed","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext mit der betroffenen Periode"}},"required":["error","message"]}}}},"503":{"description":"Nicht storniert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Journal-entriesByIdStorno","tags":["journal-entries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Storno-Buchung zu einem Journal-Eintrag erstellen. Die Antwort ist die neue Storno-Buchung selbst, ohne Umschlag — nicht die stornierte. Gebucht wird mit dem HEUTIGEN Datum, nicht mit dem der Ursprungsbuchung.","summary":"Storno-Buchung zu einem Journal-Eintrag erstellen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/pipeline/chancen":{"get":{"responses":{"200":{"description":"Liste der Chancen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"bezeichnung":{"type":"string"},"kundeName":{"type":"string"},"kundeId":{},"ansprechpartner":{},"email":{},"telefon":{},"stage":{"type":"string"},"wahrscheinlichkeit":{"type":"number"},"erwarteteEinnahmen":{"type":"number"},"erwartetesAbschlussdatum":{},"quelle":{},"zustaendigerUser":{},"notizen":{},"verlorenGrund":{},"tags":{"type":"array","items":{}},"waehrung":{"type":"string"},"createdAt":{},"updatedAt":{},"closedAt":{},"lastStageChangeAt":{}},"required":["id","bezeichnung","kundeName","stage","wahrscheinlichkeit","erwarteteEinnahmen","tags","waehrung"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","bezeichnung":"string","kundeName":"string","stage":"string","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"tags":[],"waehrung":"string"}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1PipelineChancen","tags":["pipeline"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"stage","schema":{"type":"string","enum":["neu","qualifiziert","angebot","verhandlung","gewonnen","verloren"]}},{"in":"query","name":"kundeId","schema":{"type":"string"}}],"description":"Liest die Verkaufschancen des Mandanten aus `verkaufschancen`, neueste zuerst. `stage` grenzt auf eine der sechs Stufen ein, `kundeId` auf einen Kunden; beide Filter wirken auch auf `pagination.total`, das die Zahl aller Treffer nennt und nicht die der gelieferten Zeilen. `limit` liegt zwischen 1 und 200 und steht ohne Angabe auf 50, `offset` beginnt bei 0. Die Tabelle kennt kein Soft-Delete: was hier fehlt, ist gelöscht und nicht bloß ausgeblendet.","summary":"Liest die Verkaufschancen des Mandanten aus `verkaufschancen`, neueste zuerst","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Chance angelegt — dieselbe Form wie ein Listen-Eintrag","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"bezeichnung":{"type":"string"},"kundeName":{"type":"string"},"kundeId":{},"ansprechpartner":{},"email":{},"telefon":{},"stage":{"type":"string"},"wahrscheinlichkeit":{"type":"number"},"erwarteteEinnahmen":{"type":"number"},"erwartetesAbschlussdatum":{},"quelle":{},"zustaendigerUser":{},"notizen":{},"verlorenGrund":{},"tags":{"type":"array","items":{}},"waehrung":{"type":"string"},"createdAt":{},"updatedAt":{},"closedAt":{},"lastStageChangeAt":{}},"required":["id","bezeichnung","kundeName","stage","wahrscheinlichkeit","erwarteteEinnahmen","tags","waehrung"],"additionalProperties":false},"example":{"id":"string","bezeichnung":"string","kundeName":"string","stage":"string","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"tags":[],"waehrung":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PipelineChancen","tags":["pipeline"],"parameters":[],"description":"Schreibt eine Zeile in `verkaufschancen`. Pflicht sind nur `bezeichnung` und `kundeName`; ohne eigene Angabe startet die Chance in der Stufe `neu` mit 25 % Wahrscheinlichkeit und 0 als erwarteter Einnahme. `kundeId` wird als freier Text übernommen und nicht gegen den Kundenstamm geprüft, eine unbekannte Id fällt hier also nicht auf. Währung (EUR) und die leere Schlagwortliste setzt die Datenbank; beide lassen sich beim Anlegen nicht mitgeben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"kundeName":{"type":"string","minLength":1,"maxLength":255},"kundeId":{"type":"string"},"ansprechpartner":{"type":"string","maxLength":255},"email":{"type":"string","format":"email"},"telefon":{"type":"string","maxLength":50},"stage":{"type":"string","enum":["neu","qualifiziert","angebot","verhandlung","gewonnen","verloren"],"default":"neu"},"wahrscheinlichkeit":{"type":"integer","minimum":0,"maximum":100,"default":25},"erwarteteEinnahmen":{"type":"number","minimum":0,"default":0},"erwartetesAbschlussdatum":{"type":"string","format":"date"},"quelle":{"type":"string","enum":["empfehlung","messe","website","kaltakquise","andere"]},"zustaendigerUser":{"type":"string"},"notizen":{"type":"string"}},"required":["bezeichnung","kundeName"]},"example":{"bezeichnung":"string","kundeName":"string","kundeId":"string","ansprechpartner":"string","email":"beispiel@example.com","telefon":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01","quelle":"empfehlung","zustaendigerUser":"string","notizen":"string"}}}},"summary":"Schreibt eine Zeile in `verkaufschancen`","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/pipeline/chancen/{id}":{"get":{"responses":{"200":{"description":"Chance-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"bezeichnung":{"type":"string"},"kundeName":{"type":"string"},"kundeId":{},"ansprechpartner":{},"email":{},"telefon":{},"stage":{"type":"string"},"wahrscheinlichkeit":{"type":"number"},"erwarteteEinnahmen":{"type":"number"},"erwartetesAbschlussdatum":{},"quelle":{},"zustaendigerUser":{},"notizen":{},"verlorenGrund":{},"tags":{"type":"array","items":{}},"waehrung":{"type":"string"},"createdAt":{},"updatedAt":{},"closedAt":{},"lastStageChangeAt":{},"aktivitaeten":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"chanceId":{},"typ":{"type":"string"},"betreff":{"type":"string"},"inhalt":{},"erledigt":{"type":"boolean"},"faelligAm":{},"userId":{},"createdAt":{}},"required":["id","typ","betreff","erledigt"],"additionalProperties":false}}},"required":["id","bezeichnung","kundeName","stage","wahrscheinlichkeit","erwarteteEinnahmen","tags","waehrung","aktivitaeten"],"additionalProperties":false},"example":{"id":"string","bezeichnung":"string","kundeName":"string","stage":"string","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"tags":[],"waehrung":"string","aktivitaeten":[{"id":"string","typ":"string","betreff":"string","erledigt":true}]}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Chance nicht gefunden"}},"operationId":"getApiV1PipelineChancenById","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liefert eine einzelne Verkaufschance inkl. Aktivitäten. Die Antwort ist das Listen-Item ohne Umschlag, ergänzt um das Feld `aktivitaeten` mit allen Aktivitäten der Chance. Nur lesend; eine unbekannte Kennung ergibt 404 `chance_not_found`.","summary":"Liefert eine einzelne Verkaufschance inkl. Aktivitäten","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Chance aktualisiert — die vollstaendige Chance OHNE `aktivitaeten`. Wer die Aktivitaeten mit will, liest die Detailansicht.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"bezeichnung":{"type":"string"},"kundeName":{"type":"string"},"kundeId":{},"ansprechpartner":{},"email":{},"telefon":{},"stage":{"type":"string"},"wahrscheinlichkeit":{"type":"number"},"erwarteteEinnahmen":{"type":"number"},"erwartetesAbschlussdatum":{},"quelle":{},"zustaendigerUser":{},"notizen":{},"verlorenGrund":{},"tags":{"type":"array","items":{}},"waehrung":{"type":"string"},"createdAt":{},"updatedAt":{},"closedAt":{},"lastStageChangeAt":{}},"required":["id","bezeichnung","kundeName","stage","wahrscheinlichkeit","erwarteteEinnahmen","tags","waehrung"],"additionalProperties":false},"example":{"id":"string","bezeichnung":"string","kundeName":"string","stage":"string","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"tags":[],"waehrung":"string"}}}},"400":{"description":"Validierungsfehler oder leerer Rumpf (`no_fields_to_update`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Chance nicht gefunden (`chance_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1PipelineChancenById","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Ändert genau die Felder, die im Rumpf stehen; alle übrigen bleiben unberührt. Ein leerer Rumpf ist ein Fehler (400 `no_fields_to_update`) und keine wirkungslose Erfolgsmeldung. Steht `stage` im Rumpf, wird zusätzlich `lastStageChangeAt` auf jetzt gesetzt, `closedAt` dagegen nicht: eine auf diesem Weg gewonnene Chance bleibt ohne Abschlussdatum und taucht in den Monatszahlen von `/pipeline/stats` nicht auf. Dafür ist `PATCH /pipeline/chancen/:id/stage` da. Der Verlustgrund lässt sich hier nicht setzen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"kundeName":{"type":"string","minLength":1,"maxLength":255},"kundeId":{"type":"string"},"ansprechpartner":{"type":"string","maxLength":255},"email":{"type":"string","format":"email"},"telefon":{"type":"string","maxLength":50},"stage":{"type":"string","enum":["neu","qualifiziert","angebot","verhandlung","gewonnen","verloren"],"default":"neu"},"wahrscheinlichkeit":{"type":"integer","minimum":0,"maximum":100,"default":25},"erwarteteEinnahmen":{"type":"number","minimum":0,"default":0},"erwartetesAbschlussdatum":{"type":"string","format":"date"},"quelle":{"type":"string","enum":["empfehlung","messe","website","kaltakquise","andere"]},"zustaendigerUser":{"type":"string"},"notizen":{"type":"string"}}},"example":{"bezeichnung":"string","kundeName":"string","kundeId":"string","ansprechpartner":"string","email":"beispiel@example.com","telefon":"string","stage":"neu","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"erwartetesAbschlussdatum":"2026-01-01","quelle":"empfehlung","zustaendigerUser":"string","notizen":"string"}}}},"summary":"Ändert genau die Felder, die im Rumpf stehen; alle übrigen bleiben unberührt","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Chance gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Chance nicht gefunden (`chance_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1PipelineChancenById","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Entfernt die Chance endgültig. Die Tabelle führt kein `deleted_at`, ein Wiederherstellen gibt es also nicht. Mitgelöscht werden alle Aktivitäten der Chance. Verknüpfte Angebote bleiben dagegen bestehen und verlieren nur ihre Zuordnung: `quotes.chance_id` wird auf `null` gesetzt, bevor die Chance verschwindet.","summary":"Entfernt die Chance endgültig","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/pipeline/chancen/{id}/stage":{"patch":{"responses":{"200":{"description":"Stufe aktualisiert. Zwei Nebenwirkungen, die der Rumpf nicht nennt: `closedAt` wird bei `gewonnen`/`verloren` auf jetzt gesetzt und bei JEDER anderen Stufe wieder auf `null` geloescht — ein Rueckwechsel nimmt also das Abschlussdatum mit. Und `verlorenGrund` wird immer geschrieben: wer ihn im Rumpf weglaesst, loescht einen bestehenden Grund.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"bezeichnung":{"type":"string"},"kundeName":{"type":"string"},"kundeId":{},"ansprechpartner":{},"email":{},"telefon":{},"stage":{"type":"string"},"wahrscheinlichkeit":{"type":"number"},"erwarteteEinnahmen":{"type":"number"},"erwartetesAbschlussdatum":{},"quelle":{},"zustaendigerUser":{},"notizen":{},"verlorenGrund":{},"tags":{"type":"array","items":{}},"waehrung":{"type":"string"},"createdAt":{},"updatedAt":{},"closedAt":{},"lastStageChangeAt":{}},"required":["id","bezeichnung","kundeName","stage","wahrscheinlichkeit","erwarteteEinnahmen","tags","waehrung"],"additionalProperties":false},"example":{"id":"string","bezeichnung":"string","kundeName":"string","stage":"string","wahrscheinlichkeit":0,"erwarteteEinnahmen":0,"tags":[],"waehrung":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Chance nicht gefunden (`chance_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchApiV1PipelineChancenByIdStage","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Chance auf eine andere Pipeline-Stufe setzen","description":"Setzt `stage` auf einen der sechs erlaubten Werte und stellt dabei `lastStageChangeAt` auf jetzt, und zwar auch dann, wenn die neue Stufe der bisherigen entspricht. Wie lange eine Chance unbewegt liegt, zählt also ab dem letzten Aufruf dieser Route und nicht ab dem letzten echten Stufenwechsel. Nur dieser Weg pflegt `closedAt`, und nur Chancen mit `closedAt` erscheinen in den Monatszahlen von `/pipeline/stats`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"stage":{"type":"string","enum":["neu","qualifiziert","angebot","verhandlung","gewonnen","verloren"]},"verlorenGrund":{"type":"string"}},"required":["stage"]},"example":{"stage":"neu","verlorenGrund":"string"}}}}}},"/api/v1/pipeline/chancen/{id}/angebote":{"get":{"responses":{"200":{"description":"Angebote dieser Chance — mit `total`, ohne Blaetterung. `total` heisst hier die Zahl der gelieferten Zeilen, nicht eine Geldsumme.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"quoteNumber":{},"title":{},"status":{},"total":{"type":"number"},"createdAt":{}},"required":["total"],"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"total":0}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Chance nicht gefunden (`chance_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1PipelineChancenByIdAngebote","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Angebote zu einer Chance auflisten","description":"Liest aus `quotes` die Angebote, deren `chance_id` auf diese Chance zeigt, neueste zuerst. Soft-gelöschte Angebote (`deleted_at`) bleiben außen vor. Gibt es die Chance nicht, antwortet der Aufruf mit 404 statt mit einer leeren Liste. Je Angebot kommen nur Nummer, Titel, Status, Summe und Anlagedatum mit; Positionen und die übrigen Belegfelder liefert diese Route nicht."},"post":{"responses":{"200":{"description":"Verknuepft. ZWEI Erfolgsfaelle mit demselben Status: `message: \"verknuepft\"` (neu gesetzt) und `message: \"bereits verknuepft\"` (dieselbe Chance hing schon dran, es wurde nichts geaendert).","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"quoteId":{"type":"string"},"chanceId":{"type":"string"}},"required":["message","quoteId","chanceId"],"additionalProperties":false},"example":{"message":"string","quoteId":"string","chanceId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Chance (`chance_not_found`) oder Angebot (`quote_not_found`) nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Das Angebot haengt an einer ANDEREN Chance (`quote_already_linked`) — `chanceId` nennt welche","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PipelineChancenByIdAngebote","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt `quotes.chance_id` auf diese Chance. Ein Angebot gehört zu höchstens einer Chance: hängt es bereits an einer anderen, lehnt der Aufruf mit 409 ab, statt es still umzuhängen. Ein soft-gelöschtes Angebot gilt als nicht vorhanden. Am Beleg ändert sich sonst nichts, und die Stufe der Chance bleibt unverändert; wer den Vorgang auf `angebot` schieben will, tut das über `PATCH /pipeline/chancen/:id/stage`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"quoteId":{"type":"string","minLength":1}},"required":["quoteId"]},"example":{"quoteId":"string"}}}},"summary":"Setzt `quotes.chance_id` auf diese Chance","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/pipeline/chancen/{id}/angebote/{quoteId}":{"delete":{"responses":{"200":{"description":"Verknuepfung geloest. Meldet auch dann 200, wenn das Angebot gar nicht an dieser Chance hing — der Aufruf ist wiederholbar und belegt nicht, dass vorher eine Verknuepfung bestand.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"quoteId":{"type":"string"},"chanceId":{"type":"string"}},"required":["message","quoteId","chanceId"],"additionalProperties":false},"example":{"message":"string","quoteId":"string","chanceId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Chance nicht gefunden (`chance_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1PipelineChancenByIdAngeboteByQuoteId","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"quoteId","required":true}],"description":"Setzt `quotes.chance_id` zurück auf `null` und rührt den Beleg sonst nicht an. Die Anweisung greift nur, wenn Angebot und Chance auch wirklich zusammengehören: eine Verknüpfung, die einer anderen Chance gehört, lässt sich damit nicht lösen. Die Chance selbst muss es geben, sonst 404; eine unbekannte Angebots-Id dagegen ist kein Fehler.","summary":"Setzt `quotes.chance_id` zurück auf `null` und rührt den Beleg sonst nicht an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/pipeline/chancen/{id}/aktivitaeten":{"post":{"responses":{"201":{"description":"Aktivitaet angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"chanceId":{},"typ":{"type":"string"},"betreff":{"type":"string"},"inhalt":{},"erledigt":{"type":"boolean"},"faelligAm":{},"userId":{},"createdAt":{}},"required":["id","typ","betreff","erledigt"],"additionalProperties":false},"example":{"id":"string","typ":"string","betreff":"string","erledigt":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Chance nicht gefunden (`chance_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PipelineChancenByIdAktivitaeten","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aktivität zu einer Chance festhalten (Notiz, Anruf, Termin …)","description":"Prüft zuerst, ob es die Chance beim Mandanten gibt, und schreibt dann eine Zeile in `chance_aktivitaeten`. `typ` ist eines von notiz, email, anruf, termin oder aufgabe. Die Aktivität beginnt immer als offen; umschalten lässt sie sich nur über `PATCH /pipeline/aktivitaeten/:id/erledigt`. Der angemeldete Nutzer wird nicht mitgeschrieben, `userId` bleibt leer. `faelligAm` ist ein reines Datum ohne Uhrzeit und löst weder Erinnerung noch Aufgabe aus.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"typ":{"type":"string","enum":["notiz","email","anruf","termin","aufgabe"]},"betreff":{"type":"string","minLength":1,"maxLength":255},"inhalt":{"type":"string"},"faelligAm":{"type":"string","format":"date"}},"required":["typ","betreff"]},"example":{"typ":"notiz","betreff":"string","inhalt":"string","faelligAm":"2026-01-01"}}}}}},"/api/v1/pipeline/aktivitaeten/{id}/erledigt":{"patch":{"responses":{"200":{"description":"Aktivitaet aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"chanceId":{},"typ":{"type":"string"},"betreff":{"type":"string"},"inhalt":{},"erledigt":{"type":"boolean"},"faelligAm":{},"userId":{},"createdAt":{}},"required":["id","typ","betreff","erledigt"],"additionalProperties":false},"example":{"id":"string","typ":"string","betreff":"string","erledigt":true}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Aktivitaet nicht gefunden (`aktivitaet_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchApiV1PipelineAktivitaetenByIdErledigt","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Kippt `erledigt` auf den jeweils anderen Wert. Der Aufruf nimmt keinen Rumpf und kennt damit keinen Zielzustand: zweimal aufgerufen steht die Aktivität wieder so da wie vorher. Angesprochen wird sie direkt über ihre eigene Id, ohne die Chance im Pfad; eine Id aus einem fremden Mandanten oder eine unbekannte endet in 404.","summary":"Kippt `erledigt` auf den jeweils anderen Wert","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/pipeline/chancen/{id}/convert-to-quote":{"post":{"responses":{"201":{"description":"Angebotsentwurf erstellt. Die Antwort traegt jedes Feld DOPPELT — einmal snake_case, einmal camelCase. Das ist Ruecksicht auf aeltere Aufrufer; neue Integrationen nehmen `quoteId`/`quoteNumber`. Der Audit-Eintrag wird nebenher geschrieben und blockiert nicht: scheitert er, kommt trotzdem 201.","content":{"application/json":{"schema":{"type":"object","properties":{"quote_id":{"type":"string","format":"uuid"},"quoteId":{"type":"string","format":"uuid"},"quote_number":{"type":"string"},"quoteNumber":{"type":"string"}},"required":["quote_id","quoteId","quote_number","quoteNumber"],"additionalProperties":false},"example":{"quote_id":"00000000-0000-4000-8000-000000000000","quoteId":"00000000-0000-4000-8000-000000000000","quote_number":"string","quoteNumber":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Chance nicht gefunden (`chance_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — die Transaktion wurde zurueckgerollt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1PipelineChancenByIdConvert-to-quote","tags":["pipeline"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Legt in `quotes` einen Entwurf im Status `draft` mit genau einer Position an: Bezeichnung der Chance, Menge 1, Einzelpreis gleich der erwarteten Einnahme. Dieser Betrag gilt als Nettowert, die Steuer kommt oben darauf; der Satz stammt aus dem Standardsatz des Mandanten, ersatzweise aus `NEMIX_DEFAULT_TAX_RATE` und zuletzt aus 19 %. Die Angebotsnummer zieht der Aufruf aus dem Nummernkreis `offer_number`, und zwar in einer eigenen Transaktion vor dem Schreiben des Belegs: scheitert der Beleg danach, ist diese Nummer trotzdem verbraucht. Angebot und Chance werden gemeinsam geschrieben oder gar nicht. Der Beleg trägt die Chance als `chance_id`, und die Chance rückt auf `angebot` vor, sofern sie in `neu` oder `qualifiziert` steht. Gegen Wiederholung ist der Aufruf nicht abgesichert: jeder weitere Aufruf erzeugt ein weiteres Angebot.","summary":"Legt in `quotes` einen Entwurf im Status `draft` mit genau einer Position an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/pipeline/stats":{"get":{"responses":{"200":{"description":"Pipeline-Kennzahlen. `byStage` traegt IMMER alle sechs Stufen — die Karte wird mit Nullen vorbelegt, eine 0 heisst also „keine Chance in dieser Stufe\". `conversionRate` ist ein Prozentwert mit einer Nachkommastelle und steht auf 0, solange gar nichts abgeschlossen ist — das ist kein gemessenes „0 % Erfolg\".","content":{"application/json":{"schema":{"type":"object","properties":{"totalOpen":{"type":"number"},"totalValue":{"type":"number"},"weightedValue":{"type":"number"},"wonThisMonth":{"type":"number"},"lostThisMonth":{"type":"number"},"conversionRate":{"type":"number"},"byStage":{"type":"object","properties":{"neu":{"type":"number"},"qualifiziert":{"type":"number"},"angebot":{"type":"number"},"verhandlung":{"type":"number"},"gewonnen":{"type":"number"},"verloren":{"type":"number"}},"required":["neu","qualifiziert","angebot","verhandlung","gewonnen","verloren"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string","const":"db"}},"required":["source"],"additionalProperties":false}},"required":["totalOpen","totalValue","weightedValue","wonThisMonth","lostThisMonth","conversionRate","byStage","meta"],"additionalProperties":false},"example":{"totalOpen":0,"totalValue":0,"weightedValue":0,"wonThisMonth":0,"lostThisMonth":0,"conversionRate":0,"byStage":{"neu":0,"qualifiziert":0,"angebot":0,"verhandlung":0,"gewonnen":0,"verloren":0},"meta":{"source":"db"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"chanceId":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1PipelineStats","tags":["pipeline"],"parameters":[],"description":"Rechnet über alle Chancen des Mandanten und nimmt keine Parameter entgegen. Als offen zählen die Stufen neu, qualifiziert, angebot und verhandlung; nur aus ihnen entstehen `totalOpen`, die Summe `totalValue` und `weightedValue`, das jede erwartete Einnahme mit ihrer Wahrscheinlichkeit gewichtet. `wonThisMonth` und `lostThisMonth` zählen nach `closedAt` im laufenden Kalendermonat: eine Chance, deren Stufe ohne `PATCH /pipeline/chancen/:id/stage` auf gewonnen gesetzt wurde, hat kein `closedAt` und fehlt hier. `conversionRate` betrachtet dagegen alle je abgeschlossenen Chancen und nicht nur den laufenden Monat.","summary":"Rechnet über alle Chancen des Mandanten und nimmt keine Parameter entgegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inventur/stats":{"get":{"responses":{"200":{"description":"KPI-Werte","content":{"application/json":{"schema":{"type":"object","properties":{"offen":{"type":"integer"},"inProgress":{"type":"integer"},"abgeschlossenThisYear":{"type":"integer"},"gesamtabweichungThisYear":{"type":"number"},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["offen","inProgress","abgeschlossenThisYear","gesamtabweichungThisYear","meta"],"additionalProperties":false},"example":{"offen":0,"inProgress":0,"abgeschlossenThisYear":0,"gesamtabweichungThisYear":0,"meta":{"source":"string"}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1InventurStats","tags":["inventur"],"parameters":[],"summary":"Kennzahlen zu Inventur-Durchgaengen","description":"KPIs zu Inventur-Durchgaengen (offen, in Bearbeitung, abgeschlossen, Abweichungssumme)."}},"/api/v1/inventur/wareneingang":{"get":{"responses":{"200":{"description":"Liste Wareneingaenge","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"belegnummer":{},"lieferantName":{},"lieferantId":{},"bestellnr":{},"eingangsdatum":{},"status":{},"positionsCount":{"type":"integer"},"notizen":{},"createdAt":{}},"required":["positionsCount"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"positionsCount":0}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1InventurWareneingang","tags":["inventur"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["angekommen","geprueft","eingelagert","reklamiert"]}}],"description":"Liste der Wareneingaenge des Tenants mit Filter und Pagination. Gelesen wird `warenein_gaenge` des Mandanten, neueste zuerst. `limit` liegt zwischen 1 und 200 (Vorgabe 50), `offset` beginnt bei 0; optional laesst sich auf `status` (angekommen/geprueft/eingelagert/reklamiert) einschraenken. `pagination.total` zaehlt mit demselben Filter wie die Liste, ist also die Gesamtzahl der Treffer, nicht der Tabelle.","summary":"Liste der Wareneingaenge des Tenants mit Filter und Pagination","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Wareneingang erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"belegnummer":{},"lieferantName":{},"lieferantId":{},"bestellnr":{},"eingangsdatum":{},"status":{},"positionsCount":{"type":"integer"},"notizen":{},"createdAt":{}},"required":["positionsCount"],"additionalProperties":false},"example":{"positionsCount":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1InventurWareneingang","tags":["inventur"],"parameters":[],"description":"Neuen Wareneingang anlegen (Belegnummer wird automatisch generiert). Die Nummer hat die Form `WE-JJJJ-NNNN` und entsteht aus der Zahl der bereits vorhandenen Belege dieses Jahres plus eins; eine Eindeutigkeitspruefung in der Datenbank gibt es dafuer nicht. Eingangsdatum und Status setzt die Tabelle selbst (`CURRENT_DATE`, `angekommen`). Der Wareneingang wird nur erfasst — Lagerbestaende bucht dieser Aufruf nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lieferantName":{"type":"string","minLength":1,"maxLength":255},"lieferantId":{"type":"string"},"bestellnr":{"type":"string"},"positionsCount":{"type":"integer","minimum":0},"notizen":{"type":"string"}},"required":["lieferantName"]},"example":{"lieferantName":"string","lieferantId":"string","bestellnr":"string","positionsCount":0,"notizen":"string"}}}},"summary":"Neuen Wareneingang anlegen (Belegnummer wird automatisch generiert)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inventur/wareneingang/{id}/status":{"patch":{"responses":{"200":{"description":"Status aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"belegnummer":{},"lieferantName":{},"lieferantId":{},"bestellnr":{},"eingangsdatum":{},"status":{},"positionsCount":{"type":"integer"},"notizen":{},"createdAt":{}},"required":["positionsCount"],"additionalProperties":false},"example":{"positionsCount":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchApiV1InventurWareneingangByIdStatus","tags":["inventur"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Setzt den Status eines Wareneingangs","description":"Status eines Wareneingangs aktualisieren (angekommen/geprueft/eingelagert/reklamiert).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["angekommen","geprueft","eingelagert","reklamiert"]}},"required":["status"]},"example":{"status":"angekommen"}}}}}},"/api/v1/inventur/positionen/{posId}/istbestand":{"patch":{"responses":{"200":{"description":"Position aktualisiert. `istbestand: null` heisst „noch nicht gezaehlt\", `0` heisst „gezaehlt, nichts vorhanden\".","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"durchgangId":{},"artikelNr":{},"artikelName":{},"lagerplatz":{},"sollbestand":{"type":"number"},"istbestand":{"type":["number","null"]},"abweichung":{"type":["number","null"]},"einheit":{},"einzelwert":{"type":"number"},"abweichungswert":{"type":["number","null"]},"erfasstAm":{},"erfasstVon":{},"createdAt":{}},"required":["sollbestand","istbestand","abweichung","einzelwert","abweichungswert"],"additionalProperties":false},"example":{"sollbestand":0,"istbestand":0,"abweichung":0,"einzelwert":0,"abweichungswert":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Position nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchApiV1InventurPositionenByPosIdIstbestand","tags":["inventur"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"posId","required":true}],"description":"Istbestand einer Inventur-Position erfassen und Abweichung berechnen. Aus Ist minus Soll entsteht `abweichung` (auf drei Nachkommastellen gerundet), daraus mit dem Einzelwert der `abweichungswert` (zwei Nachkommastellen); `erfasst_am` wird gesetzt. Anschliessend zaehlt der Aufruf die erfassten Positionen des Durchgangs neu, und ein Durchgang im Status `angelegt` wechselt dabei auf `erfassen`. Eine fremde oder unbekannte Position ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"istbestand":{"type":"number","minimum":0}},"required":["istbestand"]},"example":{"istbestand":0}}}},"summary":"Istbestand einer Inventur-Position erfassen und Abweichung berechnen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inventur":{"get":{"responses":{"200":{"description":"Liste Durchgaenge","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"bezeichnung":{},"stichtag":{},"status":{},"lagerId":{},"artikelCount":{"type":"integer"},"erfassteCount":{"type":"integer"},"abweichungSumme":{"type":"number"},"notizen":{},"abgeschlossenAm":{},"createdAt":{},"updatedAt":{}},"required":["artikelCount","erfassteCount","abweichungSumme"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"artikelCount":0,"erfassteCount":0,"abweichungSumme":0}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1Inventur","tags":["inventur"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["angelegt","erfassen","abgeschlossen","storniert"]}}],"description":"Liste der Inventur-Durchgaenge des Tenants mit Filter und Pagination. Gelesen wird `inventur_durchgaenge` des Mandanten, neueste zuerst. `limit` liegt zwischen 1 und 200 (Vorgabe 50), `offset` beginnt bei 0; optional laesst sich auf `status` (angelegt/erfassen/abgeschlossen/storniert) einschraenken. `pagination.total` zaehlt mit demselben Filter wie die Liste. Die Positionen eines Durchgangs kommen hier NICHT mit — die liefert nur der Einzelabruf.","summary":"Liste der Inventur-Durchgaenge des Tenants mit Filter und Pagination","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Durchgang angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"bezeichnung":{},"stichtag":{},"status":{},"lagerId":{},"artikelCount":{"type":"integer"},"erfassteCount":{"type":"integer"},"abweichungSumme":{"type":"number"},"notizen":{},"abgeschlossenAm":{},"createdAt":{},"updatedAt":{}},"required":["artikelCount","erfassteCount","abweichungSumme"],"additionalProperties":false},"example":{"artikelCount":0,"erfassteCount":0,"abweichungSumme":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1Inventur","tags":["inventur"],"parameters":[],"description":"Neuen Inventur-Durchgang anlegen. Schreibt eine Zeile nach `inventur_durchgaenge`; ohne `stichtag` nimmt der Aufruf das heutige Datum. Status, Artikel- und Erfassungszahl setzt die Tabelle selbst — der Durchgang startet also leer und auf `angelegt`. Positionen entstehen dabei nicht, die kommen ueber `POST /api/v1/inventur/{id}/positionen` dazu.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"stichtag":{"type":"string","format":"date"},"lagerId":{"type":"string"},"notizen":{"type":"string"}},"required":["bezeichnung"]},"example":{"bezeichnung":"string","stichtag":"2026-01-01","lagerId":"string","notizen":"string"}}}},"summary":"Neuen Inventur-Durchgang anlegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inventur/{id}":{"get":{"responses":{"200":{"description":"Durchgang mit Positionen — der Durchgang steht FLACH, `positionen` haengt daneben. Kein `{ durchgang, positionen }`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"bezeichnung":{},"stichtag":{},"status":{},"lagerId":{},"artikelCount":{"type":"integer"},"erfassteCount":{"type":"integer"},"abweichungSumme":{"type":"number"},"notizen":{},"abgeschlossenAm":{},"createdAt":{},"updatedAt":{},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{},"durchgangId":{},"artikelNr":{},"artikelName":{},"lagerplatz":{},"sollbestand":{"type":"number"},"istbestand":{"type":["number","null"]},"abweichung":{"type":["number","null"]},"einheit":{},"einzelwert":{"type":"number"},"abweichungswert":{"type":["number","null"]},"erfasstAm":{},"erfasstVon":{},"createdAt":{}},"required":["sollbestand","istbestand","abweichung","einzelwert","abweichungswert"],"additionalProperties":false}}},"required":["artikelCount","erfassteCount","abweichungSumme","positionen"],"additionalProperties":false},"example":{"artikelCount":0,"erfassteCount":0,"abweichungSumme":0,"positionen":[{"sollbestand":0,"istbestand":0,"abweichung":0,"einzelwert":0,"abweichungswert":0}]}}}},"401":{"description":"Unauthorized"},"404":{"description":"Durchgang nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1InventurById","tags":["inventur"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Inventur-Durchgang inklusive aller Positionen abrufen. Der Durchgang steht FLACH in der Antwort, `positionen` haengt als Liste daneben — es gibt kein `{ durchgang, positionen }`. Die Positionen sind nach Artikelnummer aufsteigend sortiert und werden nicht gekappt. Ein fremder oder unbekannter Durchgang ergibt 404.","summary":"Inventur-Durchgang inklusive aller Positionen abrufen","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Geloescht — nur eine Quittung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Durchgang nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Nur angelegte oder stornierte Durchgaenge sind loeschbar (invalid_status_for_delete)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1InventurById","tags":["inventur"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Inventur-Durchgang loeschen (nur angelegt oder storniert). Geloescht wird endgueltig und ohne `deleted_at`: erst alle Positionen des Durchgangs, dann der Durchgang selbst. Steht er auf `erfassen` oder `abgeschlossen`, lehnt der Aufruf mit 409 `invalid_status_for_delete` ab — ein abgeschlossener traegt bereits Journalbuchungen. Zurueck kommt nur eine Quittung, kein Datensatz.","summary":"Inventur-Durchgang loeschen (nur angelegt oder storniert)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inventur/{id}/positionen":{"post":{"responses":{"201":{"description":"Positionen angelegt — `count` nennt die Zahl der eingefuegten Zeilen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"durchgangId":{},"artikelNr":{},"artikelName":{},"lagerplatz":{},"sollbestand":{"type":"number"},"istbestand":{"type":["number","null"]},"abweichung":{"type":["number","null"]},"einheit":{},"einzelwert":{"type":"number"},"abweichungswert":{"type":["number","null"]},"erfasstAm":{},"erfasstVon":{},"createdAt":{}},"required":["sollbestand","istbestand","abweichung","einzelwert","abweichungswert"],"additionalProperties":false}},"count":{"type":"integer"}},"required":["data","count"],"additionalProperties":false},"example":{"data":[{"sollbestand":0,"istbestand":0,"abweichung":0,"einzelwert":0,"abweichungswert":0}],"count":0}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Durchgang nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1InventurByIdPositionen","tags":["inventur"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Bulk-Anlage von Inventur-Positionen fuer einen Durchgang. Die Positionen werden einzeln nacheinander eingefuegt, NICHT in einer umschliessenden Transaktion — bricht es in der Mitte ab, bleiben die bereits eingefuegten stehen. Ohne `einheit` wird „Stk\" gesetzt. Danach zaehlt der Aufruf `artikel_count` des Durchgangs neu; `count` in der Antwort nennt die Zahl der eingefuegten Zeilen. Ein unbekannter Durchgang ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"positions":{"type":"array","items":{"type":"object","properties":{"artikelNr":{"type":"string","minLength":1},"artikelName":{"type":"string","minLength":1},"sollbestand":{"type":"number","minimum":0},"einzelwert":{"type":"number","minimum":0},"lagerplatz":{"type":"string"},"einheit":{"type":"string"}},"required":["artikelNr","artikelName","sollbestand","einzelwert"]},"minItems":1}},"required":["positions"]},"example":{"positions":[{"artikelNr":"string","artikelName":"string","sollbestand":0,"einzelwert":0,"lagerplatz":"string","einheit":"string"}]}}}},"summary":"Bulk-Anlage von Inventur-Positionen fuer einen Durchgang","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inventur/{id}/abschliessen":{"post":{"responses":{"200":{"description":"Durchgang abgeschlossen","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"bezeichnung":{},"stichtag":{},"status":{},"lagerId":{},"artikelCount":{"type":"integer"},"erfassteCount":{"type":"integer"},"abweichungSumme":{"type":"number"},"notizen":{},"abgeschlossenAm":{},"createdAt":{},"updatedAt":{}},"required":["artikelCount","erfassteCount","abweichungSumme"],"additionalProperties":false},"example":{"artikelCount":0,"erfassteCount":0,"abweichungSumme":0}}}},"401":{"description":"Unauthorized"},"404":{"description":"Durchgang nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Bereits abgeschlossen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"423":{"description":"Buchungsperiode geschlossen — der Abschluss wurde NICHT gebucht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1InventurByIdAbschliessen","tags":["inventur"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Inventur-Durchgang abschliessen und Abweichungssumme berechnen. Der Durchgang bekommt Status `abgeschlossen` und `abgeschlossen_am`; in `abweichung_summe` landet die Summe der BETRAEGE aller erfassten Abweichungen. Ist die vorzeichenbehaftete Abweichung ungleich null, entsteht zusaetzlich eine Journalbuchung zwischen Bestands- und Bestandsveraenderungskonto unter dem Beleg `INV-<id-anfang>`. Ein bereits abgeschlossener Durchgang ergibt 409. Ist die Buchungsperiode geschlossen, kommt 423 — der Statuswechsel steht dann bereits in der Datenbank, die Journalbuchung nicht.","summary":"Inventur-Durchgang abschliessen und Abweichungssumme berechnen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/inventur/{id}/stornieren":{"post":{"responses":{"200":{"description":"Durchgang storniert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"bezeichnung":{},"stichtag":{},"status":{},"lagerId":{},"artikelCount":{"type":"integer"},"erfassteCount":{"type":"integer"},"abweichungSumme":{"type":"number"},"notizen":{},"abgeschlossenAm":{},"createdAt":{},"updatedAt":{}},"required":["artikelCount","erfassteCount","abweichungSumme"],"additionalProperties":false},"example":{"artikelCount":0,"erfassteCount":0,"abweichungSumme":0}}}},"401":{"description":"Unauthorized"},"404":{"description":"Durchgang nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Nicht stornierbar: `already_closed` (abgeschlossen) oder `already_cancelled` — die `message` unterscheidet, der Status nicht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1InventurByIdStornieren","tags":["inventur"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Inventur-Durchgang stornieren (nur aus angelegt/erfassen). Gesetzt wird ausschliesslich der Status auf `storniert`: die erfassten Positionen bleiben stehen, und es wird nichts gebucht und nichts zurueckgebucht. Ein abgeschlossener Durchgang wird mit 409 `already_closed` abgelehnt, ein bereits stornierter mit 409 `already_cancelled` — der Aufruf ist also nicht idempotent.","summary":"Inventur-Durchgang stornieren (nur aus angelegt/erfassen)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/lager/bestand":{"get":{"responses":{"200":{"description":"Die passenden Bestandszeilen; `total` zaehlt nur die gelieferten.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"artikelId":{"type":"string","format":"uuid"},"sku":{"type":["string","null"]},"artikelName":{"type":["string","null"]},"einheit":{"type":["string","null"]},"ortId":{"type":"string","format":"uuid"},"ortCode":{"type":["string","null"]},"ortName":{"type":["string","null"]},"chargeId":{"type":["string","null"],"format":"uuid","description":"null = Bestand ohne Chargenfuehrung."},"menge":{"type":"number","description":"Gefuehrter Bestand. KANN NEGATIV SEIN — siehe `fehlbestand`."},"reserviert":{"type":"number"},"verfuegbar":{"type":"number","description":"menge minus reserviert. Wird nicht bei 0 gekappt."},"mindestbestand":{"type":"number","description":"Aus dem Artikelstamm; 0 heisst „nicht gepflegt\"."},"unterMindestbestand":{"type":"boolean","description":"Nur wahr, wenn ein Mindestbestand > 0 gepflegt ist UND verfuegbar darunter liegt."},"fehlbestand":{"type":"boolean","description":"Der gefuehrte Bestand ist negativ."}},"required":["artikelId","sku","artikelName","einheit","ortId","ortCode","ortName","chargeId","menge","reserviert","verfuegbar","mindestbestand","unterMindestbestand","fehlbestand"]}},"total":{"type":"integer"}},"required":["data","total"]},"example":{"data":[{"artikelId":"00000000-0000-4000-8000-000000000000","sku":"string","artikelName":"string","einheit":"string","ortId":"00000000-0000-4000-8000-000000000000","ortCode":"string","ortName":"string","chargeId":"00000000-0000-4000-8000-000000000000","menge":0,"reserviert":0,"verfuegbar":0,"mindestbestand":0,"unterMindestbestand":true,"fehlbestand":true}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Fachlicher Fehler der Lagerlogik.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}}},"operationId":"getApiV1LagerBestand","tags":["lager"],"parameters":[{"in":"query","name":"artikelId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"ortId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"summary":"Lagerbestand abfragen","description":"Der gefuehrte Bestand, eine Zeile je Artikel, Lagerort und Charge, mit den\nStammdaten von Artikel und Ort daneben. Sortiert nach Artikelname und\nOrtskurzzeichen.\n\nFilter: `artikelId` und `ortId`, beide freiwillig. `limit` begrenzt auf 1\nbis 500 Zeilen (Vorgabe 100). ES WIRD NICHT GEBLAETTERT: es gibt kein\n`offset`, und `total` ist die Anzahl der ZURUECKGEGEBENEN Zeilen, nicht die\nZahl aller Bestandszeilen. Wer 100 bekommt, weiss nicht, ob es mehr gibt.\n\nArtikel und Ort werden fest verbunden: eine Bestandszeile, deren Artikel\noder Ort nicht mehr existiert, ERSCHEINT NICHT — sie faellt aus der Antwort,\nohne dass es auffaellt.\n\n`verfuegbar` ist `menge` minus `reserviert` und wird NICHT bei 0 gekappt.\nAuch `menge` selbst kann negativ sein: das Buchen verhindert einen\nFehlbestand nicht, es meldet ihn nur (`fehlbestand`).\n\nDie Tabellen werden bei Bedarf angelegt; ein frischer Mandant bekommt eine\nleere Liste und keinen Fehler.\n\nUngegatet — jeder angemeldete Benutzer darf lesen. Das Buchen daneben\n(`POST /lager/buchung`) verlangt `manager`."}},"/api/v1/lager/bewegungen":{"get":{"responses":{"200":{"description":"Die passenden Bewegungen; `total` zaehlt nur die gelieferten.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"artikelId":{"type":"string","format":"uuid"},"artikelName":{"type":["string","null"]},"sku":{"type":["string","null"]},"menge":{"type":"number","description":"IMMER positiv. Die Richtung steckt in `von` und `nach`."},"vorgang":{"type":"string","description":"eingang, ausgang, umlagerung, korrektur, storno oder retoure — eine Beschriftung."},"grund":{"type":["string","null"]},"von":{"type":"object","properties":{"code":{"type":["string","null"]},"name":{"type":["string","null"]}},"required":["code","name"]},"nach":{"type":"object","properties":{"code":{"type":["string","null"]},"name":{"type":["string","null"]}},"required":["code","name"]},"beleg":{"type":["object","null"],"properties":{"typ":{"type":["string","null"]},"id":{"type":["string","null"]},"position":{"type":["integer","null"]}},"required":["typ","id","position"],"description":"null bei einer Buchung ohne Belegbezug — etwa ueber POST /lager/buchung."},"gebuchtAm":{"type":"string"}},"required":["id","artikelId","artikelName","sku","menge","vorgang","grund","von","nach","beleg","gebuchtAm"]}},"total":{"type":"integer"}},"required":["data","total"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","artikelId":"00000000-0000-4000-8000-000000000000","artikelName":"string","sku":"string","menge":0,"vorgang":"string","grund":"string","von":{"code":"string","name":"string"},"nach":{"code":"string","name":"string"},"beleg":{"typ":"string","id":"string","position":0},"gebuchtAm":"string"}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Fachlicher Fehler der Lagerlogik.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}}},"operationId":"getApiV1LagerBewegungen","tags":["lager"],"parameters":[{"in":"query","name":"artikelId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"ortId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"belegId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"von","schema":{"type":"string","format":"date"}},{"in":"query","name":"bis","schema":{"type":"string","format":"date"}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"summary":"Bewegungsjournal des Lagers lesen","description":"Die Lagerbewegungen, neueste zuerst. Das Journal ist FORTSCHREIBEND: es\ngibt keine Route, die eine Bewegung aendert oder loescht. Eine\nFehlbuchung wird durch eine Gegenbuchung ausgeglichen, und beide Zeilen\nbleiben sichtbar.\n\n`menge` ist immer positiv; wohin gebucht wurde, sagen `von` und `nach`.\n`vorgang` ist nur eine Beschriftung — auf den Bestand wirkt sich bei jedem\nWert dasselbe aus.\n\nFilter, alle freiwillig: `artikelId`; `ortId` trifft Quelle ODER Ziel;\n`belegId`; `von` und `bis` als Tagesdatum, beide EINSCHLIESSLICH — `bis`\ndeckt den ganzen Tag ab. `limit` begrenzt auf 1 bis 500 Zeilen (Vorgabe\n100). ES WIRD NICHT GEBLAETTERT: kein `offset`, und `total` zaehlt nur die\ngelieferten Zeilen.\n\nArtikel und BEIDE Orte werden fest verbunden: eine Bewegung, deren Artikel\noder Ort nicht mehr existiert, erscheint nicht in der Antwort.\n\nDie Tabellen werden bei Bedarf angelegt.\n\nUngegatet — jeder angemeldete Benutzer darf lesen."}},"/api/v1/lager/orte":{"get":{"responses":{"200":{"description":"Alle Orte des Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string","description":"Kurzzeichen des Ortes, im Mandanten eindeutig."},"name":{"type":"string"},"art":{"type":"string","description":"Bauart des Ortes; bestimmt die Sortierung der Liste."},"virtuelleRolle":{"type":["string","null"],"description":"Gesetzt bei Gegenkonten (Wareneingang, Schwund) — kein realer Ort."},"parentId":{"type":["string","null"],"format":"uuid"},"istStandard":{"type":"boolean"},"aktiv":{"type":"boolean","description":"Inaktive Orte werden MITGELIEFERT, nicht gefiltert."}},"required":["id","code","name","art","virtuelleRolle","parentId","istStandard","aktiv"]}},"total":{"type":"integer"}},"required":["data","total"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","art":"string","virtuelleRolle":"string","parentId":"00000000-0000-4000-8000-000000000000","istStandard":true,"aktiv":true}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Fachlicher Fehler der Lagerlogik.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}}},"operationId":"getApiV1LagerOrte","tags":["lager"],"parameters":[],"summary":"Lagerorte auflisten","description":"Gibt ALLE Lagerorte des Mandanten zurueck, nach Art und Kurzzeichen\nsortiert. Es wird nicht geblaettert und nicht gefiltert: inaktive Orte\nund virtuelle Gegenkonten sind mit dabei, erkennbar an `aktiv` und\n`virtuelleRolle`. Wer nur bebuchbare Orte anzeigen will, filtert selbst.\n\nDie Tabellen werden bei Bedarf angelegt; ein frischer Mandant bekommt\neine leere Liste und keinen Fehler.\n\nUngegatet — jeder angemeldete Benutzer darf lesen. Das Buchen daneben\n(`POST /lager/buchung`) verlangt `manager`."}},"/api/v1/lager/pruefung":{"get":{"responses":{"200":{"description":"Pruefergebnis. `abweichungen` ist leer, wenn alles stimmt.","content":{"application/json":{"schema":{"type":"object","properties":{"inOrdnung":{"type":"boolean","description":"Keine Abweichung. Bei `geprueft: 0` trivial wahr."},"geprueft":{"type":"integer","description":"Verglichene Bestandszeilen."},"abweichungen":{"type":"array","items":{"type":"object","properties":{"artikelId":{"type":"string","format":"uuid"},"ortId":{"type":"string","format":"uuid"},"gefuehrt":{"type":"number","description":"Was im Bestand steht."},"ausJournal":{"type":"number","description":"Was die Bewegungen ergeben."},"abweichung":{"type":"number","description":"gefuehrt minus ausJournal, auf drei Stellen gerundet."}},"required":["artikelId","ortId","gefuehrt","ausJournal","abweichung"]}}},"required":["inOrdnung","geprueft","abweichungen"]},"example":{"inOrdnung":true,"geprueft":0,"abweichungen":[{"artikelId":"00000000-0000-4000-8000-000000000000","ortId":"00000000-0000-4000-8000-000000000000","gefuehrt":0,"ausJournal":0,"abweichung":0}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Fachlicher Fehler der Lagerlogik.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}}},"operationId":"getApiV1LagerPruefung","tags":["lager"],"parameters":[],"summary":"Bestand gegen das Bewegungsjournal pruefen","description":"Rechnet fuer jede Kombination aus Artikel, Ort und Charge die Summe\naller Bewegungen nach und stellt sie dem gefuehrten Bestand gegenueber.\nAbweichungen sind Hinweise auf verlorene oder doppelt gebuchte\nBewegungen. Es wird NICHTS korrigiert — die Route liest nur.\n\n`inOrdnung` ist die Kurzfassung von „keine Abweichung gefunden\". Achtung\nbei der Auslegung: in einem leeren Lager ist sie trivial `true`, weil\nnichts zu vergleichen war. Erst zusammen mit `geprueft > 0` ist sie eine\nAussage.\n\nDie Abfrage geht ueber das GESAMTE Bewegungsjournal, ohne Blaettern und\nohne Zeitfenster. Bei grossen Bestaenden ist der Aufruf entsprechend\nteuer; er gehoert in eine Pruefung, nicht in eine Seitenanzeige.\n\nUngegatet — jeder angemeldete Benutzer darf pruefen."}},"/api/v1/lager/buchung":{"post":{"responses":{"201":{"description":"Gebucht. Bestand und Journal sind geaendert.","content":{"application/json":{"schema":{"type":"object","properties":{"bewegungId":{"type":["string","null"],"format":"uuid"},"warnungen":{"type":"array","items":{"type":"string"},"description":"Heute nur der Hinweis auf einen entstandenen Fehlbestand."},"bereitsGebucht":{"type":"boolean","description":"Bei dieser Route immer false — der Schutz greift nur mit Belegbezug."}},"required":["bewegungId","warnungen","bereitsGebucht"]},"example":{"bewegungId":"00000000-0000-4000-8000-000000000000","warnungen":["string"],"bereitsGebucht":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"422":{"description":"Fachlich unmoeglich: `gleicher_ort` (Quelle = Ziel) oder `menge_ungueltig`. Es wurde nichts gebucht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"integer"}},"required":["error"]}}}}},"operationId":"postApiV1LagerBuchung","tags":["lager"],"parameters":[],"summary":"Eine Lagerbuchung ausfuehren — der Bestand aendert sich","description":"HIER WIRD WIRKLICH GEBUCHT. Der Aufruf schreibt eine Zeile ins\nBewegungsjournal UND schreibt beide Bestandszeilen fort: die Quelle um\n`menge` herunter, das Ziel um `menge` herauf. Beides laeuft in EINER\nTransaktion — entweder beides oder nichts.\n\nES IST IMMER EINE UMBUCHUNG ZWISCHEN ZWEI ORTEN. `vonOrtId` und `nachOrtId`\nsind Pflicht und muessen verschieden sein (sonst 422 `gleicher_ort`).\nBestand entsteht und verschwindet nie aus dem Nichts: einen Wareneingang\nbucht man GEGEN einen virtuellen Ort (Gegenkonto, siehe `virtuelleRolle`\nin `GET /lager/orte`).\n\n`menge` ist immer positiv; die Richtung ergibt sich aus den Orten.\n`vorgang` ist nur eine BESCHRIFTUNG des Journaleintrags — gerechnet wird\nbei allen vier Werten dasselbe. Die Journalwerte `storno` und `retoure`\nsind hier nicht waehlbar; sie entstehen nur aus Belegvorgaengen.\n\nNICHT STORNIERBAR. Es gibt keine Route, die eine Bewegung loescht oder\nzuruecknimmt. Eine Fehlbuchung wird durch eine GEGENBUCHUNG ausgeglichen:\nderselbe Aufruf mit vertauschten Orten. Danach stehen ZWEI Bewegungen im\nJournal, und der Bestand ist wieder wie vorher.\n\nNICHT WIEDERHOLUNGSSICHER. Der Doppelbuchungsschutz der Datenbank greift\nnur fuer Bewegungen MIT Belegbezug, und diese Route setzt keinen. Zwei\ngleiche Aufrufe erzeugen zwei Bewegungen und buchen doppelt. `bereitsGebucht`\nin der Antwort ist hier deshalb immer `false`.\n\nEIN FEHLBESTAND WIRD NICHT VERHINDERT. Reicht der Bestand am Quellort\nnicht, wird trotzdem gebucht — die Ware hat das Haus physisch verlassen,\ndas laesst sich durch Verweigern nicht rueckgaengig machen. Die Antwort\ntraegt dann eine Warnung, und der Bestand steht danach im Minus.\n\nWer gebucht hat, wird nur vermerkt, wenn der Aufrufer eine echte\nBenutzer-Kennung hat. Ein API-Schluessel wird als „niemand\" gespeichert;\ndie Buchung selbst geht trotzdem durch.\n\nAb Rolle `manager`, zusaetzlich greift die Modul-Wache `inventory`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"artikelId":{"type":"string","format":"uuid"},"vonOrtId":{"type":"string","format":"uuid"},"nachOrtId":{"type":"string","format":"uuid"},"menge":{"type":"number","exclusiveMinimum":0},"chargeId":{"type":["string","null"],"format":"uuid"},"vorgang":{"type":"string","enum":["eingang","ausgang","umlagerung","korrektur"],"default":"umlagerung"},"grund":{"type":"string","maxLength":500}},"required":["artikelId","vonOrtId","nachOrtId","menge"]},"example":{"artikelId":"00000000-0000-4000-8000-000000000000","vonOrtId":"00000000-0000-4000-8000-000000000000","nachOrtId":"00000000-0000-4000-8000-000000000000","menge":1,"chargeId":"00000000-0000-4000-8000-000000000000","vorgang":"eingang","grund":"string"}}}}}},"/api/v1/konditionen":{"get":{"responses":{"200":{"description":"Liste der Konditionen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Kondition"},"typ":{"type":"string","enum":["sale","purchase"],"description":"Verkaufs- oder Einkaufskondition"},"partnerId":{"type":["string","null"],"format":"uuid","description":"Kunde oder Lieferant; null = gilt fuer alle"},"partnerType":{"type":"string","description":"Geltungsbereich: customer, supplier, group oder all (keine Pruefung in der Tabelle)"},"artikelId":{"type":["string","null"],"format":"uuid","description":"Artikel; null = gilt fuer alle Artikel"},"artikelGruppe":{"type":["string","null"],"maxLength":100,"description":"Artikelgruppe statt Einzelartikel"},"preis":{"type":"number","minimum":0,"description":"Preis vor Rabatt, als Zahl"},"waehrung":{"type":"string","minLength":3,"maxLength":3,"description":"Waehrungscode (ISO-4217)"},"mengenMin":{"type":"number","minimum":0,"description":"Untere Mengengrenze der Staffel, einschliesslich"},"mengenMax":{"type":["number","null"],"minimum":0,"description":"Obere Mengengrenze; null = nach oben offen"},"gueltigVon":{"type":"string","description":"Beginn der Gueltigkeit als Datum (ISO)"},"gueltigBis":{"type":["string","null"],"description":"Ende der Gueltigkeit (ISO); null = unbefristet"},"rabattProzent":{"type":"number","minimum":0,"maximum":100,"description":"Rabatt in Prozent auf den Preis"},"version":{"type":"integer","minimum":1,"description":"Versionszaehler der Kondition, beginnt bei 1"},"vorgaengerId":{"type":["string","null"],"format":"uuid","description":"Die abgeloeste Vorgaengerversion"},"status":{"type":"string","description":"active, expired oder superseded (keine Pruefung in der Tabelle)"},"notizen":{"type":["string","null"],"description":"Freitext"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","typ","partnerId","partnerType","artikelId","artikelGruppe","preis","waehrung","mengenMin","mengenMax","gueltigVon","gueltigBis","rabattProzent","version","vorgaengerId","status","notizen","createdAt","updatedAt"],"additionalProperties":false},"description":"Die Konditionen dieser Seite"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angewendete Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Datensaetze"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer bei diesem Filter"}},"required":["limit","offset","total"],"additionalProperties":false,"description":"Seitenangaben — limit/offset, keine Seitennummer"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, in dem gesucht wurde"},"source":{"type":"string","const":"db","description":"Datenquelle; hier immer die Datenbank"}},"required":["tenantId","source"],"additionalProperties":false,"description":"Angaben zur Abfrage"}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","typ":"sale","partnerId":"00000000-0000-4000-8000-000000000000","partnerType":"string","artikelId":"00000000-0000-4000-8000-000000000000","artikelGruppe":"string","preis":0,"waehrung":"str","mengenMin":0,"mengenMax":0,"gueltigVon":"string","gueltigBis":"string","rabattProzent":0,"version":1,"vorgaengerId":"00000000-0000-4000-8000-000000000000","status":"string","notizen":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":1,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Konditionen","tags":["Konditionen"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"typ","schema":{"type":"string","enum":["sale","purchase"]}},{"in":"query","name":"partnerType","schema":{"type":"string","enum":["customer","supplier","group","all"]}},{"in":"query","name":"artikelId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","expired","superseded"]}},{"in":"query","name":"gueltigZum","schema":{"type":"string","format":"date"}}],"summary":"List conditions","description":"Listet alle Konditionen des Mandanten mit optionalen Filtern. Geblättert wird über limit/offset, nicht über eine Seitennummer. `gueltigZum` fragt punktgenau: geliefert wird, was an diesem Datum gilt. Die Tabelle wird beim ersten Zugriff angelegt — ein frischer Mandant bekommt eine leere Liste, keinen Fehler."},"post":{"responses":{"201":{"description":"Kondition angelegt — der neue Datensatz, inklusive Id und Version","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Kondition"},"typ":{"type":"string","enum":["sale","purchase"],"description":"Verkaufs- oder Einkaufskondition"},"partnerId":{"type":["string","null"],"format":"uuid","description":"Kunde oder Lieferant; null = gilt fuer alle"},"partnerType":{"type":"string","description":"Geltungsbereich: customer, supplier, group oder all (keine Pruefung in der Tabelle)"},"artikelId":{"type":["string","null"],"format":"uuid","description":"Artikel; null = gilt fuer alle Artikel"},"artikelGruppe":{"type":["string","null"],"maxLength":100,"description":"Artikelgruppe statt Einzelartikel"},"preis":{"type":"number","minimum":0,"description":"Preis vor Rabatt, als Zahl"},"waehrung":{"type":"string","minLength":3,"maxLength":3,"description":"Waehrungscode (ISO-4217)"},"mengenMin":{"type":"number","minimum":0,"description":"Untere Mengengrenze der Staffel, einschliesslich"},"mengenMax":{"type":["number","null"],"minimum":0,"description":"Obere Mengengrenze; null = nach oben offen"},"gueltigVon":{"type":"string","description":"Beginn der Gueltigkeit als Datum (ISO)"},"gueltigBis":{"type":["string","null"],"description":"Ende der Gueltigkeit (ISO); null = unbefristet"},"rabattProzent":{"type":"number","minimum":0,"maximum":100,"description":"Rabatt in Prozent auf den Preis"},"version":{"type":"integer","minimum":1,"description":"Versionszaehler der Kondition, beginnt bei 1"},"vorgaengerId":{"type":["string","null"],"format":"uuid","description":"Die abgeloeste Vorgaengerversion"},"status":{"type":"string","description":"active, expired oder superseded (keine Pruefung in der Tabelle)"},"notizen":{"type":["string","null"],"description":"Freitext"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","typ","partnerId","partnerType","artikelId","artikelGruppe","preis","waehrung","mengenMin","mengenMax","gueltigVon","gueltigBis","rabattProzent","version","vorgaengerId","status","notizen","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","typ":"sale","partnerId":"00000000-0000-4000-8000-000000000000","partnerType":"string","artikelId":"00000000-0000-4000-8000-000000000000","artikelGruppe":"string","preis":0,"waehrung":"str","mengenMin":0,"mengenMax":0,"gueltigVon":"string","gueltigBis":"string","rabattProzent":0,"version":1,"vorgaengerId":"00000000-0000-4000-8000-000000000000","status":"string","notizen":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Konditionen","tags":["Konditionen"],"parameters":[],"description":"Legt eine neue Kondition an. Ist `vorgaengerId` gesetzt, wird der alte Eintrag auf `superseded` gesetzt und die Version um eins erhöht. Zeigt `vorgaengerId` ins Leere, entsteht die Kondition trotzdem — mit Version 1 und ohne Fehlermeldung. Beide Schritte laufen nacheinander, nicht in einer gemeinsamen Transaktion.","summary":"Create condition","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"typ":{"type":"string","enum":["sale","purchase"]},"partnerId":{"type":["string","null"],"format":"uuid","default":null},"partnerType":{"type":"string","enum":["customer","supplier","group","all"],"default":"all"},"artikelId":{"type":["string","null"],"format":"uuid"},"artikelGruppe":{"type":["string","null"],"maxLength":100},"preis":{"type":"number","minimum":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"mengenMin":{"type":"number","minimum":0,"default":1},"mengenMax":{"type":["number","null"],"minimum":0},"gueltigVon":{"type":"string","format":"date"},"gueltigBis":{"type":["string","null"],"format":"date"},"rabattProzent":{"type":"number","minimum":0,"maximum":100,"default":0},"vorgaengerId":{"type":["string","null"],"format":"uuid"},"notizen":{"type":["string","null"]}},"required":["typ","preis","gueltigVon"]},"example":{"typ":"sale","partnerId":"00000000-0000-4000-8000-000000000000","partnerType":"customer","artikelId":"00000000-0000-4000-8000-000000000000","artikelGruppe":"string","preis":0,"waehrung":"str","mengenMin":0,"mengenMax":0,"gueltigVon":"2026-01-01","gueltigBis":"2026-01-01","rabattProzent":0,"vorgaengerId":"00000000-0000-4000-8000-000000000000","notizen":"string"}}}}}},"/api/v1/konditionen/stats":{"get":{"responses":{"200":{"description":"Konditionen-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"aktiveGesamt":{"type":"integer","minimum":0,"description":"Konditionen im Status active"},"abgelaufenGesamt":{"type":"integer","minimum":0,"description":"Konditionen im Status expired"},"aktiveSale":{"type":"integer","minimum":0,"description":"Davon Verkaufskonditionen"},"aktivePurchase":{"type":"integer","minimum":0,"description":"Davon Einkaufskonditionen"},"staffelnSale":{"type":"integer","minimum":0,"description":"Aktive Verkaufskonditionen mit Mengenstaffel (mengenMin groesser 1)"},"staffelnPurchase":{"type":"integer","minimum":0,"description":"Aktive Einkaufskonditionen mit Mengenstaffel (mengenMin groesser 1)"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, fuer den gezaehlt wurde"},"source":{"type":"string","const":"db","description":"Datenquelle; hier immer die Datenbank"},"asOf":{"type":"string","description":"Stichtag der Zaehlung (ISO-Datum)"}},"required":["tenantId","source","asOf"],"additionalProperties":false,"description":"Angaben zur Abfrage"}},"required":["aktiveGesamt","abgelaufenGesamt","aktiveSale","aktivePurchase","staffelnSale","staffelnPurchase","meta"],"additionalProperties":false},"example":{"aktiveGesamt":0,"abgelaufenGesamt":0,"aktiveSale":0,"aktivePurchase":0,"staffelnSale":0,"staffelnPurchase":0,"meta":{"tenantId":"string","source":"db","asOf":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KonditionenStats","tags":["Konditionen"],"parameters":[],"summary":"Condition key figures","description":"Kennzahlen zu Konditionen: aktive, abgelaufene und aktive Mengenstaffeln je Typ. Als Staffel zählt eine Kondition mit einer unteren Mengengrenze über 1. Konditionen im Status `superseded` tauchen in keiner der sechs Zahlen auf."}},"/api/v1/konditionen/lookup":{"get":{"responses":{"200":{"description":"Bester Preis gefunden — die Kondition plus drei Rechenfelder","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Kondition"},"typ":{"type":"string","enum":["sale","purchase"],"description":"Verkaufs- oder Einkaufskondition"},"partnerId":{"type":["string","null"],"format":"uuid","description":"Kunde oder Lieferant; null = gilt fuer alle"},"partnerType":{"type":"string","description":"Geltungsbereich: customer, supplier, group oder all (keine Pruefung in der Tabelle)"},"artikelId":{"type":["string","null"],"format":"uuid","description":"Artikel; null = gilt fuer alle Artikel"},"artikelGruppe":{"type":["string","null"],"maxLength":100,"description":"Artikelgruppe statt Einzelartikel"},"preis":{"type":"number","minimum":0,"description":"Preis vor Rabatt, als Zahl"},"waehrung":{"type":"string","minLength":3,"maxLength":3,"description":"Waehrungscode (ISO-4217)"},"mengenMin":{"type":"number","minimum":0,"description":"Untere Mengengrenze der Staffel, einschliesslich"},"mengenMax":{"type":["number","null"],"minimum":0,"description":"Obere Mengengrenze; null = nach oben offen"},"gueltigVon":{"type":"string","description":"Beginn der Gueltigkeit als Datum (ISO)"},"gueltigBis":{"type":["string","null"],"description":"Ende der Gueltigkeit (ISO); null = unbefristet"},"rabattProzent":{"type":"number","minimum":0,"maximum":100,"description":"Rabatt in Prozent auf den Preis"},"version":{"type":"integer","minimum":1,"description":"Versionszaehler der Kondition, beginnt bei 1"},"vorgaengerId":{"type":["string","null"],"format":"uuid","description":"Die abgeloeste Vorgaengerversion"},"status":{"type":"string","description":"active, expired oder superseded (keine Pruefung in der Tabelle)"},"notizen":{"type":["string","null"],"description":"Freitext"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO)"},"effektivPreis":{"type":"number","minimum":0,"description":"Preis nach Rabatt, auf vier Nachkommastellen gerundet"},"lookupMenge":{"type":"number","minimum":0,"description":"Die abgefragte Menge"},"lookupDatum":{"type":"string","description":"Der abgefragte Stichtag (ISO-Datum)"}},"required":["id","typ","partnerId","partnerType","artikelId","artikelGruppe","preis","waehrung","mengenMin","mengenMax","gueltigVon","gueltigBis","rabattProzent","version","vorgaengerId","status","notizen","createdAt","updatedAt","effektivPreis","lookupMenge","lookupDatum"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","typ":"sale","partnerId":"00000000-0000-4000-8000-000000000000","partnerType":"string","artikelId":"00000000-0000-4000-8000-000000000000","artikelGruppe":"string","preis":0,"waehrung":"str","mengenMin":0,"mengenMax":0,"gueltigVon":"string","gueltigBis":"string","rabattProzent":0,"version":1,"vorgaengerId":"00000000-0000-4000-8000-000000000000","status":"string","notizen":"string","createdAt":"string","updatedAt":"string","effektivPreis":0,"lookupMenge":0,"lookupDatum":"string"}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Keine passende Kondition gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"no_kondition_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KonditionenLookup","tags":["Konditionen"],"parameters":[{"in":"query","name":"typ","schema":{"type":"string","enum":["sale","purchase"]},"required":true},{"in":"query","name":"partnerId","schema":{"type":"string","format":"uuid"},"required":false},{"in":"query","name":"artikelId","schema":{"type":"string","format":"uuid"},"required":false},{"in":"query","name":"menge","schema":{"type":"number","minimum":0,"default":1},"required":false},{"in":"query","name":"datum","schema":{"type":"string","format":"date"},"required":false}],"description":"Liefert den besten Preis für typ+partner+artikel+menge zum Stichtag nach Staffel-Logik. Priorisierung: partner-spezifisch > partner_type-Gruppe > all. Innerhalb gleicher Spezifität: spezifischste Mengenstaffel gewinnt. Ohne `datum` gilt heute. Die Antwort ist die gefundene Kondition, erweitert um den gerechneten Preis und die Abfragewerte.","summary":"Look up best price"}},"/api/v1/konditionen/{id}":{"get":{"responses":{"200":{"description":"Konditionen-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Kondition"},"typ":{"type":"string","enum":["sale","purchase"],"description":"Verkaufs- oder Einkaufskondition"},"partnerId":{"type":["string","null"],"format":"uuid","description":"Kunde oder Lieferant; null = gilt fuer alle"},"partnerType":{"type":"string","description":"Geltungsbereich: customer, supplier, group oder all (keine Pruefung in der Tabelle)"},"artikelId":{"type":["string","null"],"format":"uuid","description":"Artikel; null = gilt fuer alle Artikel"},"artikelGruppe":{"type":["string","null"],"maxLength":100,"description":"Artikelgruppe statt Einzelartikel"},"preis":{"type":"number","minimum":0,"description":"Preis vor Rabatt, als Zahl"},"waehrung":{"type":"string","minLength":3,"maxLength":3,"description":"Waehrungscode (ISO-4217)"},"mengenMin":{"type":"number","minimum":0,"description":"Untere Mengengrenze der Staffel, einschliesslich"},"mengenMax":{"type":["number","null"],"minimum":0,"description":"Obere Mengengrenze; null = nach oben offen"},"gueltigVon":{"type":"string","description":"Beginn der Gueltigkeit als Datum (ISO)"},"gueltigBis":{"type":["string","null"],"description":"Ende der Gueltigkeit (ISO); null = unbefristet"},"rabattProzent":{"type":"number","minimum":0,"maximum":100,"description":"Rabatt in Prozent auf den Preis"},"version":{"type":"integer","minimum":1,"description":"Versionszaehler der Kondition, beginnt bei 1"},"vorgaengerId":{"type":["string","null"],"format":"uuid","description":"Die abgeloeste Vorgaengerversion"},"status":{"type":"string","description":"active, expired oder superseded (keine Pruefung in der Tabelle)"},"notizen":{"type":["string","null"],"description":"Freitext"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","typ","partnerId","partnerType","artikelId","artikelGruppe","preis","waehrung","mengenMin","mengenMax","gueltigVon","gueltigBis","rabattProzent","version","vorgaengerId","status","notizen","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","typ":"sale","partnerId":"00000000-0000-4000-8000-000000000000","partnerType":"string","artikelId":"00000000-0000-4000-8000-000000000000","artikelGruppe":"string","preis":0,"waehrung":"str","mengenMin":0,"mengenMax":0,"gueltigVon":"string","gueltigBis":"string","rabattProzent":0,"version":1,"vorgaengerId":"00000000-0000-4000-8000-000000000000","status":"string","notizen":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Kondition nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kondition_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KonditionenById","tags":["Konditionen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get condition","description":"Liefert eine einzelne Kondition — auch abgelaufene und abgelöste, es wird nicht nach Status gefiltert."},"put":{"responses":{"200":{"description":"Kondition aktualisiert — der Datensatz nach der Änderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Kondition"},"typ":{"type":"string","enum":["sale","purchase"],"description":"Verkaufs- oder Einkaufskondition"},"partnerId":{"type":["string","null"],"format":"uuid","description":"Kunde oder Lieferant; null = gilt fuer alle"},"partnerType":{"type":"string","description":"Geltungsbereich: customer, supplier, group oder all (keine Pruefung in der Tabelle)"},"artikelId":{"type":["string","null"],"format":"uuid","description":"Artikel; null = gilt fuer alle Artikel"},"artikelGruppe":{"type":["string","null"],"maxLength":100,"description":"Artikelgruppe statt Einzelartikel"},"preis":{"type":"number","minimum":0,"description":"Preis vor Rabatt, als Zahl"},"waehrung":{"type":"string","minLength":3,"maxLength":3,"description":"Waehrungscode (ISO-4217)"},"mengenMin":{"type":"number","minimum":0,"description":"Untere Mengengrenze der Staffel, einschliesslich"},"mengenMax":{"type":["number","null"],"minimum":0,"description":"Obere Mengengrenze; null = nach oben offen"},"gueltigVon":{"type":"string","description":"Beginn der Gueltigkeit als Datum (ISO)"},"gueltigBis":{"type":["string","null"],"description":"Ende der Gueltigkeit (ISO); null = unbefristet"},"rabattProzent":{"type":"number","minimum":0,"maximum":100,"description":"Rabatt in Prozent auf den Preis"},"version":{"type":"integer","minimum":1,"description":"Versionszaehler der Kondition, beginnt bei 1"},"vorgaengerId":{"type":["string","null"],"format":"uuid","description":"Die abgeloeste Vorgaengerversion"},"status":{"type":"string","description":"active, expired oder superseded (keine Pruefung in der Tabelle)"},"notizen":{"type":["string","null"],"description":"Freitext"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO)"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO)"}},"required":["id","typ","partnerId","partnerType","artikelId","artikelGruppe","preis","waehrung","mengenMin","mengenMax","gueltigVon","gueltigBis","rabattProzent","version","vorgaengerId","status","notizen","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","typ":"sale","partnerId":"00000000-0000-4000-8000-000000000000","partnerType":"string","artikelId":"00000000-0000-4000-8000-000000000000","artikelGruppe":"string","preis":0,"waehrung":"str","mengenMin":0,"mengenMax":0,"gueltigVon":"string","gueltigBis":"string","rabattProzent":0,"version":1,"vorgaengerId":"00000000-0000-4000-8000-000000000000","status":"string","notizen":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Kondition nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kondition_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1KonditionenById","tags":["Konditionen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update condition","description":"Ändert eine Kondition. Nicht mitgeschickte Felder bleiben auf ihrem bisherigen Wert — der Handler liest den Datensatz vorher und setzt ihn wieder ein. Die Versionsnummer bleibt dabei unverändert: das hier ist eine Korrektur, kein Versionswechsel. Für eine neue Version: POST mit `vorgaengerId`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"typ":{"type":"string","enum":["sale","purchase"]},"partnerId":{"type":["string","null"],"format":"uuid","default":null},"partnerType":{"type":"string","enum":["customer","supplier","group","all"],"default":"all"},"artikelId":{"type":["string","null"],"format":"uuid"},"artikelGruppe":{"type":["string","null"],"maxLength":100},"preis":{"type":"number","minimum":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"mengenMin":{"type":"number","minimum":0,"default":1},"mengenMax":{"type":["number","null"],"minimum":0},"gueltigVon":{"type":"string","format":"date"},"gueltigBis":{"type":["string","null"],"format":"date"},"rabattProzent":{"type":"number","minimum":0,"maximum":100,"default":0},"vorgaengerId":{"type":["string","null"],"format":"uuid"},"notizen":{"type":["string","null"]},"status":{"type":"string","enum":["active","expired","superseded"]}}},"example":{"typ":"sale","partnerId":"00000000-0000-4000-8000-000000000000","partnerType":"customer","artikelId":"00000000-0000-4000-8000-000000000000","artikelGruppe":"string","preis":0,"waehrung":"str","mengenMin":0,"mengenMax":0,"gueltigVon":"2026-01-01","gueltigBis":"2026-01-01","rabattProzent":0,"vorgaengerId":"00000000-0000-4000-8000-000000000000","notizen":"string","status":"active"}}}}},"delete":{"responses":{"200":{"description":"Kondition deaktiviert — nur eine Quittung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","minLength":1,"description":"Erfolgsmeldung im Klartext, enthaelt die Id"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Kondition nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kondition_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1KonditionenById","tags":["Konditionen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Deactivate condition","description":"Setzt eine Kondition auf `expired`. Der Datensatz bleibt bestehen und ist über GET /konditionen/{id} weiter lesbar; nur die Preisermittlung übergeht ihn ab jetzt. Ein bereits abgelaufener Eintrag lässt sich erneut deaktivieren, ohne dass sich etwas ändert."}},"/api/v1/rahmen/auftraege":{"get":{"responses":{"200":{"description":"Liste der Rahmenaufträge","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"customerId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1RahmenAuftraege","tags":["Rahmen"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","active","expired","completed","cancelled"]}},{"in":"query","name":"customerId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"supplierId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"laufendAm","schema":{"type":"string","format":"date"}}],"summary":"List framework orders","description":"Listet Rahmenaufträge des Mandanten"},"post":{"responses":{"201":{"description":"Rahmenauftrag angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"customerId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1RahmenAuftraege","tags":["Rahmen"],"parameters":[],"summary":"Create framework order","description":"Legt einen neuen Rahmenauftrag an. Die Rahmennummer (RA-JJJJ-NNNN) vergibt der Server; abgerufene Menge und Abruf-Historie starten bei 0 bzw. leer.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"titel":{"type":"string","maxLength":255},"notizen":{"type":"string"},"gesamtMenge":{"type":"number","minimum":0},"gesamtWert":{"type":"number","minimum":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"laufzeitVon":{"type":"string","format":"date"},"laufzeitBis":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","active","expired","completed","cancelled"],"default":"draft"},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":"string","format":"uuid"},"description":{"type":"string","minLength":1},"quantity":{"type":"number","exclusiveMinimum":0},"unitPrice":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"}},"required":["description","quantity","unitPrice"]},"default":[]}},"required":["customerId"]},"example":{"customerId":"00000000-0000-4000-8000-000000000000","titel":"string","notizen":"string","gesamtMenge":0,"gesamtWert":0,"waehrung":"str","laufzeitVon":"2026-01-01","laufzeitBis":"2026-01-01","status":"draft","items":[{"articleId":"00000000-0000-4000-8000-000000000000","description":"string","quantity":1,"unitPrice":0,"unit":"string"}]}}}}}},"/api/v1/rahmen/auftraege/{id}":{"get":{"responses":{"200":{"description":"Rahmenauftrag-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"customerId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenauftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1RahmenAuftraegeById","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get framework order","description":"Liefert einen Rahmenauftrag mit kompletter Abruf-Historie"},"put":{"responses":{"200":{"description":"Rahmenauftrag aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"customerId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenauftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1RahmenAuftraegeById","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace framework order","description":"Ersetzt einen Rahmenauftrag vollständig. Jedes Feld des Rumpfes wird geschrieben, auch die Positionsliste — nicht mitgeschickte Positionen sind danach weg. Abgerufene Menge und Abruf-Historie bleiben unberührt. Der Status wird ungeprüft gesetzt: anders als /cancel gilt hier keine Übergangsregel.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"titel":{"type":"string","maxLength":255},"notizen":{"type":"string"},"gesamtMenge":{"type":"number","minimum":0},"gesamtWert":{"type":"number","minimum":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"laufzeitVon":{"type":"string","format":"date"},"laufzeitBis":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","active","expired","completed","cancelled"],"default":"draft"},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":"string","format":"uuid"},"description":{"type":"string","minLength":1},"quantity":{"type":"number","exclusiveMinimum":0},"unitPrice":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"}},"required":["description","quantity","unitPrice"]},"default":[]}},"required":["customerId"]},"example":{"customerId":"00000000-0000-4000-8000-000000000000","titel":"string","notizen":"string","gesamtMenge":0,"gesamtWert":0,"waehrung":"str","laufzeitVon":"2026-01-01","laufzeitBis":"2026-01-01","status":"draft","items":[{"articleId":"00000000-0000-4000-8000-000000000000","description":"string","quantity":1,"unitPrice":0,"unit":"string"}]}}}}}},"/api/v1/rahmen/auftraege/{id}/abruf":{"post":{"responses":{"200":{"description":"Abruf gebucht","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"customerId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"400":{"description":"Menge überschreitet Restmenge","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenauftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1RahmenAuftraegeByIdAbruf","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Book call-off on framework order","description":"Bucht einen Abruf auf einen Rahmenauftrag — erhöht abgerufene_menge und appended abrufe[]. Antwortet 200 mit dem Rahmenauftrag, nicht 201: der Abruf ist ein Eintrag im Beleg, keine eigene Ressource. Die Restmengen-Prüfung greift nur, wenn eine Gesamtmenge hinterlegt ist — ohne sie ist die Abrufmenge unbegrenzt. Eine mitgeschickte orderId wird ungeprüft übernommen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"menge":{"type":"number","exclusiveMinimum":0},"wert":{"type":"number","minimum":0},"orderId":{"type":"string"}},"required":["menge","wert"]},"example":{"menge":1,"wert":0,"orderId":"string"}}}}}},"/api/v1/rahmen/auftraege/{id}/cancel":{"post":{"responses":{"200":{"description":"Rahmenauftrag storniert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"customerId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenauftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Rahmenauftrag bereits abgeschlossen/storniert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1RahmenAuftraegeByIdCancel","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Cancel framework order","description":"Setzt den Rahmenauftrag auf Status \"cancelled\" und liefert ihn zurück. Die Zeile bleibt erhalten, es wird nichts gelöscht. Ist der Status bereits \"cancelled\" oder \"completed\", antwortet der Aufruf 409 und ändert nichts."}},"/api/v1/rahmen/auftraege/{id}/restmengen":{"get":{"responses":{"200":{"description":"Restmengen pro Position","content":{"application/json":{"schema":{"type":"object","properties":{"rahmenId":{"type":"string"},"rahmenNumber":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"positionen":{"type":"array","items":{"type":"object","properties":{"position":{"type":"number"},"articleId":{"type":["string","null"]},"description":{"type":"string"},"unit":{"type":"string"},"geplanteMenge":{"type":"number"},"unitPrice":{"type":"number"},"abgerufenemenge":{"type":"number"},"restMenge":{"type":"number"}},"required":["position","articleId","description","unit","geplanteMenge","unitPrice","abgerufenemenge","restMenge"]}}},"required":["rahmenId","gesamtMenge","abgerufenemenge","restMenge","positionen"],"additionalProperties":false},"example":{"rahmenId":"string","gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"positionen":[{"position":0,"articleId":"string","description":"string","unit":"string","geplanteMenge":0,"unitPrice":0,"abgerufenemenge":0,"restMenge":0}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenauftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1RahmenAuftraegeByIdRestmengen","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get remaining quantities of framework order","description":"Aufschlüsselung der Restmenge pro Position im Rahmenauftrag. Die Zahlen je Position sind GESCHÄTZT: es wird keine Abrufmenge pro Position geführt, die Gesamt-Abrufmenge wird anteilig nach geplanter Menge verteilt. Nur die Summenzeile ist exakt."}},"/api/v1/rahmen/bestellungen":{"get":{"responses":{"200":{"description":"Liste der Rahmenbestellungen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"supplierId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1RahmenBestellungen","tags":["Rahmen"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","active","expired","completed","cancelled"]}},{"in":"query","name":"customerId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"supplierId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"laufendAm","schema":{"type":"string","format":"date"}}],"summary":"List framework purchase orders","description":"Listet Rahmenbestellungen des Mandanten"},"post":{"responses":{"201":{"description":"Rahmenbestellung angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"supplierId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1RahmenBestellungen","tags":["Rahmen"],"parameters":[],"summary":"Create framework purchase order","description":"Legt eine neue Rahmenbestellung an. Die Rahmennummer (RB-JJJJ-NNNN) vergibt der Server; abgerufene Menge und Abruf-Historie starten bei 0 bzw. leer.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"supplierId":{"type":"string","format":"uuid"},"titel":{"type":"string","maxLength":255},"notizen":{"type":"string"},"gesamtMenge":{"type":"number","minimum":0},"gesamtWert":{"type":"number","minimum":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"laufzeitVon":{"type":"string","format":"date"},"laufzeitBis":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","active","expired","completed","cancelled"],"default":"draft"},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":"string","format":"uuid"},"description":{"type":"string","minLength":1},"quantity":{"type":"number","exclusiveMinimum":0},"unitPrice":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"}},"required":["description","quantity","unitPrice"]},"default":[]}},"required":["supplierId"]},"example":{"supplierId":"00000000-0000-4000-8000-000000000000","titel":"string","notizen":"string","gesamtMenge":0,"gesamtWert":0,"waehrung":"str","laufzeitVon":"2026-01-01","laufzeitBis":"2026-01-01","status":"draft","items":[{"articleId":"00000000-0000-4000-8000-000000000000","description":"string","quantity":1,"unitPrice":0,"unit":"string"}]}}}}}},"/api/v1/rahmen/bestellungen/{id}":{"get":{"responses":{"200":{"description":"Rahmenbestellung-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"supplierId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenbestellung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1RahmenBestellungenById","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get framework purchase order","description":"Liefert eine Rahmenbestellung mit kompletter Abruf-Historie"},"put":{"responses":{"200":{"description":"Rahmenbestellung aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"supplierId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenbestellung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1RahmenBestellungenById","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Replace framework purchase order","description":"Ersetzt eine Rahmenbestellung vollständig. Jedes Feld des Rumpfes wird geschrieben, auch die Positionsliste — nicht mitgeschickte Positionen sind danach weg. Abgerufene Menge und Abruf-Historie bleiben unberührt. Der Status wird ungeprüft gesetzt: anders als /cancel gilt hier keine Übergangsregel.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"supplierId":{"type":"string","format":"uuid"},"titel":{"type":"string","maxLength":255},"notizen":{"type":"string"},"gesamtMenge":{"type":"number","minimum":0},"gesamtWert":{"type":"number","minimum":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"laufzeitVon":{"type":"string","format":"date"},"laufzeitBis":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","active","expired","completed","cancelled"],"default":"draft"},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":"string","format":"uuid"},"description":{"type":"string","minLength":1},"quantity":{"type":"number","exclusiveMinimum":0},"unitPrice":{"type":"number","minimum":0},"unit":{"type":"string","default":"Stk"}},"required":["description","quantity","unitPrice"]},"default":[]}},"required":["supplierId"]},"example":{"supplierId":"00000000-0000-4000-8000-000000000000","titel":"string","notizen":"string","gesamtMenge":0,"gesamtWert":0,"waehrung":"str","laufzeitVon":"2026-01-01","laufzeitBis":"2026-01-01","status":"draft","items":[{"articleId":"00000000-0000-4000-8000-000000000000","description":"string","quantity":1,"unitPrice":0,"unit":"string"}]}}}}}},"/api/v1/rahmen/bestellungen/{id}/abruf":{"post":{"responses":{"200":{"description":"Abruf gebucht","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"supplierId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"400":{"description":"Menge überschreitet Restmenge","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenbestellung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1RahmenBestellungenByIdAbruf","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Book call-off on framework purchase order","description":"Bucht einen Abruf auf eine Rahmenbestellung — erhöht abgerufene_menge und appended abrufe[]. Antwortet 200 mit der Rahmenbestellung, nicht 201: der Abruf ist ein Eintrag im Beleg, keine eigene Ressource. Die Restmengen-Prüfung greift nur, wenn eine Gesamtmenge hinterlegt ist — ohne sie ist die Abrufmenge unbegrenzt. Eine mitgeschickte orderId wird ungeprüft übernommen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"menge":{"type":"number","exclusiveMinimum":0},"wert":{"type":"number","minimum":0},"orderId":{"type":"string"}},"required":["menge","wert"]},"example":{"menge":1,"wert":0,"orderId":"string"}}}}}},"/api/v1/rahmen/bestellungen/{id}/cancel":{"post":{"responses":{"200":{"description":"Rahmenbestellung storniert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"rahmenNumber":{},"titel":{},"notizen":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"gesamtWert":{"type":["number","null"]},"waehrung":{},"laufzeitVon":{},"laufzeitBis":{},"status":{},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":["string","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"unit":{"type":"string"}},"required":["description","quantity","unitPrice"]}},"abrufe":{"type":"array","items":{"type":"object","properties":{"orderId":{"type":["string","null"]},"abrufDatum":{"type":"string"},"menge":{"type":"number"},"wert":{"type":"number"}},"required":["orderId","abrufDatum","menge","wert"]}},"createdAt":{},"updatedAt":{},"supplierId":{}},"required":["gesamtMenge","abgerufenemenge","restMenge","gesamtWert","items","abrufe"],"additionalProperties":false},"example":{"gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"gesamtWert":0,"items":[{"articleId":"string","description":"string","quantity":0,"unitPrice":0,"unit":"string"}],"abrufe":[{"orderId":"string","abrufDatum":"string","menge":0,"wert":0}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenbestellung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Rahmenbestellung bereits abgeschlossen/storniert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1RahmenBestellungenByIdCancel","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Cancel framework purchase order","description":"Setzt die Rahmenbestellung auf Status \"cancelled\" und liefert sie zurück. Die Zeile bleibt erhalten, es wird nichts gelöscht. Ist der Status bereits \"cancelled\" oder \"completed\", antwortet der Aufruf 409 und ändert nichts."}},"/api/v1/rahmen/bestellungen/{id}/restmengen":{"get":{"responses":{"200":{"description":"Restmengen pro Position","content":{"application/json":{"schema":{"type":"object","properties":{"rahmenId":{"type":"string"},"rahmenNumber":{},"gesamtMenge":{"type":["number","null"]},"abgerufenemenge":{"type":"number"},"restMenge":{"type":["number","null"]},"positionen":{"type":"array","items":{"type":"object","properties":{"position":{"type":"number"},"articleId":{"type":["string","null"]},"description":{"type":"string"},"unit":{"type":"string"},"geplanteMenge":{"type":"number"},"unitPrice":{"type":"number"},"abgerufenemenge":{"type":"number"},"restMenge":{"type":"number"}},"required":["position","articleId","description","unit","geplanteMenge","unitPrice","abgerufenemenge","restMenge"]}}},"required":["rahmenId","gesamtMenge","abgerufenemenge","restMenge","positionen"],"additionalProperties":false},"example":{"rahmenId":"string","gesamtMenge":0,"abgerufenemenge":0,"restMenge":0,"positionen":[{"position":0,"articleId":"string","description":"string","unit":"string","geplanteMenge":0,"unitPrice":0,"abgerufenemenge":0,"restMenge":0}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenbestellung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1RahmenBestellungenByIdRestmengen","tags":["Rahmen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get remaining quantities of framework purchase order","description":"Aufschlüsselung der Restmenge pro Position in der Rahmenbestellung. Die Zahlen je Position sind GESCHÄTZT: es wird keine Abrufmenge pro Position geführt, die Gesamt-Abrufmenge wird anteilig nach geplanter Menge verteilt. Nur die Summenzeile ist exakt."}},"/api/v1/rahmen/stats":{"get":{"responses":{"200":{"description":"Rahmen-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"auftraege":{"type":"object","properties":{"aktiv":{"type":"number"},"auslaufend30Tage":{"type":"number"},"ausgeschoepft":{"type":"number"},"ausgeschoepftProzent":{"type":"number"},"gesamtwert":{"type":"number"}},"required":["aktiv","auslaufend30Tage","ausgeschoepft","ausgeschoepftProzent","gesamtwert"]},"bestellungen":{"type":"object","properties":{"aktiv":{"type":"number"},"auslaufend30Tage":{"type":"number"},"ausgeschoepft":{"type":"number"},"ausgeschoepftProzent":{"type":"number"},"gesamtwert":{"type":"number"}},"required":["aktiv","auslaufend30Tage","ausgeschoepft","ausgeschoepftProzent","gesamtwert"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["auftraege","bestellungen","meta"],"additionalProperties":false},"example":{"auftraege":{"aktiv":0,"auslaufend30Tage":0,"ausgeschoepft":0,"ausgeschoepftProzent":0,"gesamtwert":0},"bestellungen":{"aktiv":0,"auslaufend30Tage":0,"ausgeschoepft":0,"ausgeschoepftProzent":0,"gesamtwert":0},"meta":{"source":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1RahmenStats","tags":["Rahmen"],"parameters":[],"summary":"Get framework contract statistics","description":"KPIs zu Rahmenaufträgen und Rahmenbestellungen (aktiv, ausgeschöpft, auslaufend). Alle Kennzahlen zählen ausschließlich Belege im Status \"active\"."}},"/api/v1/rahmenauftraege/{id}/abrufe":{"post":{"responses":{"201":{"description":"Abruf gebucht. ACHTUNG: diese Form hat escalator_aufschlag_pct, total_abgerufen und restmenge, dafür KEIN created_by — anders als ein Abruf aus der Liste.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Abrufs"},"rahmenauftrag_id":{"type":"string","format":"uuid","description":"Rahmenauftrag, gegen den abgerufen wurde"},"position_id":{"type":["string","null"],"description":"Position im Rahmen; null = Abruf gegen den Rahmen als Ganzes"},"menge":{"type":"number","exclusiveMinimum":0,"description":"Abgerufene Menge"},"preis_lock":{"type":"number","description":"Festgeschriebener Einzelpreis inklusive Aufschlag — spaetere Regeln aendern ihn nicht mehr"},"abgerufen_am":{"type":"string","description":"Zeitpunkt des Abrufs (ISO)"},"kunden_auftrag_id":{"type":["string","null"],"description":"Verknuepfter Kundenauftrag, falls angegeben"},"escalator_basis":{"type":["number","null"],"description":"Preis VOR dem Aufschlag; zusammen mit preis_lock der Nachweis der Rechnung"},"escalator_aufschlag_pct":{"type":"number","description":"Angewendeter Aufschlag in Prozent; 0, wenn keine Regel griff"},"total_abgerufen":{"type":"number","description":"Summe ALLER Abrufe des Rahmens nach diesem Abruf, ueber alle Positionen"},"restmenge":{"type":["number","null"],"description":"Verbleibende Gesamtmenge; null, wenn der Rahmen keine Gesamtmenge fuehrt"},"notes":{"type":["string","null"],"description":"Notiz zum Abruf"}},"required":["id","rahmenauftrag_id","position_id","menge","preis_lock","abgerufen_am","kunden_auftrag_id","escalator_basis","escalator_aufschlag_pct","total_abgerufen","restmenge","notes"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","rahmenauftrag_id":"00000000-0000-4000-8000-000000000000","position_id":"string","menge":1,"preis_lock":0,"abgerufen_am":"string","kunden_auftrag_id":"string","escalator_basis":0,"escalator_aufschlag_pct":0,"total_abgerufen":0,"restmenge":0,"notes":"string"}}}},"400":{"description":"Menge überschreitet die Restmenge — der Rumpf nennt, wie viel noch geht","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"menge_exceeds_position","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"},"restmenge_position":{"type":"number","description":"Was diese POSITION noch hergibt"}},"required":["error","message","restmenge_position"],"additionalProperties":false,"description":"Gegen eine einzelne Position geprueft"},{"type":"object","properties":{"error":{"type":"string","const":"menge_exceeds_rahmen","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"},"restmenge":{"type":"number","description":"Was der RAHMEN insgesamt noch hergibt"}},"required":["error","message","restmenge"],"additionalProperties":false,"description":"Gegen den Rahmen als Ganzes geprueft"}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenauftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rahmenauftrag_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1RahmenauftraegeByIdAbrufe","tags":["RahmenAbrufe"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Bucht einen Abruf gegen einen Rahmenauftrag. Validiert Σ ≤ Gesamtmenge (per Position falls position_id gesetzt), berechnet effektiven Preis (Basis + ggf. Escalator-Aufschlag) und legt den Abruf in rahmenauftrag_abrufe an. preis_lock = effektiver Preis, escalator_basis = original Rahmen-Preis vor Eskalation. Der Preis wird festgeschrieben: eine später angelegte Preisgleitregel ändert ihn nicht mehr. Führt der Rahmen weder Positionen noch eine Gesamtmenge, wird die Menge NICHT begrenzt und der Basispreis ist 0. Das Feld auto_create_order wird entgegengenommen, aber nicht ausgewertet — es entsteht kein Auftrag.","summary":"Book call-off","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"position_id":{"type":"string"},"menge":{"type":"number","exclusiveMinimum":0},"kunden_auftrag_id":{"type":"string"},"notes":{"type":"string","maxLength":2000},"auto_create_order":{"type":"boolean","default":false}},"required":["menge"]},"example":{"position_id":"string","menge":1,"kunden_auftrag_id":"string","notes":"string","auto_create_order":true}}}}},"get":{"responses":{"200":{"description":"Liste mit Aggregaten. Die Abrufe hier tragen created_by, aber weder escalator_aufschlag_pct noch restmenge — anders als beim Anlegen.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Abrufs"},"rahmenauftrag_id":{"type":"string","format":"uuid","description":"Rahmenauftrag, gegen den abgerufen wurde"},"position_id":{"type":["string","null"],"description":"Position im Rahmen; null = Abruf gegen den Rahmen als Ganzes"},"menge":{"type":"number","description":"Abgerufene Menge"},"preis_lock":{"type":"number","description":"Festgeschriebener Einzelpreis inklusive Aufschlag"},"abgerufen_am":{"type":"string","description":"Zeitpunkt des Abrufs (ISO)"},"kunden_auftrag_id":{"type":["string","null"],"description":"Verknuepfter Kundenauftrag"},"escalator_basis":{"type":["number","null"],"description":"Preis VOR dem Aufschlag"},"created_by":{"type":["string","null"],"description":"Wer gebucht hat; faellt auf die ROLLE zurueck, wenn keine Benutzer-Id im Kontext steht"},"notes":{"type":["string","null"],"description":"Notiz zum Abruf"}},"required":["id","rahmenauftrag_id","position_id","menge","preis_lock","abgerufen_am","kunden_auftrag_id","escalator_basis","created_by","notes"],"additionalProperties":false},"description":"Alle Abrufe des Rahmens, neueste zuerst"},"aggregate":{"type":"object","properties":{"total_abgerufen":{"type":"number","description":"Summe der abgerufenen Mengen"},"total_wert":{"type":"number","description":"Summe aus Menge mal festgeschriebenem Preis — der abgerufene Wert"},"restmenge":{"type":["number","null"],"description":"Gesamtmenge minus Abrufe; null, wenn der Rahmen keine Gesamtmenge fuehrt"},"gesamt_menge":{"type":["number","null"],"description":"Vereinbarte Gesamtmenge des Rahmens"}},"required":["total_abgerufen","total_wert","restmenge","gesamt_menge"],"additionalProperties":false,"description":"Aggregate ueber ALLE Positionen, nicht je Position"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, in dem gelesen wurde"},"source":{"type":"string","const":"db","description":"Datenquelle; hier immer die Datenbank"}},"required":["tenantId","source"],"additionalProperties":false,"description":"Angaben zur Abfrage"}},"required":["data","aggregate","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","rahmenauftrag_id":"00000000-0000-4000-8000-000000000000","position_id":"string","menge":0,"preis_lock":0,"abgerufen_am":"string","kunden_auftrag_id":"string","escalator_basis":0,"created_by":"string","notes":"string"}],"aggregate":{"total_abgerufen":0,"total_wert":0,"restmenge":0,"gesamt_menge":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenauftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rahmenauftrag_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1RahmenauftraegeByIdAbrufe","tags":["RahmenAbrufe"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Listet alle Abrufe eines Rahmenauftrags inkl. Σ-Menge, Σ-Wert und Restmenge. Es wird nicht geblättert und nicht gefiltert — die Antwort enthält immer alle Abrufe. Die Aggregate gelten für den GANZEN Rahmen, auch wenn die Abrufe verschiedenen Positionen zugeordnet sind.","summary":"List call-offs"}},"/api/v1/rahmenauftraege/{id}/forecast":{"get":{"responses":{"200":{"description":"Forecast-Daten — eine lineare Fortschreibung, keine Zusage","content":{"application/json":{"schema":{"type":"object","properties":{"rahmenauftrag_id":{"type":"string","format":"uuid","description":"Der ausgewertete Rahmenauftrag"},"rahmen_number":{"type":"string","description":"Nummer des Rahmenauftrags"},"gesamt_menge":{"type":["number","null"],"description":"Vereinbarte Gesamtmenge; null, wenn keine gefuehrt wird"},"total_abgerufen":{"type":"number","description":"Summe aller Abrufe, ueber die gesamte Laufzeit"},"rest_menge":{"type":["number","null"],"description":"Gesamtmenge minus Abrufe; null ohne Gesamtmenge"},"avg_per_day":{"type":"number","minimum":0,"description":"Durchschnittliche Abrufmenge je Tag aus den letzten 90 Tagen; 0 ohne Abrufe darin"},"avg_per_month":{"type":"number","minimum":0,"description":"Der Tagesschnitt mal 30 — keine eigene Messung"},"days_to_zero":{"type":["integer","null"],"minimum":0,"description":"Tage bis zur Vollabnahme bei gleichbleibender Rate; null ohne Rate oder ohne Gesamtmenge"},"projected_full_date":{"type":["string","null"],"description":"Der hochgerechnete Tag der Vollabnahme (ISO-Datum); null wie bei days_to_zero"},"confidence_pct":{"type":"integer","minimum":0,"maximum":95,"description":"Nur ein Mass fuer die DATENDICHTE: Abrufe der letzten 90 Tage mal 8, gedeckelt bei 95. Keine statistische Guete"},"data_points":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","description":"Tag des Abrufs (ISO-Datum, ohne Uhrzeit)"},"kumulative_menge_abgerufen":{"type":"number","description":"Summe aller Abrufe bis einschliesslich diesem Tag"},"rest_menge":{"type":["number","null"],"description":"Verbleibende Menge an diesem Tag; null ohne vereinbarte Gesamtmenge"}},"required":["date","kumulative_menge_abgerufen","rest_menge"],"additionalProperties":false},"description":"Die Burndown-Kurve, aelteste zuerst"}},"required":["rahmenauftrag_id","rahmen_number","gesamt_menge","total_abgerufen","rest_menge","avg_per_day","avg_per_month","days_to_zero","projected_full_date","confidence_pct","data_points"],"additionalProperties":false},"example":{"rahmenauftrag_id":"00000000-0000-4000-8000-000000000000","rahmen_number":"string","gesamt_menge":0,"total_abgerufen":0,"rest_menge":0,"avg_per_day":0,"avg_per_month":0,"days_to_zero":0,"projected_full_date":"string","confidence_pct":0,"data_points":[{"date":"string","kumulative_menge_abgerufen":0,"rest_menge":0}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenauftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rahmenauftrag_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1RahmenauftraegeByIdForecast","tags":["RahmenAbrufe"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Burndown-Forecast: aktuelle Restmenge, durchschnittliche Abruf-Rate (90 Tage), Tage bis Vollabnahme (lineare Extrapolation) und Datenpunkte über die Zeit. Die Hochrechnung ist rein linear und schreibt die letzten 90 Tage fort — keine Saison, kein Modell. confidence_pct misst nur, wie viele Abrufe in diesen 90 Tagen liegen (Anzahl mal 8, gedeckelt bei 95), nicht die Güte der Prognose. Ohne vereinbarte Gesamtmenge bleiben Restmenge, Tage bis Vollabnahme und Zieldatum null.","summary":"Burndown forecast"}},"/api/v1/rahmenauftraege/{id}/escalator-rules":{"post":{"responses":{"201":{"description":"Regel angelegt — die neue Regel, inklusive Id und Anlagezeitpunkt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Regel"},"rahmenauftrag_id":{"type":"string","format":"uuid","description":"Rahmenauftrag, fuer den die Regel gilt"},"position_id":{"type":["string","null"],"description":"Position, fuer die die Regel gilt; null = fuer alle Positionen"},"effective_from":{"type":"string","description":"Ab wann die Regel greift (ISO-Datum)"},"delta_pct":{"type":"number","minimum":-100,"maximum":1000,"description":"Preisaenderung in Prozent; negativ ist ein Abschlag"},"notes":{"type":["string","null"],"description":"Notiz zur Regel"},"created_at":{"type":"string","description":"Anlagezeitpunkt (ISO)"}},"required":["id","rahmenauftrag_id","position_id","effective_from","delta_pct","notes","created_at"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","rahmenauftrag_id":"00000000-0000-4000-8000-000000000000","position_id":"string","effective_from":"string","delta_pct":0,"notes":"string","created_at":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rahmenauftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rahmenauftrag_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1RahmenauftraegeByIdEscalator-rules","tags":["RahmenAbrufe"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Legt eine Escalator-Regel (Δ% ab effective_from) für einen Rahmenauftrag (optional pro Position) an. Die Regel wirkt nur auf KÜNFTIGE Abrufe: bereits gebuchte behalten ihren festgeschriebenen Preis, auch wenn das Startdatum in der Vergangenheit liegt. Mehrere Regeln sind erlaubt; beim Abruf gewinnt die jüngste gültige, und eine positionsgenaue Regel schlägt eine für den ganzen Rahmen.","summary":"Create escalator rule","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"position_id":{"type":"string"},"effective_from":{"type":"string","format":"date"},"delta_pct":{"type":"number","minimum":-100,"maximum":1000},"notes":{"type":"string","maxLength":2000}},"required":["effective_from","delta_pct"]},"example":{"position_id":"string","effective_from":"2026-01-01","delta_pct":0,"notes":"string"}}}}},"get":{"responses":{"200":{"description":"Liste der Regeln — leer auch dann, wenn es den Rahmenauftrag gar nicht gibt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Regel"},"rahmenauftrag_id":{"type":"string","format":"uuid","description":"Rahmenauftrag, fuer den die Regel gilt"},"position_id":{"type":["string","null"],"description":"Position, fuer die die Regel gilt; null = fuer alle Positionen"},"effective_from":{"type":"string","description":"Ab wann die Regel greift (ISO-Datum)"},"delta_pct":{"type":"number","minimum":-100,"maximum":1000,"description":"Preisaenderung in Prozent; negativ ist ein Abschlag"},"notes":{"type":["string","null"],"description":"Notiz zur Regel"},"created_at":{"type":"string","description":"Anlagezeitpunkt (ISO)"}},"required":["id","rahmenauftrag_id","position_id","effective_from","delta_pct","notes","created_at"],"additionalProperties":false},"description":"Alle Regeln des Rahmens, juengste zuerst"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, in dem gelesen wurde"},"source":{"type":"string","const":"db","description":"Datenquelle; hier immer die Datenbank"}},"required":["tenantId","source"],"additionalProperties":false,"description":"Angaben zur Abfrage"}},"required":["data","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","rahmenauftrag_id":"00000000-0000-4000-8000-000000000000","position_id":"string","effective_from":"string","delta_pct":0,"notes":"string","created_at":"string"}],"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1RahmenauftraegeByIdEscalator-rules","tags":["RahmenAbrufe"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Listet alle Escalator-Regeln eines Rahmenauftrags, jüngstes Startdatum zuerst. Anders als die übrigen Aufrufe prüft dieser NICHT, ob es den Rahmenauftrag gibt: eine unbekannte Id ergibt 200 mit leerer Liste, kein 404.","summary":"List escalator rules"}},"/api/v1/rahmenauftraege/{id}/escalator-rules/{ruleId}":{"delete":{"responses":{"204":{"description":"Regel gelöscht — ohne Rumpf"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Regel nicht gefunden, oder sie gehört zu einem anderen Rahmenauftrag","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"rule_not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1RahmenauftraegeByIdEscalator-rulesByRuleId","tags":["RahmenAbrufe"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"ruleId","required":true}],"description":"Löscht eine Escalator-Regel endgültig. Bereits gebuchte Abrufe behalten ihren festgeschriebenen Preis — das Löschen wirkt nur auf künftige Abrufe. Die Regel muss zu dem Rahmenauftrag aus dem Pfad gehören, sonst 404.","summary":"Delete escalator rule"}},"/api/v1/credit-notes":{"get":{"responses":{"200":{"description":"Liste der Gutschriften","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"creditNoteNumber":{},"typ":{},"partnerId":{},"referenzInvoiceId":{},"referenzOrderId":{},"stornoTyp":{},"ausgestelltAm":{},"grund":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"waehrung":{},"status":{},"gebuchtAm":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","subtotal":0,"tax":0,"total":0}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Credit-notes","tags":["CreditNotes"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"typ","schema":{"type":"string","enum":["sale","purchase"]}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","issued","paid","cancelled"]}},{"in":"query","name":"partner_id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"dateFrom","schema":{"type":"string","format":"date"}},{"in":"query","name":"dateTo","schema":{"type":"string","format":"date"}}],"summary":"Listet Gutschriften des Mandanten","description":"Blaettert ueber `limit` (1…200, Vorgabe 50) und `offset`, zuletzt angelegte zuerst. Filtern laesst sich nach `typ` (sale = eigene V-Gutschrift, purchase = erhaltene E-Gutschrift), `status`, `partner_id` sowie ueber `dateFrom`/`dateTo` auf das Ausstellungsdatum; mehrere Filter wirken zusammen (UND). Soft-geloeschte Gutschriften erscheinen nie. `pagination.total` zaehlt die Treffer NACH den Filtern, aber ohne Blaetterung. Storno-Vollgutschriften tragen negative Betraege — die Liste gibt sie unveraendert weiter, anders als die Kennzahlen unter /stats."},"post":{"responses":{"201":{"description":"Gutschrift angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"creditNoteNumber":{},"typ":{},"partnerId":{},"referenzInvoiceId":{},"referenzOrderId":{},"stornoTyp":{},"ausgestelltAm":{},"grund":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"waehrung":{},"status":{},"gebuchtAm":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"tax":0,"total":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"423":{"description":"Buchungsperiode geschlossen — die Gutschrift ist dennoch angelegt"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1Credit-notes","tags":["CreditNotes"],"parameters":[],"summary":"Legt eine neue Gutschrift an","description":"Bei storno_typ=voll und referenz_invoice_id werden items aus der Originalrechnung mit negativen Beträgen kopiert; die Summen stammen dann aus der Rechnung, nicht aus dem Rumpf. Sind eigene Positionen dabei, rechnet der Server subtotal/tax/total aus den Zeilen NEU — mitgesendete Kopfsummen werden verworfen. Nur ohne Positionen zaehlen die Summen aus dem Rumpf. Die Gutschriftnummer vergibt der Server luecklos je Jahr und Art (VG-JJJJ-NNNN fuer sale, EG-JJJJ-NNNN fuer purchase); eine soft-geloeschte Nummer wird nie erneut vergeben. Bei status=issued UND typ=sale entsteht zusaetzlich die Journalbuchung 4400 an 1400 ueber den Betrag ohne Vorzeichen — sie laeuft NICHT in derselben Transaktion wie das Anlegen: ist die Periode geschlossen (423), steht die Gutschrift bereits in der Datenbank.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"typ":{"type":"string","enum":["sale","purchase"]},"partner_id":{"type":"string","format":"uuid"},"referenz_invoice_id":{"type":"string","format":"uuid"},"referenz_order_id":{"type":"string","format":"uuid"},"storno_typ":{"type":"string","enum":["voll","teil","standalone"]},"ausgestellt_am":{"type":"string","format":"date"},"grund":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","maxLength":500},"description":{"type":"string","minLength":1,"maxLength":500},"quantity":{"type":"number","minimum":-1000000,"maximum":1000000},"unit_price":{"type":"number","minimum":-1000000,"maximum":1000000},"total":{"type":"number","minimum":-1000000,"maximum":1000000},"vat_rate":{"type":"number","minimum":0,"maximum":100,"default":19}},"required":["description","quantity","unit_price","total"]},"maxItems":1000,"default":[]},"subtotal":{"type":"number","minimum":-1000000,"maximum":1000000,"default":0},"tax":{"type":"number","minimum":-1000000,"maximum":1000000,"default":0},"total":{"type":"number","minimum":-1000000,"maximum":1000000,"default":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"status":{"type":"string","enum":["draft","issued","paid","cancelled"],"default":"draft"},"notizen":{"type":"string"}},"required":["typ","partner_id","ausgestellt_am"]},"example":{"typ":"sale","partner_id":"00000000-0000-4000-8000-000000000000","referenz_invoice_id":"00000000-0000-4000-8000-000000000000","referenz_order_id":"00000000-0000-4000-8000-000000000000","storno_typ":"voll","ausgestellt_am":"2026-01-01","grund":"string","items":[{"title":"string","description":"string","quantity":0,"unit_price":0,"total":0,"vat_rate":0}],"subtotal":0,"tax":0,"total":0,"waehrung":"str","status":"draft","notizen":"string"}}}}}},"/api/v1/credit-notes/stats":{"get":{"responses":{"200":{"description":"Gutschriften-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"saleOpenCount":{"type":"integer"},"saleOpenSumme":{"type":"number"},"purchaseOpenCount":{"type":"integer"},"purchaseOpenSumme":{"type":"number"},"purchasePaidCount":{"type":"integer"},"purchasePaidSumme":{"type":"number"},"stornoVolumenYtd":{"type":"number"},"totalCount":{"type":"integer"},"meta":{"type":"object","properties":{"tenantId":{},"year":{"type":"integer"},"source":{"type":"string"}},"required":["year","source"]}},"required":["saleOpenCount","saleOpenSumme","purchaseOpenCount","purchaseOpenSumme","purchasePaidCount","purchasePaidSumme","stornoVolumenYtd","totalCount","meta"]},"example":{"saleOpenCount":0,"saleOpenSumme":0,"purchaseOpenCount":0,"purchaseOpenSumme":0,"purchasePaidCount":0,"purchasePaidSumme":0,"stornoVolumenYtd":0,"totalCount":0,"meta":{"year":0,"source":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Credit-notesStats","tags":["CreditNotes"],"parameters":[],"summary":"Kennzahlen zu Gutschriften: offen, erhalten, Storno-Volumen","description":"Vier Auswertungen ueber die nicht geloeschten Gutschriften: sale/issued (offen gegenueber dem Kunden), purchase/issued (offen vom Lieferanten), purchase/paid (erhalten) sowie das Storno-Volumen des laufenden Kalenderjahres (storno_typ gesetzt, Status nicht cancelled, Ausstellungsjahr = aktuelles Jahr). ALLE Summen sind Betraege OHNE Vorzeichen — Storno-Vollgutschriften liegen negativ in der Tabelle und wuerden die Kennzahl sonst ins Minus ziehen. `totalCount` zaehlt alle nicht geloeschten Gutschriften jeden Status. `meta.year` nennt das Jahr, auf das sich das Storno-Volumen bezieht."}},"/api/v1/credit-notes/{id}":{"get":{"responses":{"200":{"description":"Gutschrift-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"creditNoteNumber":{},"typ":{},"partnerId":{},"referenzInvoiceId":{},"referenzOrderId":{},"stornoTyp":{},"ausgestelltAm":{},"grund":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"waehrung":{},"status":{},"gebuchtAm":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"tax":0,"total":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Gutschrift nicht gefunden"}},"operationId":"getApiV1Credit-notesById","tags":["CreditNotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Liefert eine einzelne Gutschrift","description":"Liest EINE Gutschrift anhand ihrer UUID — nicht anhand der Gutschriftnummer. Soft-geloeschte gelten als nicht vorhanden und ergeben 404. Die Antwort enthaelt die Positionen aus dem JSONB-Feld `items` samt der gespeicherten Vorzeichen; die drei Betraege kommen als Zahlen, nicht als Dezimal-Strings."},"put":{"responses":{"200":{"description":"Gutschrift aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"creditNoteNumber":{},"typ":{},"partnerId":{},"referenzInvoiceId":{},"referenzOrderId":{},"stornoTyp":{},"ausgestelltAm":{},"grund":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"waehrung":{},"status":{},"gebuchtAm":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"tax":0,"total":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Gutschrift nicht gefunden"},"409":{"description":"Nur Entwürfe können bearbeitet werden"}},"operationId":"putApiV1Credit-notesById","tags":["CreditNotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aktualisiert eine Gutschrift (nur status=draft)","description":"Nur ab Rolle „manager\" und nur im Entwurf: eine bereits ausgestellte, bezahlte oder stornierte Gutschrift wird mit 409 abgewiesen. Teil-Update — nicht gesendete Felder behalten ihren bisherigen Wert. Der Status bleibt zwingend „draft\", auch wenn der Rumpf einen anderen nennt; dafuer gibt es PATCH /:id/status. Gutschriftnummer und Ausstellungsjahr aendern sich nie. Anders als beim Anlegen werden die Summen hier NICHT aus den Positionen nachgerechnet — gesendete Kopfsummen werden uebernommen wie sie sind, und es entsteht keine Journalbuchung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"typ":{"type":"string","enum":["sale","purchase"]},"partner_id":{"type":"string","format":"uuid"},"referenz_invoice_id":{"type":"string","format":"uuid"},"referenz_order_id":{"type":"string","format":"uuid"},"storno_typ":{"type":"string","enum":["voll","teil","standalone"]},"ausgestellt_am":{"type":"string","format":"date"},"grund":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","maxLength":500},"description":{"type":"string","minLength":1,"maxLength":500},"quantity":{"type":"number","minimum":-1000000,"maximum":1000000},"unit_price":{"type":"number","minimum":-1000000,"maximum":1000000},"total":{"type":"number","minimum":-1000000,"maximum":1000000},"vat_rate":{"type":"number","minimum":0,"maximum":100,"default":19}},"required":["description","quantity","unit_price","total"]},"maxItems":1000,"default":[]},"subtotal":{"type":"number","minimum":-1000000,"maximum":1000000,"default":0},"tax":{"type":"number","minimum":-1000000,"maximum":1000000,"default":0},"total":{"type":"number","minimum":-1000000,"maximum":1000000,"default":0},"waehrung":{"type":"string","minLength":3,"maxLength":3,"default":"EUR"},"status":{"type":"string","enum":["draft","issued","paid","cancelled"],"default":"draft"},"notizen":{"type":"string"}}},"example":{"typ":"sale","partner_id":"00000000-0000-4000-8000-000000000000","referenz_invoice_id":"00000000-0000-4000-8000-000000000000","referenz_order_id":"00000000-0000-4000-8000-000000000000","storno_typ":"voll","ausgestellt_am":"2026-01-01","grund":"string","items":[{"title":"string","description":"string","quantity":0,"unit_price":0,"total":0,"vat_rate":0}],"subtotal":0,"tax":0,"total":0,"waehrung":"str","status":"draft","notizen":"string"}}}}},"delete":{"responses":{"200":{"description":"Gutschrift gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Gutschrift nicht gefunden"},"409":{"description":"Nur Entwürfe können gelöscht werden"}},"operationId":"deleteApiV1Credit-notesById","tags":["CreditNotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Soft-löscht eine Gutschrift (nur status=draft)","description":"Nur ab Rolle „manager\" und nur im Entwurf: eine ausgestellte, bezahlte oder stornierte Gutschrift wird mit 409 abgewiesen. Setzt `deleted_at` — der Datensatz bleibt in der Tabelle, verschwindet aber aus Liste, Detail und Kennzahlen. Die vergebene Gutschriftnummer bleibt verbraucht und wird nie erneut ausgegeben, damit die Nummernfolge luecklos bleibt. Die Antwort enthaelt nur eine Meldung, keinen Datensatz."}},"/api/v1/credit-notes/{id}/status":{"patch":{"responses":{"200":{"description":"Status aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"creditNoteNumber":{},"typ":{},"partnerId":{},"referenzInvoiceId":{},"referenzOrderId":{},"stornoTyp":{},"ausgestelltAm":{},"grund":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"waehrung":{},"status":{},"gebuchtAm":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"tax":0,"total":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Gutschrift nicht gefunden"}},"operationId":"patchApiV1Credit-notesByIdStatus","tags":["CreditNotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Setzt den Status einer Gutschrift (issued/paid/cancelled)","description":"Nur ab Rolle „manager\". Der Status wird ohne Reihenfolgepruefung gesetzt — jeder der drei Werte ist aus jedem Zustand heraus erlaubt, auch zurueck. Bei „issued\" setzt der Endpunkt zusaetzlich `gebuchtAm` auf die aktuelle Zeit; die anderen Werte lassen es unveraendert. Es entsteht KEINE Journalbuchung: eine nachtraeglich auf „issued\" gesetzte Gutschrift erscheint im Journal nicht. Soft-geloeschte ergeben 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["issued","paid","cancelled"]}},"required":["status"]},"example":{"status":"issued"}}}}}},"/api/v1/credit-notes/from-invoice":{"post":{"responses":{"201":{"description":"Storno-Gutschrift angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"creditNoteNumber":{},"typ":{},"partnerId":{},"referenzInvoiceId":{},"referenzOrderId":{},"stornoTyp":{},"ausgestelltAm":{},"grund":{},"items":{},"subtotal":{"type":"number"},"tax":{"type":"number"},"total":{"type":"number"},"waehrung":{},"status":{},"gebuchtAm":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["id","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"string","subtotal":0,"tax":0,"total":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Rechnung nicht gefunden"}},"operationId":"postApiV1Credit-notesFrom-invoice","tags":["CreditNotes"],"parameters":[],"summary":"Convenience: Storno-Gutschrift direkt aus einer Rechnung erzeugen","description":"Die Rechnung steht als `invoiceId` im RUMPF, nicht im Pfad. Bei storno_typ=voll (Vorgabe) uebernimmt der Endpunkt Positionen und Summen der Rechnung mit umgekehrtem Vorzeichen; bei „teil\"/„standalone\" zaehlen nur die mitgesendeten Positionen, aus denen der Server die Summen NEU rechnet. Kunde, Waehrung und Rechnungsbezug stammen aus der Rechnung, das Ausstellungsdatum ist der heutige Tag, `grund` faellt auf „Storno\" zurueck. Die neue Gutschrift ist immer typ=sale und status=draft — es entsteht KEINE Journalbuchung, und die Rechnung selbst bleibt unveraendert. 404, wenn die Rechnung fehlt oder soft-geloescht ist.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string","minLength":1,"description":"Die Rechnung, zu der die Storno-Gutschrift entsteht"},"storno_typ":{"type":"string","enum":["voll","teil","standalone"],"default":"voll"},"items":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","maxLength":500},"description":{"type":"string","minLength":1,"maxLength":500},"quantity":{"type":"number","minimum":-1000000,"maximum":1000000},"unit_price":{"type":"number","minimum":-1000000,"maximum":1000000},"total":{"type":"number","minimum":-1000000,"maximum":1000000},"vat_rate":{"type":"number","minimum":0,"maximum":100,"default":19}},"required":["description","quantity","unit_price","total"]},"maxItems":1000},"grund":{"type":"string"}},"required":["invoiceId"]},"example":{"invoiceId":"string","storno_typ":"voll","items":[{"title":"string","description":"string","quantity":0,"unit_price":0,"total":0,"vat_rate":0}],"grund":"string"}}}}}},"/api/v1/credit-notes/{id}/send":{"post":{"responses":{"200":{"description":"Gutschrift versendet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"messageId":{"type":"string"},"sentAt":{"type":"string","description":"Zeitpunkt des Versands (ISO)"},"recipients":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}}},"required":["to","cc","bcc"]}},"required":["ok","messageId","sentAt","recipients"]},"example":{"ok":true,"messageId":"string","sentAt":"string","recipients":{"to":["string"],"cc":["string"],"bcc":["string"]}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"402":{"description":"E-Mail-Kontingent erschoepft"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Gutschrift nicht gefunden"},"413":{"description":"Zusatz-Anhaenge ueberschreiten zusammen 10 MB — es wurde nichts verschickt"},"500":{"description":"Versand unerwartet fehlgeschlagen"},"502":{"description":"E-Mail-Versand fehlgeschlagen"}},"operationId":"postApiV1Credit-notesByIdSend","tags":["CreditNotes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Versendet eine Gutschrift per E-Mail an den/die Empfaenger","description":"Nur ab Rolle „manager\". Die Mail traegt die Gutschriftdaten im HTML-Text; ein Gutschrift-PDF wird NICHT erzeugt und NICHT angehaengt — Anhaenge entstehen nur aus `extraAttachments` im Rumpf (zusammen hoechstens 10 MB, sonst 413). Vor dem Versand wird das E-Mail-Kontingent des Mandanten geprueft und hochgezaehlt (402 bei Erschoepfung; bei fehlgeschlagenem Versand und bei zu groszen Anhaengen wieder gutgeschrieben); die verbleibende Menge steht in den Antwort-Kopfzeilen. Anrede, Branding und Betreff-Vorlage kommen aus den Mandanteneinstellungen, die Signatur aus dem Profil des absendenden Nutzers. Nebenwirkung: eine Gutschrift im Entwurf wechselt nach erfolgreichem Versand auf „issued\" — eine Journalbuchung entsteht dabei NICHT. Zusaetzlich wird ein Eintrag „document_sent\" im Aktivitaetsverlauf geschrieben. 502, wenn der Mailversender ablehnt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"cc":{"type":"array","items":{"type":"string","format":"email"}},"bcc":{"type":"array","items":{"type":"string","format":"email"}},"subject":{"type":"string","minLength":1,"maxLength":255},"message":{"type":"string","maxLength":4000},"attachPdf":{"type":"boolean"},"includePortalLink":{"type":"boolean","default":true},"lang":{"type":"string","enum":["de","en","fr","es","nl","da","pl","cs","zh"]},"template":{"type":"string","enum":["doc-quote","doc-order","doc-delivery","doc-invoice","dunning-level1","dunning-level2","dunning-level3"]},"extraAttachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"contentBase64":{"type":"string","minLength":1},"contentType":{"type":"string","maxLength":100}},"required":["filename","contentBase64"]},"maxItems":10},"attachmentMode":{"type":"string","enum":["separate","merge"]}},"required":["to"]},"example":{"to":["beispiel@example.com"],"cc":["beispiel@example.com"],"bcc":["beispiel@example.com"],"subject":"string","message":"string","attachPdf":true,"includePortalLink":true,"lang":"de","template":"doc-quote","extraAttachments":[{"filename":"string","contentBase64":"string","contentType":"string"}],"attachmentMode":"separate"}}}}}},"/api/v1/deliveries/stats":{"get":{"responses":{"200":{"description":"Kennzahlen ueber alle nicht geloeschten Lieferscheine. Nur `heuteGeliefert` und `dieseWocheGeliefert` sind zeitlich begrenzt — die uebrigen vier zaehlen den Gesamtbestand.","content":{"application/json":{"schema":{"type":"object","properties":{"entwurf":{"type":"number"},"imVersand":{"type":"number"},"heuteGeliefert":{"type":"number"},"dieseWocheGeliefert":{"type":"number"},"deliveredTotal":{"type":"number"},"total":{"type":"number"},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string","const":"db"}},"required":["source"],"additionalProperties":false}},"required":["entwurf","imVersand","heuteGeliefert","dieseWocheGeliefert","deliveredTotal","total","meta"],"additionalProperties":false},"example":{"entwurf":0,"imVersand":0,"heuteGeliefert":0,"dieseWocheGeliefert":0,"deliveredTotal":0,"total":0,"meta":{"source":"db"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"getApiV1DeliveriesStats","tags":["Deliveries"],"parameters":[],"description":"Sechs Zaehler ueber die Lieferscheine des Mandanten, aus EINER Aggregat-Abfrage: `entwurf` (Status draft oder picked), `imVersand` (shipped), `heuteGeliefert` und `dieseWocheGeliefert` (delivered, nach `geliefert_am`; die Woche beginnt Montag), `deliveredTotal` und `total`. Geloeschte Lieferscheine zaehlen nirgends mit. Es gibt keine Filter — die Zahlen gelten immer fuer den ganzen Bestand.","summary":"Sechs Zaehler ueber die Lieferscheine des Mandanten, aus EINER Aggregat-Abfrage","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/deliveries":{"get":{"responses":{"200":{"description":"Liste der Lieferscheine","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"deliveryNumber":{"type":["string","null"]},"customerId":{"type":["string","null"]},"customerName":{"type":["string","null"]},"orderId":{"type":["string","null"]},"orderNumber":{"type":["string","null"]},"ausgestelltAm":{"type":"null"},"geliefertAm":{"type":"null"},"empfaenger":{},"spediteur":{"type":["string","null"]},"trackingNumber":{"type":["string","null"]},"items":{"type":["array","null"],"items":{}},"status":{"type":["string","null"]},"title":{"type":["string","null"]},"introText":{"type":["string","null"]},"notizen":{"type":["string","null"]},"customFields":{"type":["object","null"],"additionalProperties":{}},"sourceDocumentType":{"type":["string","null"]},"sourceDocumentId":{"type":["string","null"]},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"versandAm":{"type":"null"},"gesamtGewicht":{"type":"null"},"gesamtVolumen":{"type":"null"},"packstuecke":{"type":"null"},"pdfUrl":{"type":"null"}},"required":["id"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"pages":{"type":"integer"}},"required":["page","limit","total","pages"]},"meta":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","deliveryNumber":"string","customerId":"string","customerName":"string","orderId":"string","orderNumber":"string","ausgestelltAm":null,"geliefertAm":null,"spediteur":"string","trackingNumber":"string","items":[],"status":"string","title":"string","introText":"string","notizen":"string","customFields":{},"sourceDocumentType":"string","sourceDocumentId":"string","createdAt":null,"updatedAt":null,"versandAm":null,"gesamtGewicht":null,"gesamtVolumen":null,"packstuecke":null,"pdfUrl":null}],"pagination":{"page":0,"limit":0,"total":0,"pages":0},"meta":{}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Deliveries","tags":["Deliveries"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","picked","shipped","delivered","cancelled"]}},{"in":"query","name":"customerId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"orderId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"dateFrom","schema":{"type":"string","format":"date"}},{"in":"query","name":"dateTo","schema":{"type":"string","format":"date"}}],"description":"Listet die nicht geloeschten Lieferscheine, neueste zuerst (nach Anlagedatum). Eingrenzen ueber `status`, `customerId`, `orderId` sowie `dateFrom`/`dateTo` auf das Ausstellungsdatum. Geblaettert wird ueber `limit` (1..200, Vorgabe 50) und `offset`; `pagination.total` zaehlt die Treffer OHNE Limit, also die vollstaendige Filtermenge. Jede Zeile traegt zusaetzlich den Kundennamen und die Auftragsnummer aus den verknuepften Datensaetzen.","summary":"Listet die nicht geloeschten Lieferscheine, neueste zuerst (nach Anlagedatum)","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Lieferschein angelegt. Die Nummer kommt atomar aus `number_ranges` (SELECT … FOR UPDATE) und wird nur noch durch das Mandanten-Muster formatiert — es gibt keine zweite Zaehlerquelle.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deliveryNumber":{"type":["string","null"]},"customerId":{"type":["string","null"]},"customerName":{"type":["string","null"]},"orderId":{"type":["string","null"]},"orderNumber":{"type":["string","null"]},"ausgestelltAm":{"type":"null"},"geliefertAm":{"type":"null"},"empfaenger":{},"spediteur":{"type":["string","null"]},"trackingNumber":{"type":["string","null"]},"items":{"type":["array","null"],"items":{}},"status":{"type":["string","null"]},"title":{"type":["string","null"]},"introText":{"type":["string","null"]},"notizen":{"type":["string","null"]},"customFields":{"type":["object","null"],"additionalProperties":{}},"sourceDocumentType":{"type":["string","null"]},"sourceDocumentId":{"type":["string","null"]},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"versandAm":{"type":"null"},"gesamtGewicht":{"type":"null"},"gesamtVolumen":{"type":"null"},"packstuecke":{"type":"null"},"pdfUrl":{"type":"null"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","deliveryNumber":"string","customerId":"string","customerName":"string","orderId":"string","orderNumber":"string","ausgestelltAm":null,"geliefertAm":null,"spediteur":"string","trackingNumber":"string","items":[],"status":"string","title":"string","introText":"string","notizen":"string","customFields":{},"sourceDocumentType":"string","sourceDocumentId":"string","createdAt":null,"updatedAt":null,"versandAm":null,"gesamtGewicht":null,"gesamtVolumen":null,"packstuecke":null,"pdfUrl":null}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Unzureichende Rolle"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"postApiV1Deliveries","tags":["Deliveries"],"parameters":[],"description":"Legt einen Lieferschein an. Die Lieferscheinnummer vergibt der Server selbst — sie kommt gesperrt aus dem Nummernkreis des Mandanten und wird anschliessend nach dessen Muster formatiert; eine mitgeschickte Nummer gibt es nicht. Pflicht sind nur `customerId` und `ausgestelltAm`; ohne `items` entsteht ein Beleg ohne Positionen, ohne `status` einer im Entwurf. Nach dem Anlegen geht das Ereignis `delivery.created` an die Webhooks des Mandanten — nebenlaeufig, sein Fehlschlag beruehrt die Antwort nicht. Ab Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"orderId":{"type":"string","format":"uuid"},"ausgestelltAm":{"type":"string","format":"date"},"versandAm":{"type":"string","format":"date"},"geliefertAm":{"type":"string","format":"date"},"empfaenger":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"}}},"spediteur":{"type":"string","maxLength":100},"trackingNumber":{"type":"string","maxLength":100},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":"string","minLength":1},"quantity":{"type":"number","exclusiveMinimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0,"default":0},"total":{"type":"number","minimum":0,"default":0},"taxRate":{"type":"number","minimum":0},"sourceOrderItemId":{"type":"string","format":"uuid"}},"required":["description","quantity"]},"default":[]},"gesamtGewicht":{"type":"number","minimum":0},"gesamtVolumen":{"type":"number","minimum":0},"packstuecke":{"type":"integer","minimum":0},"status":{"type":"string","enum":["draft","picked","shipped","delivered","cancelled"],"default":"draft"},"title":{"type":"string","maxLength":200},"introText":{"type":"string"},"notizen":{"type":"string"},"pdfUrl":{"type":"string","format":"uri"},"sourceDocumentType":{"type":"string","maxLength":20},"sourceDocumentId":{"type":"string","format":"uuid"},"customFields":{"type":"object","additionalProperties":{}}},"required":["customerId","ausgestelltAm"]},"example":{"customerId":"00000000-0000-4000-8000-000000000000","orderId":"00000000-0000-4000-8000-000000000000","ausgestelltAm":"2026-01-01","versandAm":"2026-01-01","geliefertAm":"2026-01-01","empfaenger":{"name":"string","street":"string","city":"string","zip":"string","country":"string"},"spediteur":"string","trackingNumber":"string","items":[{"articleId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","quantity":1,"unit":"string","unitPrice":0,"total":0,"taxRate":0,"sourceOrderItemId":"00000000-0000-4000-8000-000000000000"}],"gesamtGewicht":0,"gesamtVolumen":0,"packstuecke":0,"status":"draft","title":"string","introText":"string","notizen":"string","pdfUrl":"https://example.com","sourceDocumentType":"string","sourceDocumentId":"00000000-0000-4000-8000-000000000000","customFields":{}}}}},"summary":"Legt einen Lieferschein an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/deliveries/{id}":{"get":{"responses":{"200":{"description":"Lieferschein-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deliveryNumber":{"type":["string","null"]},"customerId":{"type":["string","null"]},"customerName":{"type":["string","null"]},"orderId":{"type":["string","null"]},"orderNumber":{"type":["string","null"]},"ausgestelltAm":{"type":"null"},"geliefertAm":{"type":"null"},"empfaenger":{},"spediteur":{"type":["string","null"]},"trackingNumber":{"type":["string","null"]},"items":{"type":["array","null"],"items":{}},"status":{"type":["string","null"]},"title":{"type":["string","null"]},"introText":{"type":["string","null"]},"notizen":{"type":["string","null"]},"customFields":{"type":["object","null"],"additionalProperties":{}},"sourceDocumentType":{"type":["string","null"]},"sourceDocumentId":{"type":["string","null"]},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"versandAm":{"type":"null"},"gesamtGewicht":{"type":"null"},"gesamtVolumen":{"type":"null"},"packstuecke":{"type":"null"},"pdfUrl":{"type":"null"},"belegkette":{"type":"object","properties":{"sourceOrder":{"type":["object","null"],"properties":{"orderNumber":{"type":"string"}},"required":["orderNumber"]},"linkedInvoice":{"type":["object","null"],"properties":{"id":{"type":"string"},"invoiceNumber":{"type":"string"}},"required":["id","invoiceNumber"]}},"required":["sourceOrder","linkedInvoice"]}},"required":["id","customerName","orderNumber","belegkette"],"additionalProperties":false},"example":{"id":"string","deliveryNumber":"string","customerId":"string","customerName":"string","orderId":"string","orderNumber":"string","ausgestelltAm":null,"geliefertAm":null,"spediteur":"string","trackingNumber":"string","items":[],"status":"string","title":"string","introText":"string","notizen":"string","customFields":{},"sourceDocumentType":"string","sourceDocumentId":"string","createdAt":null,"updatedAt":null,"versandAm":null,"gesamtGewicht":null,"gesamtVolumen":null,"packstuecke":null,"pdfUrl":null,"belegkette":{"sourceOrder":{"orderNumber":"string"},"linkedInvoice":{"id":"string","invoiceNumber":"string"}}}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Lieferschein nicht gefunden"}},"operationId":"getApiV1DeliveriesById","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liefert einen einzelnen Lieferschein inkl. Belegfluss-Info. Zum Datensatz kommen die Nummer des Quellauftrags (`sourceOrder`) und der nachgeschlagene Kundenname; laesst sich eines davon nicht lesen, bleibt das Feld null und der Rest wird trotzdem geliefert. Eine Kennung ohne UUID-Form oder ein geloeschter Lieferschein ergibt 404, kein Serverfehler.","summary":"Liefert einen einzelnen Lieferschein inkl. Belegfluss-Info","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Lieferschein aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deliveryNumber":{"type":["string","null"]},"customerId":{"type":["string","null"]},"customerName":{"type":["string","null"]},"orderId":{"type":["string","null"]},"orderNumber":{"type":["string","null"]},"ausgestelltAm":{"type":"null"},"geliefertAm":{"type":"null"},"empfaenger":{},"spediteur":{"type":["string","null"]},"trackingNumber":{"type":["string","null"]},"items":{"type":["array","null"],"items":{}},"status":{"type":["string","null"]},"title":{"type":["string","null"]},"introText":{"type":["string","null"]},"notizen":{"type":["string","null"]},"customFields":{"type":["object","null"],"additionalProperties":{}},"sourceDocumentType":{"type":["string","null"]},"sourceDocumentId":{"type":["string","null"]},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"versandAm":{"type":"null"},"gesamtGewicht":{"type":"null"},"gesamtVolumen":{"type":"null"},"packstuecke":{"type":"null"},"pdfUrl":{"type":"null"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","deliveryNumber":"string","customerId":"string","customerName":"string","orderId":"string","orderNumber":"string","ausgestelltAm":null,"geliefertAm":null,"spediteur":"string","trackingNumber":"string","items":[],"status":"string","title":"string","introText":"string","notizen":"string","customFields":{},"sourceDocumentType":"string","sourceDocumentId":"string","createdAt":null,"updatedAt":null,"versandAm":null,"gesamtGewicht":null,"gesamtVolumen":null,"packstuecke":null,"pdfUrl":null}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Unzureichende Rolle"},"404":{"description":"Lieferschein nicht gefunden (`delivery_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"409":{"description":"Der Status laesst keine Bearbeitung mehr zu — nur `draft` und `picked` sind aenderbar. Wer nur Versanddaten korrigieren will, nimmt `PATCH /:id/versand`; das geht auch nach dem Versand.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"422":{"description":"Eine Mandanten-Regel wurde verletzt (`entity_rule_violation`) — die Antwort nennt die verletzten Regeln einzeln","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"putApiV1DeliveriesById","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Schreibt einen Lieferschein neu. Trotz PUT sind alle Felder einzeln optional: was der Rumpf nicht nennt, wird aus dem Bestand uebernommen. Eine Ausnahme sind die eigenen Felder — schickt der Aufruf `customFields`, ERSETZT das den ganzen Block; laesst er sie weg, bleibt er erhalten. Aenderbar sind nur Belege im Status `draft` oder `picked` (sonst 409); Versanddaten nach dem Versand korrigiert `PATCH /:id/versand`. Vor dem Schreiben laufen die Mandanten-Regeln: eine Verletzung bricht mit 422 ab, berechnete Werte fliessen in die eigenen Felder. Ein gecachtes PDF des Belegs wird danach verworfen. Ab Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"orderId":{"type":"string","format":"uuid"},"ausgestelltAm":{"type":"string","format":"date"},"versandAm":{"type":"string","format":"date"},"geliefertAm":{"type":"string","format":"date"},"empfaenger":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string","default":"DE"}}},"spediteur":{"type":"string","maxLength":100},"trackingNumber":{"type":"string","maxLength":100},"items":{"type":"array","items":{"type":"object","properties":{"articleId":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":"string","minLength":1},"quantity":{"type":"number","exclusiveMinimum":0},"unit":{"type":"string","default":"Stk"},"unitPrice":{"type":"number","minimum":0,"default":0},"total":{"type":"number","minimum":0,"default":0},"taxRate":{"type":"number","minimum":0},"sourceOrderItemId":{"type":"string","format":"uuid"}},"required":["description","quantity"]},"default":[]},"gesamtGewicht":{"type":"number","minimum":0},"gesamtVolumen":{"type":"number","minimum":0},"packstuecke":{"type":"integer","minimum":0},"status":{"type":"string","enum":["draft","picked","shipped","delivered","cancelled"],"default":"draft"},"title":{"type":"string","maxLength":200},"introText":{"type":"string"},"notizen":{"type":"string"},"pdfUrl":{"type":"string","format":"uri"},"sourceDocumentType":{"type":"string","maxLength":20},"sourceDocumentId":{"type":"string","format":"uuid"},"customFields":{"type":"object","additionalProperties":{}}}},"example":{"customerId":"00000000-0000-4000-8000-000000000000","orderId":"00000000-0000-4000-8000-000000000000","ausgestelltAm":"2026-01-01","versandAm":"2026-01-01","geliefertAm":"2026-01-01","empfaenger":{"name":"string","street":"string","city":"string","zip":"string","country":"string"},"spediteur":"string","trackingNumber":"string","items":[{"articleId":"00000000-0000-4000-8000-000000000000","title":"string","description":"string","quantity":1,"unit":"string","unitPrice":0,"total":0,"taxRate":0,"sourceOrderItemId":"00000000-0000-4000-8000-000000000000"}],"gesamtGewicht":0,"gesamtVolumen":0,"packstuecke":0,"status":"draft","title":"string","introText":"string","notizen":"string","pdfUrl":"https://example.com","sourceDocumentType":"string","sourceDocumentId":"00000000-0000-4000-8000-000000000000","customFields":{}}}}},"summary":"Schreibt einen Lieferschein neu","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Lieferschein geloescht — als Soft-Delete: die Zeile bleibt stehen und traegt nur `deleted_at`. Aus allen Listen und Kennzahlen faellt sie damit heraus.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Unzureichende Rolle"},"404":{"description":"Lieferschein nicht gefunden (`delivery_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"409":{"description":"Nur Entwuerfe koennen geloescht werden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"deleteApiV1DeliveriesById","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Loescht einen Lieferschein weich: die Zeile bleibt stehen und bekommt nur `deleted_at` gesetzt, womit sie aus Listen und Kennzahlen verschwindet. Erlaubt ist das ausschliesslich im Status `draft` — jeder andere Status ergibt 409, ein bereits geloeschter oder unbekannter Beleg 404. Eine Ruecknahme dieses Schritts bietet die API nicht an. Ab Rolle `manager`.","summary":"Loescht einen Lieferschein weich","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/deliveries/{id}/status":{"patch":{"responses":{"200":{"description":"Status aktualisiert. Der Lieferschein kommt flach zurueck, `lager` daneben traegt den Bericht der ausgeloesten Lagerbuchung. Beim Wechsel auf `delivered` geht zusaetzlich das Ereignis `delivery.goods_issued` an die Webhook-Abonnenten.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deliveryNumber":{"type":["string","null"]},"customerId":{"type":["string","null"]},"customerName":{"type":["string","null"]},"orderId":{"type":["string","null"]},"orderNumber":{"type":["string","null"]},"ausgestelltAm":{"type":"null"},"geliefertAm":{"type":"null"},"empfaenger":{},"spediteur":{"type":["string","null"]},"trackingNumber":{"type":["string","null"]},"items":{"type":["array","null"],"items":{}},"status":{"type":["string","null"]},"title":{"type":["string","null"]},"introText":{"type":["string","null"]},"notizen":{"type":["string","null"]},"customFields":{"type":["object","null"],"additionalProperties":{}},"sourceDocumentType":{"type":["string","null"]},"sourceDocumentId":{"type":["string","null"]},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"versandAm":{"type":"null"},"gesamtGewicht":{"type":"null"},"gesamtVolumen":{"type":"null"},"packstuecke":{"type":"null"},"pdfUrl":{"type":"null"},"lager":{}},"required":["id"],"additionalProperties":false},"example":{"id":"string","deliveryNumber":"string","customerId":"string","customerName":"string","orderId":"string","orderNumber":"string","ausgestelltAm":null,"geliefertAm":null,"spediteur":"string","trackingNumber":"string","items":[],"status":"string","title":"string","introText":"string","notizen":"string","customFields":{},"sourceDocumentType":"string","sourceDocumentId":"string","createdAt":null,"updatedAt":null,"versandAm":null,"gesamtGewicht":null,"gesamtVolumen":null,"packstuecke":null,"pdfUrl":null}}}},"400":{"description":"Ungültiger Status-Übergang"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Unzureichende Rolle"},"404":{"description":"Lieferschein nicht gefunden (`delivery_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"422":{"description":"Die Lagerbuchung scheiterte — und damit der ganze Statuswechsel. Einen Zustand „Status gewechselt, aber nicht gebucht\" gibt es nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"patchApiV1DeliveriesByIdStatus","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Setzt den Status eines Lieferscheins","description":"Setzt den Status eines Lieferscheins (draft→picked→shipped→delivered, cancelled von beliebigem)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","picked","shipped","delivered","cancelled"]}},"required":["status"]},"example":{"status":"draft"}}}}}},"/api/v1/deliveries/{id}/versand":{"patch":{"responses":{"200":{"description":"Versanddaten aktualisiert. Anders als `PUT /:id` geht dieser Aufruf auch nach dem Versand — er ruehrt den Beleg-Inhalt nicht an.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deliveryNumber":{"type":["string","null"]},"customerId":{"type":["string","null"]},"customerName":{"type":["string","null"]},"orderId":{"type":["string","null"]},"orderNumber":{"type":["string","null"]},"ausgestelltAm":{"type":"null"},"geliefertAm":{"type":"null"},"empfaenger":{},"spediteur":{"type":["string","null"]},"trackingNumber":{"type":["string","null"]},"items":{"type":["array","null"],"items":{}},"status":{"type":["string","null"]},"title":{"type":["string","null"]},"introText":{"type":["string","null"]},"notizen":{"type":["string","null"]},"customFields":{"type":["object","null"],"additionalProperties":{}},"sourceDocumentType":{"type":["string","null"]},"sourceDocumentId":{"type":["string","null"]},"createdAt":{"type":"null"},"updatedAt":{"type":"null"},"versandAm":{"type":"null"},"gesamtGewicht":{"type":"null"},"gesamtVolumen":{"type":"null"},"packstuecke":{"type":"null"},"pdfUrl":{"type":"null"}},"required":["id"],"additionalProperties":false},"example":{"id":"string","deliveryNumber":"string","customerId":"string","customerName":"string","orderId":"string","orderNumber":"string","ausgestelltAm":null,"geliefertAm":null,"spediteur":"string","trackingNumber":"string","items":[],"status":"string","title":"string","introText":"string","notizen":"string","customFields":{},"sourceDocumentType":"string","sourceDocumentId":"string","createdAt":null,"updatedAt":null,"versandAm":null,"gesamtGewicht":null,"gesamtVolumen":null,"packstuecke":null,"pdfUrl":null}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Unzureichende Rolle"},"404":{"description":"Lieferschein nicht gefunden (`delivery_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"409":{"description":"Stornierte Lieferscheine sind gesperrt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"patchApiV1DeliveriesByIdVersand","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Korrigiert nur die Versanddaten, auch nach dem Versand","description":"Korrigiert nur die Versanddaten (Tracking, Spediteur, Versand-/Lieferdatum) — auch nach dem Versand. Beleg-Inhalt bleibt unverändert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"trackingNumber":{"type":["string","null"],"maxLength":100},"spediteur":{"type":["string","null"],"maxLength":100},"versandAm":{"type":["string","null"],"format":"date"},"geliefertAm":{"type":["string","null"],"format":"date"}}},"example":{"trackingNumber":"string","spediteur":"string","versandAm":"2026-01-01","geliefertAm":"2026-01-01"}}}}}},"/api/v1/deliveries/{id}/retoure":{"post":{"responses":{"200":{"description":"Ruecknahme gebucht. `retoure` traegt das Ergebnis der Lagerschicht; die Buchung laeuft in EINER Transaktion — scheitert sie, wird alles zurueckgerollt und nichts bleibt halb gebucht.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"retoure":{}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"400":{"description":"Falscher Status — nur versandte/gelieferte Belege"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Unzureichende Rolle"},"404":{"description":"Lieferschein nicht gefunden (`delivery_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"422":{"description":"Ruecknahmemenge uebersteigt die gelieferte Menge, oder die Lagerbuchung scheiterte — `error` traegt die Kennung, `message` den Grund","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"postApiV1DeliveriesByIdRetoure","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Bucht eine Ruecknahme zu einem Lieferschein. Je Position wird ueber `positionIndex`, `quantity` und `ziel` gesagt, wie viel zurueckkommt und wohin — `lager` erhoeht den Bestand am Lagerort des Belegs, `ausschuss` bucht auf den virtuellen Ausschuss-Ort. Nur Positionen mit hinterlegtem Artikel bewegen Bestand — reine Freitextzeilen bleiben unberuehrt. Die zurueckgenommene Menge wird am Beleg fortgeschrieben, dieselbe Position darf also mehrfach zurueck. Erlaubt nur bei Belegen im Status `shipped` oder `delivered` (sonst 400); mehr zurueckzunehmen als geliefert wurde, ergibt 422. Alles laeuft in EINER Transaktion — scheitert ein Schritt, bleibt nichts halb gebucht. Der optionale `grund` wird mitgeschrieben. Ab Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"positionIndex":{"type":"integer","minimum":0},"quantity":{"type":"number","exclusiveMinimum":0},"ziel":{"type":"string","enum":["lager","ausschuss"]}},"required":["positionIndex","quantity","ziel"]},"minItems":1},"grund":{"type":"string","maxLength":500}},"required":["items"]},"example":{"items":[{"positionIndex":0,"quantity":1,"ziel":"lager"}],"grund":"string"}}}},"summary":"Bucht eine Ruecknahme zu einem Lieferschein","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/deliveries/{id}/convert/invoice":{"post":{"responses":{"201":{"description":"Rechnung erstellt. `invoice` ist die ROHE Datenbankzeile, nicht serialisiert — welche Spalten sie traegt, bestimmt die Tabelle, nicht dieser Aufruf. Alles laeuft in EINER Transaktion: scheitert ein Schritt, gibt es weder Rechnung noch veraenderte `invoiced_qty`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"invoice":{},"deliveryId":{"type":"string"},"taxExempt":{"type":"boolean"},"taxNote":{}},"required":["ok","deliveryId","taxExempt"],"additionalProperties":false},"example":{"ok":true,"deliveryId":"string","taxExempt":true}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Lieferschein nicht gefunden (`delivery_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"409":{"description":"Der Status erlaubt keine Rechnungsstellung, oder es gibt bereits eine Rechnung (`already_invoiced`) — dann nennt die Antwort sie mit `existing_id` und `invoice_number`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"postApiV1DeliveriesByIdConvertInvoice","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Erstellt eine Rechnung aus dem Lieferschein. Aktualisiert invoiced_qty.","summary":"Erstellt eine Rechnung aus dem Lieferschein","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/deliveries/{id}/convert-to-invoice":{"post":{"responses":{"201":{"description":"Rechnung erstellt. `invoice` ist die ROHE Datenbankzeile, nicht serialisiert — welche Spalten sie traegt, bestimmt die Tabelle, nicht dieser Aufruf. Alles laeuft in EINER Transaktion: scheitert ein Schritt, gibt es weder Rechnung noch veraenderte `invoiced_qty`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"invoice":{},"deliveryId":{"type":"string"},"taxExempt":{"type":"boolean"},"taxNote":{}},"required":["ok","deliveryId","taxExempt"],"additionalProperties":false},"example":{"ok":true,"deliveryId":"string","taxExempt":true}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Lieferschein nicht gefunden (`delivery_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"409":{"description":"Der Status erlaubt keine Rechnungsstellung, oder es gibt bereits eine Rechnung (`already_invoiced`) — dann nennt die Antwort sie mit `existing_id` und `invoice_number`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"postApiV1DeliveriesByIdConvert-to-invoice","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Erstellt eine Rechnung aus dem Lieferschein. Aktualisiert invoiced_qty.","summary":"Erstellt eine Rechnung aus dem Lieferschein","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/deliveries/{id}/pdf":{"get":{"responses":{"200":{"description":"Der Lieferschein als PDF — OHNE Preise. Die Antwort traegt einen ETag; ein erneuter Abruf mit `If-None-Match` bekommt 304 und keine Bytes.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"304":{"description":"Unveraendert — der ETag stimmt, es kommt kein Rumpf"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Lieferschein nicht gefunden (`delivery_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"getApiV1DeliveriesByIdPdf","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Erzeugt den Lieferschein als PDF und liefert die Bytes. Ein Lieferschein zeigt KEINE Preise. Die Sprache ist die des Kunden, `?lang` sticht sie aus. Die Antwort traegt einen ETag, in den Aenderungsstand, Status, Tarif, Beleg-Design und Sprache einfliessen — ein Abruf mit passendem `If-None-Match` bekommt 304 ohne Rumpf, und jede dieser Groessen macht ein gecachtes PDF ungueltig. Gerendert wird nur, was der Cache nicht schon hat.","summary":"Erzeugt den Lieferschein als PDF und liefert die Bytes","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/deliveries/{id}/send":{"post":{"responses":{"200":{"description":"ACHTUNG: `simuliert: true` heisst, dass NICHTS hinausging. Auf dev laeuft der Versand immer gegen einen Mock (SES-Sandbox) und meldet trotzdem Erfolg — `messageId` ist dann eine Kennung des Mocks, kein Zustellnachweis. Wer den Wert ignoriert, zeigt „versendet\", obwohl keine Mail den Server verlassen hat.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"messageId":{"type":"string"},"sentAt":{"type":"string"},"recipients":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}}},"required":["to","cc","bcc"],"additionalProperties":false},"simuliert":{"type":"boolean"}},"required":["ok","messageId","sentAt","recipients","simuliert"],"additionalProperties":false},"example":{"ok":true,"messageId":"string","sentAt":"string","recipients":{"to":["string"],"cc":["string"],"bcc":["string"]},"simuliert":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"402":{"description":"Das E-Mail-Kontingent des Mandanten ist erschoepft. Der Rumpf ist die Begruendung des Kontingent-Waechters (`quotaGuard.reason`) — seine Form bestimmt die Billing-Schicht, nicht dieser Aufruf, deshalb hier keine Feldzusage."},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Lieferschein nicht gefunden (`delivery_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}},"500":{"description":"Versand fehlgeschlagen (`delivery_send_failed`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"},"existing_id":{"type":"string"},"invoice_number":{"type":"string"},"violations":{"type":"array","items":{}}},"required":["error"]}}}}},"operationId":"postApiV1DeliveriesByIdSend","tags":["Deliveries"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Schickt den Lieferschein per E-Mail an die Adressen in `to` (dazu `cc`/`bcc`). Das PDF haengt an, sofern `attachPdf` nicht ausdruecklich `false` ist; es wird OHNE Preise erzeugt, in der Sprache des Kunden bzw. der in `lang`. Betreff und Text kommen aus der Belegart-Vorlage des Mandanten, falls der Rumpf keine mitgibt; die Signatur des angemeldeten Nutzers wird angehaengt. Vor dem Versand greift das E-Mail-Kontingent (402), und ein nur notduerftig gerendertes PDF bricht den Versand ab (503), statt es dem Kunden zu schicken. Der STATUS des Lieferscheins aendert sich NICHT — der Versand wird nur im Aktivitaetsverlauf vermerkt (bei einem Entwurf zusaetzlich das gecachte PDF verworfen). Ab Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"cc":{"type":"array","items":{"type":"string","format":"email"}},"bcc":{"type":"array","items":{"type":"string","format":"email"}},"subject":{"type":"string","minLength":1,"maxLength":255},"message":{"type":"string","maxLength":4000},"attachPdf":{"type":"boolean"},"includePortalLink":{"type":"boolean","default":true},"lang":{"type":"string","enum":["de","en","fr","es","nl","da","pl","cs","zh"]},"template":{"type":"string","enum":["doc-quote","doc-order","doc-delivery","doc-invoice","dunning-level1","dunning-level2","dunning-level3"]},"extraAttachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"contentBase64":{"type":"string","minLength":1},"contentType":{"type":"string","maxLength":100}},"required":["filename","contentBase64"]},"maxItems":10},"attachmentMode":{"type":"string","enum":["separate","merge"]}},"required":["to"]},"example":{"to":["beispiel@example.com"],"cc":["beispiel@example.com"],"bcc":["beispiel@example.com"],"subject":"string","message":"string","attachPdf":true,"includePortalLink":true,"lang":"de","template":"doc-quote","extraAttachments":[{"filename":"string","contentBase64":"string","contentType":"string"}],"attachmentMode":"separate"}}}},"summary":"Schickt den Lieferschein per E-Mail an die Adressen in `to` (dazu `cc`/`bcc`)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/document-chain/quote/{id}/convert-to-order":{"post":{"responses":{"201":{"description":"Neuer Auftrag erstellt — in der schlanken Form dieser Datei, nicht der aus /orders","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Auftrags"},"orderNumber":{"description":"Auftragsnummer; faellt auf die Altspalte order_number zurueck"},"customerId":{"type":["string","null"],"description":"Kunde des Auftrags"},"customerName":{"description":"Kundenname, wie er beim Umwandeln uebernommen wurde"},"title":{"description":"Betreff des Auftrags"},"status":{"description":"Status des Auftrags; ein neu erzeugter steht auf draft"},"positions":{"description":"Die Auftragszeilen"},"subtotal":{"type":"number","description":"Nettosumme als Zahl"},"tax":{"type":"number","description":"Steuerbetrag als Zahl"},"total":{"type":"number","description":"Bruttosumme als Zahl"},"sourceDocumentId":{"description":"Der Beleg, aus dem dieser entstanden ist"},"sourceDocumentType":{"description":"Belegart der Herkunft, etwa quote"},"totalQty":{"type":"number","description":"Bestellte Gesamtmenge — die Grundlage beider Restmengen"},"deliveredQty":{"type":"number","description":"Davon bereits geliefert"},"invoicedQty":{"type":"number","description":"Davon bereits fakturiert"},"expectedDelivery":{"description":"Zugesagter Liefertermin"},"notes":{"description":"Notiz zum Auftrag"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","customerId","subtotal","tax","total","totalQty","deliveredQty","invoicedQty"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","customerId":"string","subtotal":0,"tax":0,"total":0,"totalQty":0,"deliveredQty":0,"invoicedQty":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Angebot nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["quote_not_found","order_not_found","delivery_not_found","invoice_not_found"],"description":"Fester Fehlerschluessel; er nennt, WELCHER Beleg fehlt"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Angebot wurde bereits in Auftrag umgewandelt — die Antwort nennt den Auftrag von damals","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"already_converted","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"},"orderId":{"type":"string","description":"Der Auftrag, der beim ersten Mal entstanden ist"}},"required":["error","message","orderId"],"additionalProperties":false}}}},"422":{"description":"Angebot hat ungültigen Status, oder eine Teilposition zeigt ins Leere","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_quote_status","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung mit dem aktuellen Status im Text"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"Unerwarteter Serverfehler — alles, was KEIN Verbindungsfehler ist. Ein erneuter Versuch hilft hier in der Regel nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Feste Meldung ohne Innereien"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nur bei einem echten Verbindungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Document-chainQuoteByIdConvert-to-order","tags":["DocumentChain"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert quote to order","description":"Wandelt ein Angebot in einen Auftrag um (Belegkette: Quote → Order). Das Angebot muss im Status `sent` oder `accepted` stehen und darf noch nicht umgewandelt sein. Der neue Auftrag entsteht als Entwurf; das Angebot wechselt auf `accepted` und merkt sich den Auftrag. Beides passiert in einer Transaktion. Die Antwort ist der NEUE AUFTRAG — nicht das Angebot und nicht die Kette.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"partialItems":{"type":"array","items":{"type":"object","properties":{"positionIndex":{"type":"integer","minimum":0},"quantity":{"type":"number","exclusiveMinimum":0}},"required":["positionIndex","quantity"]}},"expectedDelivery":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"notes":{"type":"string"}}},"example":{"partialItems":[{"positionIndex":0,"quantity":1}],"expectedDelivery":"2026-01-01T12:00:00.000Z","notes":"string"}}}}}},"/api/v1/document-chain/order/{id}/convert-to-delivery":{"post":{"responses":{"201":{"description":"Lieferschein erstellt — in der schlanken Form dieser Datei","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Lieferscheins"},"deliveryNumber":{"description":"Lieferscheinnummer"},"orderId":{"type":["string","null"],"description":"Auftrag, zu dem geliefert wird"},"sourceDocumentId":{"description":"Der Beleg, aus dem dieser entstanden ist"},"sourceDocumentType":{"description":"Belegart der Herkunft, etwa order"},"status":{"description":"Status des Lieferscheins"},"items":{"description":"Die gelieferten Zeilen — hier `items`, nicht `positions`"},"spediteur":{"description":"Beauftragter Spediteur"},"expectedDelivery":{"description":"Zugesagter Liefertermin"},"deliveredAt":{"description":"Zeitpunkt der Auslieferung"},"notes":{"description":"Notiz zum Lieferschein"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","orderId"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","orderId":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["quote_not_found","order_not_found","delivery_not_found","invoice_not_found"],"description":"Fester Fehlerschluessel; er nennt, WELCHER Beleg fehlt"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Übermenge — die Antwort nennt die offene und die angefragte Menge","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"quantity_exceeded","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"},"openToDeliver":{"type":"number","description":"Was noch geliefert werden darf"},"requested":{"type":"number","description":"Was angefragt wurde"}},"required":["error","message","openToDeliver","requested"],"additionalProperties":false,"description":"Beim Liefern"},{"type":"object","properties":{"error":{"type":"string","const":"quantity_exceeded","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"},"openToInvoice":{"type":"number","description":"Was noch fakturiert werden darf"},"requested":{"type":"number","description":"Was angefragt wurde"}},"required":["error","message","openToInvoice","requested"],"additionalProperties":false,"description":"Beim Fakturieren"}]}}}},"500":{"description":"Unerwarteter Serverfehler — alles, was KEIN Verbindungsfehler ist. Ein erneuter Versuch hilft hier in der Regel nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Feste Meldung ohne Innereien"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nur bei einem echten Verbindungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Document-chainOrderByIdConvert-to-delivery","tags":["DocumentChain"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert order to delivery","description":"Erstellt einen Lieferschein aus einem Auftrag (Belegkette: Order → Delivery). Teillieferungen sind vorgesehen: die Mengen kommen aus dem Aufruf und werden gegen die noch offene Menge des Auftrags geprüft. Der Zähler `deliveredQty` des Auftrags wächst mit. Die Antwort ist der NEUE LIEFERSCHEIN.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"orderItemIndex":{"type":"integer","minimum":0},"quantity":{"type":"number","exclusiveMinimum":0}},"required":["orderItemIndex","quantity"]}},"spediteur":{"type":"string"},"expectedDelivery":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"notes":{"type":"string"}},"required":["items"]},"example":{"items":[{"orderItemIndex":0,"quantity":1}],"spediteur":"string","expectedDelivery":"2026-01-01T12:00:00.000Z","notes":"string"}}}}}},"/api/v1/document-chain/delivery/{id}/convert-to-invoice":{"post":{"responses":{"201":{"description":"Rechnung erstellt — in der schlanken Form dieser Datei","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Rechnung"},"invoiceNumber":{"description":"Rechnungsnummer; faellt auf die Altspalte invoice_number zurueck"},"customerId":{"type":["string","null"],"description":"Kunde der Rechnung"},"customerName":{"description":"Kundenname; leere Zeichenkette, wenn keiner ermittelbar war"},"orderId":{"type":["string","null"],"description":"Auftrag, aus dem fakturiert wurde"},"sourceDocumentId":{"description":"Der Beleg, aus dem diese entstanden ist"},"sourceDocumentType":{"description":"Belegart der Herkunft, order oder delivery"},"status":{"description":"Status der Rechnung"},"items":{"description":"Die Rechnungszeilen — hier `items`, waehrend das Modul /invoices sie `positions` nennt"},"subtotal":{"type":"number","description":"Nettosumme als Zahl"},"tax":{"type":"number","description":"Steuerbetrag als Zahl"},"total":{"type":"number","description":"Bruttosumme als Zahl"},"dueDate":{"description":"Faelligkeit"},"notes":{"description":"Notiz zur Rechnung"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","customerId","orderId","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","customerId":"string","orderId":"string","subtotal":0,"tax":0,"total":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Lieferschein nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["quote_not_found","order_not_found","delivery_not_found","invoice_not_found"],"description":"Fester Fehlerschluessel; er nennt, WELCHER Beleg fehlt"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"Unerwarteter Serverfehler — alles, was KEIN Verbindungsfehler ist. Ein erneuter Versuch hilft hier in der Regel nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Feste Meldung ohne Innereien"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nur bei einem echten Verbindungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Document-chainDeliveryByIdConvert-to-invoice","tags":["DocumentChain"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert delivery to invoice","description":"Erstellt eine Rechnung aus einem Lieferschein (Belegkette: Delivery → Invoice). Berechnet werden die Zeilen des Lieferscheins; hängt er an einem Auftrag, wächst dessen Zähler `invoicedQty` mit. ACHTUNG: es gibt KEINE Sperre gegen Doppelfaktura — derselbe Lieferschein lässt sich mehrfach berechnen, und es entsteht jedes Mal eine neue Rechnung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string"},"dueDate":{"type":"string","format":"date"}}},"example":{"notes":"string","dueDate":"2026-01-01"}}}}}},"/api/v1/document-chain/order/{id}/convert-to-invoice":{"post":{"responses":{"201":{"description":"Rechnung erstellt — in der schlanken Form dieser Datei","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Rechnung"},"invoiceNumber":{"description":"Rechnungsnummer; faellt auf die Altspalte invoice_number zurueck"},"customerId":{"type":["string","null"],"description":"Kunde der Rechnung"},"customerName":{"description":"Kundenname; leere Zeichenkette, wenn keiner ermittelbar war"},"orderId":{"type":["string","null"],"description":"Auftrag, aus dem fakturiert wurde"},"sourceDocumentId":{"description":"Der Beleg, aus dem diese entstanden ist"},"sourceDocumentType":{"description":"Belegart der Herkunft, order oder delivery"},"status":{"description":"Status der Rechnung"},"items":{"description":"Die Rechnungszeilen — hier `items`, waehrend das Modul /invoices sie `positions` nennt"},"subtotal":{"type":"number","description":"Nettosumme als Zahl"},"tax":{"type":"number","description":"Steuerbetrag als Zahl"},"total":{"type":"number","description":"Bruttosumme als Zahl"},"dueDate":{"description":"Faelligkeit"},"notes":{"description":"Notiz zur Rechnung"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","customerId","orderId","subtotal","tax","total"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","customerId":"string","orderId":"string","subtotal":0,"tax":0,"total":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Auftrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["quote_not_found","order_not_found","delivery_not_found","invoice_not_found"],"description":"Fester Fehlerschluessel; er nennt, WELCHER Beleg fehlt"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Übermenge bei partieller Abrechnung, oder eine Position zeigt ins Leere","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"quantity_exceeded","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"},"openToDeliver":{"type":"number","description":"Was noch geliefert werden darf"},"requested":{"type":"number","description":"Was angefragt wurde"}},"required":["error","message","openToDeliver","requested"],"additionalProperties":false,"description":"Beim Liefern"},{"type":"object","properties":{"error":{"type":"string","const":"quantity_exceeded","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung fuer die Oberflaeche, deutsch"},"openToInvoice":{"type":"number","description":"Was noch fakturiert werden darf"},"requested":{"type":"number","description":"Was angefragt wurde"}},"required":["error","message","openToInvoice","requested"],"additionalProperties":false,"description":"Beim Fakturieren"}]}}}},"500":{"description":"Unerwarteter Serverfehler — alles, was KEIN Verbindungsfehler ist. Ein erneuter Versuch hilft hier in der Regel nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Feste Meldung ohne Innereien"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nur bei einem echten Verbindungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Document-chainOrderByIdConvert-to-invoice","tags":["DocumentChain"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Convert order to invoice","description":"Direktweg Auftrag → Rechnung ohne Lieferschein (Belegkette: Order → Invoice). Ohne `items` wandern ALLE Auftragspositionen in die Rechnung; mit `items` nur die angegebenen, geprüft gegen die noch offene Menge. Der Zähler `invoicedQty` des Auftrags wächst mit.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"orderItemIndex":{"type":"integer","minimum":0},"quantity":{"type":"number","exclusiveMinimum":0}},"required":["orderItemIndex","quantity"]}},"notes":{"type":"string"},"dueDate":{"type":"string","format":"date"}}},"example":{"items":[{"orderItemIndex":0,"quantity":1}],"notes":"string","dueDate":"2026-01-01"}}}}}},"/api/v1/document-chain/{type}/{id}/trace":{"get":{"responses":{"200":{"description":"Belegkette mit quote, order, deliveries, invoices, reconciliation","content":{"application/json":{"schema":{"type":"object","properties":{"quote":{"type":["object","null"],"properties":{"id":{"description":"Kennung des Angebots"},"quoteNumber":{"description":"Angebotsnummer"},"status":{"description":"Status des Angebots"},"customerName":{"description":"Kundenname"},"total":{"type":"number","description":"Angebotssumme als Zahl"},"convertedToOrderId":{"description":"Der Auftrag, in den es umgewandelt wurde"},"createdAt":{"description":"Anlagezeitpunkt"}},"required":["total"],"additionalProperties":false,"description":"Das Angebot am Anfang der Kette — ein Auszug, nicht die volle Angebotsform; null, wenn keines existiert"},"order":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Auftrags"},"orderNumber":{"description":"Auftragsnummer; faellt auf die Altspalte order_number zurueck"},"customerId":{"type":["string","null"],"description":"Kunde des Auftrags"},"customerName":{"description":"Kundenname, wie er beim Umwandeln uebernommen wurde"},"title":{"description":"Betreff des Auftrags"},"status":{"description":"Status des Auftrags; ein neu erzeugter steht auf draft"},"positions":{"description":"Die Auftragszeilen"},"subtotal":{"type":"number","description":"Nettosumme als Zahl"},"tax":{"type":"number","description":"Steuerbetrag als Zahl"},"total":{"type":"number","description":"Bruttosumme als Zahl"},"sourceDocumentId":{"description":"Der Beleg, aus dem dieser entstanden ist"},"sourceDocumentType":{"description":"Belegart der Herkunft, etwa quote"},"totalQty":{"type":"number","description":"Bestellte Gesamtmenge — die Grundlage beider Restmengen"},"deliveredQty":{"type":"number","description":"Davon bereits geliefert"},"invoicedQty":{"type":"number","description":"Davon bereits fakturiert"},"expectedDelivery":{"description":"Zugesagter Liefertermin"},"notes":{"description":"Notiz zum Auftrag"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","customerId","subtotal","tax","total","totalQty","deliveredQty","invoicedQty"],"additionalProperties":false,"description":"Der Auftrag in der Mitte der Kette; null, wenn keiner existiert"},"deliveries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Lieferscheins"},"deliveryNumber":{"description":"Lieferscheinnummer"},"orderId":{"type":["string","null"],"description":"Auftrag, zu dem geliefert wird"},"sourceDocumentId":{"description":"Der Beleg, aus dem dieser entstanden ist"},"sourceDocumentType":{"description":"Belegart der Herkunft, etwa order"},"status":{"description":"Status des Lieferscheins"},"items":{"description":"Die gelieferten Zeilen — hier `items`, nicht `positions`"},"spediteur":{"description":"Beauftragter Spediteur"},"expectedDelivery":{"description":"Zugesagter Liefertermin"},"deliveredAt":{"description":"Zeitpunkt der Auslieferung"},"notes":{"description":"Notiz zum Lieferschein"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","orderId"],"additionalProperties":false},"description":"Alle Lieferscheine zum Auftrag"},"invoices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung der Rechnung"},"invoiceNumber":{"description":"Rechnungsnummer; faellt auf die Altspalte invoice_number zurueck"},"customerId":{"type":["string","null"],"description":"Kunde der Rechnung"},"customerName":{"description":"Kundenname; leere Zeichenkette, wenn keiner ermittelbar war"},"orderId":{"type":["string","null"],"description":"Auftrag, aus dem fakturiert wurde"},"sourceDocumentId":{"description":"Der Beleg, aus dem diese entstanden ist"},"sourceDocumentType":{"description":"Belegart der Herkunft, order oder delivery"},"status":{"description":"Status der Rechnung"},"items":{"description":"Die Rechnungszeilen — hier `items`, waehrend das Modul /invoices sie `positions` nennt"},"subtotal":{"type":"number","description":"Nettosumme als Zahl"},"tax":{"type":"number","description":"Steuerbetrag als Zahl"},"total":{"type":"number","description":"Bruttosumme als Zahl"},"dueDate":{"description":"Faelligkeit"},"notes":{"description":"Notiz zur Rechnung"},"createdAt":{"description":"Anlagezeitpunkt"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","customerId","orderId","subtotal","tax","total"],"additionalProperties":false},"description":"Alle Rechnungen zum Auftrag"},"reconciliation":{"type":"object","properties":{"ordered":{"type":"number","description":"Bestellte Menge laut Auftrag"},"delivered":{"type":"number","description":"Davon geliefert"},"invoiced":{"type":"number","description":"Davon fakturiert"},"openToDeliver":{"type":"number","minimum":0,"description":"Noch zu liefern; nie negativ, auch bei Uebermenge"},"openToInvoice":{"type":"number","minimum":0,"description":"Noch zu fakturieren; nie negativ, auch bei Uebermenge"}},"required":["ordered","delivered","invoiced","openToDeliver","openToInvoice"],"additionalProperties":false,"description":"Mengenabgleich — ALLE Zahlen stammen aus den Zaehlern des Auftrags, nicht aus den Zeilen der Belege. Ohne Auftrag sind sie 0"}},"required":["quote","order","deliveries","invoices","reconciliation"],"additionalProperties":false},"example":{"quote":{"total":0},"order":{"id":"00000000-0000-4000-8000-000000000000","customerId":"string","subtotal":0,"tax":0,"total":0,"totalQty":0,"deliveredQty":0,"invoicedQty":0},"deliveries":[{"id":"00000000-0000-4000-8000-000000000000","orderId":"string"}],"invoices":[{"id":"00000000-0000-4000-8000-000000000000","customerId":"string","orderId":"string","subtotal":0,"tax":0,"total":0}],"reconciliation":{"ordered":0,"delivered":0,"invoiced":0,"openToDeliver":0,"openToInvoice":0}}}}},"400":{"description":"Ungültiger Belegtyp — erlaubt sind quote, order, delivery und invoice","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_type","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Meldung mit den erlaubten Werten im Text"}},"required":["error","message"],"additionalProperties":false}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"404":{"description":"Beleg nicht gefunden — dieser Aufruf antwortet OHNE `message`, anders als die Umwandlungen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["quote_not_found","order_not_found","delivery_not_found","invoice_not_found"],"description":"Fester Fehlerschluessel; er nennt, WELCHER Beleg fehlt"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Unerwarteter Serverfehler — alles, was KEIN Verbindungsfehler ist. Ein erneuter Versuch hilft hier in der Regel nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Feste Meldung ohne Innereien"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nur bei einem echten Verbindungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Document-chainByTypeByIdTrace","tags":["DocumentChain"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"type","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Trace document chain","description":"Liefert die vollständige Belegkette für einen beliebigen Belegtyp mit Reconciliation-Daten. Egal, an welcher Stelle der Kette man einsteigt — die Antwort hat immer dieselbe Form. Der Auftrag ist der Angelpunkt: hängt der Beleg an keinem, bleiben die Listen leer und der Mengenabgleich steht auf 0. Die Zahlen des Abgleichs stammen aus den ZÄHLERN des Auftrags, nicht aus den Zeilen der Belege — laufen beide auseinander, sieht man es hier nicht."}},"/api/v1/document-chain/reconciliation/orders":{"get":{"responses":{"200":{"description":"Reconciliation-Liste mit offenen Mengen; negative Werte bedeuten Übermenge","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Auftrags"},"orderNumber":{"description":"Auftragsnummer"},"customerName":{"description":"Kundenname"},"status":{"description":"Status des Auftrags"},"orderedQty":{"type":"number","description":"Bestellte Menge"},"deliveredQty":{"type":"number","description":"Davon geliefert"},"invoicedQty":{"type":"number","description":"Davon fakturiert"},"openToDeliver":{"type":"number","description":"Noch zu liefern; kann hier NEGATIV sein, anders als in der Belegkette — bei Uebermenge wird nicht gekappt"},"openToInvoice":{"type":"number","description":"Noch zu fakturieren; kann hier NEGATIV sein, anders als in der Belegkette"},"createdAt":{"description":"Anlagezeitpunkt des Auftrags"},"updatedAt":{"description":"Letzte Aenderung"}},"required":["id","orderedQty","deliveredQty","invoicedQty","openToDeliver","openToInvoice"],"additionalProperties":false},"description":"Die offenen Auftraege dieser Seite, neueste zuerst"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angewendete Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Datensaetze"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl offener Auftraege"}},"required":["limit","offset","total"],"additionalProperties":false,"description":"Seitenangaben — limit/offset, keine Seitennummer"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, in dem gelesen wurde"},"source":{"type":"string","const":"db","description":"Datenquelle; hier immer die Datenbank"}},"required":["tenantId","source"],"additionalProperties":false,"description":"Angaben zur Abfrage"}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","orderedQty":0,"deliveredQty":0,"invoicedQty":0,"openToDeliver":0,"openToInvoice":0}],"pagination":{"limit":1,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Rolle unterhalb `user`"},"500":{"description":"Unerwarteter Serverfehler — alles, was KEIN Verbindungsfehler ist. Ein erneuter Versuch hilft hier in der Regel nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"internal_error","description":"Fester Fehlerschluessel"},"message":{"type":"string","minLength":1,"description":"Feste Meldung ohne Innereien"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nur bei einem echten Verbindungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Document-chainReconciliationOrders","tags":["DocumentChain"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"description":"Mengen-Reconciliation-Report: Alle Aufträge mit Differenz zwischen ordered/delivered/invoiced. Aufgeführt wird, wo noch etwas offen ist; Aufträge im Status `fulfilled` und gelöschte bleiben außen vor. Anders als in der Belegkette werden die offenen Mengen hier NICHT bei null gekappt — eine Übermenge zeigt sich als negative Zahl, und genau daran erkennt man sie.","summary":"Quantity reconciliation report"}},"/api/v1/bonus/regeln":{"get":{"responses":{"200":{"description":"Liste der Regeln — mit `total`, ohne Paginierung. Enthaelt auch `expired` und `inactive`: geloescht wird eine Regel nie.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"name":{},"beschreibung":{},"typ":{},"basis":{},"prozentSatz":{"type":["number","null"]},"staffeln":{},"fixbetrag":{"type":["number","null"]},"gueltigVon":{},"gueltigBis":{},"mitarbeiterIds":{},"kundenIds":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["prozentSatz","fixbetrag"],"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"prozentSatz":0,"fixbetrag":0}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1BonusRegeln","tags":["Bonus"],"parameters":[],"summary":"Listet alle Provisionsregeln des Mandanten","description":"Liest provisions_regeln vollstaendig, neueste zuerst — ohne Filter und ohne Blaetterung. Als einzige Route dieser Datei verlangt sie keine Manager-Rolle: angemeldet zu sein genuegt. Da Regeln nie geloescht, sondern nur auf `expired` gesetzt werden, stehen auch abgelaufene und inaktive in der Liste."},"post":{"responses":{"201":{"description":"Regel angelegt. Je nach `typ` ist entweder `prozentSatz`, `staffeln` oder `fixbetrag` gefuellt — die jeweils anderen bleiben `null`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"beschreibung":{},"typ":{},"basis":{},"prozentSatz":{"type":["number","null"]},"staffeln":{},"fixbetrag":{"type":["number","null"]},"gueltigVon":{},"gueltigBis":{},"mitarbeiterIds":{},"kundenIds":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["prozentSatz","fixbetrag"],"additionalProperties":false},"example":{"prozentSatz":0,"fixbetrag":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1BonusRegeln","tags":["Bonus"],"parameters":[],"summary":"Legt eine neue Provisionsregel an","description":"Schreibt eine Zeile in provisions_regeln (Manager-Rolle). Pflicht sind Name, `typ`, `basis` und `gueltigVon`; ohne Angabe gilt `status: \"active\"`. `prozentSatz`, `staffeln` und `fixbetrag` werden alle drei gespeichert, gerechnet wird spaeter aber nur mit dem, was zu `typ` passt. `basis` entscheidet, ob die spaetere Berechnung Rechnungen oder Auftraege summiert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"beschreibung":{"type":["string","null"]},"typ":{"type":"string","enum":["prozent_umsatz","prozent_db","gestaffelt","fixbetrag"]},"basis":{"type":"string","enum":["invoice","order"]},"prozentSatz":{"type":["number","null"],"minimum":0,"maximum":100},"staffeln":{"type":["array","null"],"items":{"type":"object","properties":{"vonBetrag":{"type":"number","minimum":0},"bisBetrag":{"type":["number","null"],"minimum":0},"satz":{"type":"number","minimum":0,"maximum":100}},"required":["vonBetrag","satz"]}},"fixbetrag":{"type":["number","null"],"minimum":0},"gueltigVon":{"type":"string","format":"date"},"gueltigBis":{"type":["string","null"],"format":"date"},"mitarbeiterIds":{"type":["array","null"],"items":{"type":"string","format":"uuid"}},"kundenIds":{"type":["array","null"],"items":{"type":"string","format":"uuid"}},"status":{"type":"string","enum":["active","inactive","expired"],"default":"active"}},"required":["name","typ","basis","gueltigVon"]},"example":{"name":"string","beschreibung":"string","typ":"prozent_umsatz","basis":"invoice","prozentSatz":0,"staffeln":[{"vonBetrag":0,"bisBetrag":0,"satz":0}],"fixbetrag":0,"gueltigVon":"2026-01-01","gueltigBis":"2026-01-01","mitarbeiterIds":["00000000-0000-4000-8000-000000000000"],"kundenIds":["00000000-0000-4000-8000-000000000000"],"status":"active"}}}}}},"/api/v1/bonus/regeln/{id}":{"get":{"responses":{"200":{"description":"Regel — nackt, ohne Huelle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"beschreibung":{},"typ":{},"basis":{},"prozentSatz":{"type":["number","null"]},"staffeln":{},"fixbetrag":{"type":["number","null"]},"gueltigVon":{},"gueltigBis":{},"mitarbeiterIds":{},"kundenIds":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["prozentSatz","fixbetrag"],"additionalProperties":false},"example":{"prozentSatz":0,"fixbetrag":0}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Regel nicht gefunden (`regel_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1BonusRegelnById","tags":["Bonus"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelne Provisionsregel","description":"Liest eine Regel ueber ihre Kennung aus provisions_regeln (Manager-Rolle) und liefert sie ohne Umschlag, also nicht unter `data`. Ist die Kennung unbekannt, antwortet der Endpunkt 404 mit `regel_not_found`."},"put":{"responses":{"200":{"description":"Regel aktualisiert. Bereits erzeugte Berechnungen werden NICHT neu gerechnet — sie behalten den Betrag, der zum Zeitpunkt der Berechnung galt.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"beschreibung":{},"typ":{},"basis":{},"prozentSatz":{"type":["number","null"]},"staffeln":{},"fixbetrag":{"type":["number","null"]},"gueltigVon":{},"gueltigBis":{},"mitarbeiterIds":{},"kundenIds":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["prozentSatz","fixbetrag"],"additionalProperties":false},"example":{"prozentSatz":0,"fixbetrag":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Regel nicht gefunden (`regel_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1BonusRegelnById","tags":["Bonus"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Provisionsregel aktualisieren","description":"Aendert eine Regel (Manager-Rolle). Der Handler liest zuerst den bestehenden Satz und schreibt danach ALLE Spalten zurueck — weggelassene Felder werden aus dem alten Stand wieder eingesetzt, ausdruecklich als null gesendete Felder werden geleert. Nach aussen wirkt das wie eine Teiluebernahme. Bereits erstellte Berechnungen rechnet die Aenderung NICHT neu. Unbekannte Kennung: 404 `regel_not_found`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"beschreibung":{"type":["string","null"]},"typ":{"type":"string","enum":["prozent_umsatz","prozent_db","gestaffelt","fixbetrag"]},"basis":{"type":"string","enum":["invoice","order"]},"prozentSatz":{"type":["number","null"],"minimum":0,"maximum":100},"staffeln":{"type":["array","null"],"items":{"type":"object","properties":{"vonBetrag":{"type":"number","minimum":0},"bisBetrag":{"type":["number","null"],"minimum":0},"satz":{"type":"number","minimum":0,"maximum":100}},"required":["vonBetrag","satz"]}},"fixbetrag":{"type":["number","null"],"minimum":0},"gueltigVon":{"type":"string","format":"date"},"gueltigBis":{"type":["string","null"],"format":"date"},"mitarbeiterIds":{"type":["array","null"],"items":{"type":"string","format":"uuid"}},"kundenIds":{"type":["array","null"],"items":{"type":"string","format":"uuid"}},"status":{"type":"string","enum":["active","inactive","expired"],"default":"active"}}},"example":{"name":"string","beschreibung":"string","typ":"prozent_umsatz","basis":"invoice","prozentSatz":0,"staffeln":[{"vonBetrag":0,"bisBetrag":0,"satz":0}],"fixbetrag":0,"gueltigVon":"2026-01-01","gueltigBis":"2026-01-01","mitarbeiterIds":["00000000-0000-4000-8000-000000000000"],"kundenIds":["00000000-0000-4000-8000-000000000000"],"status":"active"}}}}},"delete":{"responses":{"200":{"description":"Regel auf `expired` gesetzt — die Zeile bleibt stehen und taucht in der Liste weiter auf. Bestehende Berechnungen bleiben unberuehrt.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Regel nicht gefunden (`regel_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1BonusRegelnById","tags":["Bonus"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Provisionsregel soft-delete (status=expired)","description":"Setzt `status` auf `expired` (Manager-Rolle) — die Zeile bleibt in der Datenbank und in der Regelliste stehen, es gibt keine echte Loeschung und kein `deleted_at`. Wirkung: `POST /bonus/berechnen` findet die Regel nicht mehr, weil es nur aktive Regeln laedt. Bestehende Berechnungen bleiben unveraendert. Rueckgaengig macht man das ueber PUT mit `status: \"active\"`. Unbekannte Kennung: 404 `regel_not_found`."}},"/api/v1/bonus/berechnen":{"post":{"responses":{"200":{"description":"Berechnungen als `draft` gespeichert — eine je Mitarbeiter. WICHTIG: ist die Quelltabelle fuer die Bemessungsgrundlage nicht lesbar (z. B. weil sie in diesem Mandanten noch gar nicht existiert), rechnet der Server mit `basisBetrag: 0` weiter, statt abzubrechen. Es kommt dann 200 mit einem Bonus von 0 EUR. Eine 0 heisst hier also „kein Umsatz\" ODER „nicht messbar\" — der Unterschied steht nur im Serverprotokoll.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"regelId":{},"mitarbeiterId":{},"periodeJahr":{"type":"number"},"periodeMonat":{"type":"number"},"basisBetrag":{"type":"number"},"bonusBetrag":{"type":"number"},"berechnetAm":{},"status":{},"ausgezahltAm":{},"belegReferenzen":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeMonat","basisBetrag","bonusBetrag"],"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"periodeJahr":0,"periodeMonat":0,"basisBetrag":0,"bonusBetrag":0}],"total":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Regel nicht gefunden (`regel_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1BonusBerechnen","tags":["Bonus"],"parameters":[],"summary":"Berechnet Bonus pro Mitarbeiter für Periode und speichert als draft","description":"Laedt die Regel — nur wenn sie auf `active` steht, sonst 404 `regel_not_found` — und summiert je genanntem Mitarbeiter das Feld `total` der Rechnungen oder Auftraege (je nach `basis` der Regel), deren `owner_id` der Mitarbeiter ist und deren `date` in den angegebenen Monat faellt. Aus dieser Grundlage rechnet der `typ` der Regel den Bonus: Prozentsatz, hoechste passende Staffel oder Fixbetrag. Je Mitarbeiter entsteht eine NEUE Zeile in boni_berechnungen mit `status: \"draft\"` — ein zweiter Aufruf fuer dieselbe Periode legt Dubletten an, er ersetzt nichts. Laesst sich die Quelltabelle nicht lesen, gilt die Grundlage still als 0 und der Entwurf wird trotzdem geschrieben; ein Bonus von 0 ist deshalb nicht zwingend ein Nullumsatz. Manager-Rolle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"regelId":{"type":"string","format":"uuid"},"jahr":{"type":"integer","minimum":2000,"maximum":2100},"monat":{"type":"integer","minimum":1,"maximum":12},"mitarbeiterIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1}},"required":["regelId","jahr","monat","mitarbeiterIds"]},"example":{"regelId":"00000000-0000-4000-8000-000000000000","jahr":2000,"monat":1,"mitarbeiterIds":["00000000-0000-4000-8000-000000000000"]}}}}}},"/api/v1/bonus/berechnungen":{"get":{"responses":{"200":{"description":"Liste der Berechnungen — hier mit `pagination`, waehrend die Regelliste ein blosses `total` fuehrt. Zwei verschiedene Huellen in derselben Datei.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"regelId":{},"mitarbeiterId":{},"periodeJahr":{"type":"number"},"periodeMonat":{"type":"number"},"basisBetrag":{"type":"number"},"bonusBetrag":{"type":"number"},"berechnetAm":{},"status":{},"ausgezahltAm":{},"belegReferenzen":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeMonat","basisBetrag","bonusBetrag"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"periodeJahr":0,"periodeMonat":0,"basisBetrag":0,"bonusBetrag":0}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1BonusBerechnungen","tags":["Bonus"],"parameters":[{"in":"query","name":"mitarbeiterId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"jahr","schema":{"type":"integer","minimum":2000,"maximum":2100}},{"in":"query","name":"monat","schema":{"type":"integer","minimum":1,"maximum":12}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","approved","paid"]}},{"in":"query","name":"regelId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Listet Bonusberechnungen mit optionalen Filtern","description":"Liest boni_berechnungen (Manager-Rolle), sortiert nach Periode absteigend und darin nach Berechnungszeitpunkt. Die Parameter mitarbeiterId, jahr, monat, status und regelId werden mit UND verknuepft; `limit` liegt zwischen 1 und 200 (Vorgabe 50), `offset` blaettert. `pagination.total` zaehlt alle Treffer desselben Filters, nicht nur die gelieferte Seite."}},"/api/v1/bonus/berechnungen/bulk-approve":{"post":{"responses":{"200":{"description":"ACHTUNG, die Zahl in `message` ist die Zahl der GESENDETEN IDs, nicht der geaenderten. Umgestellt wird nur, was auf `draft` steht — eine unbekannte, bereits genehmigte oder bezahlte ID wird still uebergangen und taucht in `ids` trotzdem auf. Wer wissen will, was wirklich passiert ist, muss die Liste danach neu lesen.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"ids":{"type":"array","items":{"type":"string"}}},"required":["message","ids"],"additionalProperties":false},"example":{"message":"string","ids":["string"]}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1BonusBerechnungenBulk-approve","tags":["Bonus"],"parameters":[],"summary":"Mehrere Bonusberechnungen auf approved setzen","description":"Setzt in EINER Anweisung alle genannten Berechnungen auf `approved` (Manager-Rolle) — aber nur die, die auf `draft` stehen. Es gibt keine Vorabpruefung: unbekannte, bereits genehmigte oder bezahlte Kennungen werden still uebergangen, stehen aber trotzdem in der Antwort und in der Zahl der Meldung. Was wirklich umgestellt wurde, verraet erst ein erneutes Lesen der Liste.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1}},"required":["ids"]},"example":{"ids":["00000000-0000-4000-8000-000000000000"]}}}}}},"/api/v1/bonus/berechnungen/{id}/approve":{"post":{"responses":{"200":{"description":"Berechnung auf `approved`. Anders als die Sammelgenehmigung prueft dieser Aufruf den Vorzustand NICHT — eine bereits bezahlte Berechnung faellt hier auf `approved` zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"regelId":{},"mitarbeiterId":{},"periodeJahr":{"type":"number"},"periodeMonat":{"type":"number"},"basisBetrag":{"type":"number"},"bonusBetrag":{"type":"number"},"berechnetAm":{},"status":{},"ausgezahltAm":{},"belegReferenzen":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeMonat","basisBetrag","bonusBetrag"],"additionalProperties":false},"example":{"periodeJahr":0,"periodeMonat":0,"basisBetrag":0,"bonusBetrag":0}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Berechnung nicht gefunden (`berechnung_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1BonusBerechnungenByIdApprove","tags":["Bonus"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelne Bonusberechnung genehmigen","description":"Setzt eine Berechnung auf `approved` (Manager-Rolle) und gibt sie in ihrem neuen Stand zurueck. Der bisherige Stand wird nicht geprueft — anders als bei der Sammelgenehmigung faellt eine bereits bezahlte Berechnung hier auf `approved` zurueck, `ausgezahltAm` bleibt dabei stehen. Unbekannte Kennung: 404 `berechnung_not_found`."}},"/api/v1/bonus/berechnungen/{id}/mark-paid":{"post":{"responses":{"200":{"description":"Als bezahlt markiert, `ausgezahltAm` wird auf jetzt gesetzt. Auch hier ohne Vorzustandspruefung: eine Berechnung im Entwurf springt ohne Genehmigung direkt auf `paid`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"regelId":{},"mitarbeiterId":{},"periodeJahr":{"type":"number"},"periodeMonat":{"type":"number"},"basisBetrag":{"type":"number"},"bonusBetrag":{"type":"number"},"berechnetAm":{},"status":{},"ausgezahltAm":{},"belegReferenzen":{},"notizen":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeMonat","basisBetrag","bonusBetrag"],"additionalProperties":false},"example":{"periodeJahr":0,"periodeMonat":0,"basisBetrag":0,"bonusBetrag":0}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Admin-Rolle"},"404":{"description":"Berechnung nicht gefunden (`berechnung_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1BonusBerechnungenByIdMark-paid","tags":["Bonus"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Bonusberechnung als bezahlt markieren (nur admin)","description":"Setzt `status` auf `paid` und `ausgezahltAm` auf den Zeitpunkt des Aufrufs. Einzige Route dieser Datei, die die Admin-Rolle verlangt. Eine Genehmigung wird nicht vorausgesetzt: ein Entwurf springt ohne Zwischenschritt auf `paid`. Ausgeloest wird dabei keine Zahlung — die Markierung haelt nur fest, dass ausgezahlt wurde. Unbekannte Kennung: 404 `berechnung_not_found`."}},"/api/v1/bonus/stats":{"get":{"responses":{"200":{"description":"Kennzahlen ueber ALLE Perioden — es gibt keinen Zeitfilter. `summeDraft` ist damit die Summe aller je erzeugten, nie genehmigten Entwuerfe, nicht die des laufenden Monats. `meta.source` steht fest auf `db`.","content":{"application/json":{"schema":{"type":"object","properties":{"aktiveRegeln":{"type":"number"},"berechnungenDraft":{"type":"number"},"berechnungenApproved":{"type":"number"},"berechnungenPaid":{"type":"number"},"summeDraft":{"type":"number"},"summeApproved":{"type":"number"},"summePaid":{"type":"number"},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string","const":"db"}},"required":["source"],"additionalProperties":false}},"required":["aktiveRegeln","berechnungenDraft","berechnungenApproved","berechnungenPaid","summeDraft","summeApproved","summePaid","meta"],"additionalProperties":false},"example":{"aktiveRegeln":0,"berechnungenDraft":0,"berechnungenApproved":0,"berechnungenPaid":0,"summeDraft":0,"summeApproved":0,"summePaid":0,"meta":{"source":"db"}}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1BonusStats","tags":["Bonus"],"parameters":[],"summary":"KPIs: aktive Regeln, offene/genehmigte/ausgezahlte Bonusberechnungen","description":"Zaehlt die aktiven Regeln sowie die Berechnungen je Stand und summiert zusaetzlich die Bonusbetraege je Stand (Manager-Rolle). Die Zahlen laufen ueber den gesamten Bestand des Mandanten — es gibt keinen Zeitraum-Parameter. `meta.source` ist stets `db`; es gibt keinen Ersatzwert-Pfad."}},"/api/v1/mrp/runs":{"post":{"responses":{"201":{"description":"MRP-Lauf gestartet und bereits abgeschlossen (`status: completed`)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"runTyp":{"type":"string","enum":["manuell","cron","forecast"]},"status":{"type":"string","enum":["pending","running","completed","failed"]},"gestartetAm":{"type":"string"},"abgeschlossenAm":{"type":["string","null"]},"vorschlaegeAnzahl":{"type":"number"},"parameter":{"type":"object","additionalProperties":{}}},"required":["id","runTyp","status","gestartetAm","abgeschlossenAm","vorschlaegeAnzahl","parameter"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","runTyp":"manuell","status":"pending","gestartetAm":"string","abgeschlossenAm":"string","vorschlaegeAnzahl":0,"parameter":{}}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Datenbankfehler — der Lauf bleibt auf `running`. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1MrpRuns","tags":["MRP"],"parameters":[],"description":"Startet einen neuen MRP-Lauf und generiert Bestellvorschläge. ZWEI Einschränkungen, die man der Antwort nicht ansieht: (1) Fehlt die Tabelle `inventory_articles` oder hat sie eine andere Struktur, wird die Ausnahme verschluckt und mit einer LEEREN Artikelliste weitergerechnet — der Lauf endet dann mit 201 und `vorschlaegeAnzahl: 0`, was wie „kein Bedarf\" aussieht, aber „nichts geprüft\" heißt. Dasselbe gilt für die Teilabfragen Verbrauchsprognose und Stücklistenbedarf, die im Fehlerfall still auf 0 fallen. (2) Der Lauf wird VOR der Berechnung als `running` gespeichert; bricht die Berechnung ab, antwortet der Aufruf 503 und der Datensatz bleibt dauerhaft auf `running` stehen — es gibt keinen Pfad, der ihn auf `failed` setzt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"runTyp":{"type":"string","enum":["manuell","cron","forecast"],"default":"manuell"},"parameter":{"type":"object","additionalProperties":{},"default":{}}}},"example":{"runTyp":"manuell","parameter":{}}}}},"summary":"Startet einen neuen MRP-Lauf und generiert Bestellvorschläge","x-nemix-summary-source":"description:first-sentence"},"get":{"responses":{"200":{"description":"Lauf-Liste mit Blätter-Angaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"runTyp":{"type":"string","enum":["manuell","cron","forecast"]},"status":{"type":"string","enum":["pending","running","completed","failed"]},"gestartetAm":{"type":"string"},"abgeschlossenAm":{"type":["string","null"]},"vorschlaegeAnzahl":{"type":"number"},"parameter":{"type":"object","additionalProperties":{}}},"required":["id","runTyp","status","gestartetAm","abgeschlossenAm","vorschlaegeAnzahl","parameter"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","runTyp":"manuell","status":"pending","gestartetAm":"string","abgeschlossenAm":"string","vorschlaegeAnzahl":0,"parameter":{}}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbankfehler. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1MrpRuns","tags":["MRP"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["pending","running","completed","failed"]}}],"summary":"Liste aller MRP-Läufe, neueste zuerst","description":"Liest `mrp_runs` im Mandanten-Schema, sortiert nach `gestartet_am` absteigend. `status` filtert auf pending, running, completed oder failed; `limit` (Vorgabe 50, höchstens 200) und `offset` blättern, und `pagination.total` zählt alle Treffer des Filters, nicht nur die ausgelieferte Seite. Jeder Lauf bringt seine Vorschlagszahl und die verwendeten Parameter mit; die Vorschläge selbst liefert GET /mrp/runs/:id. Die MRP-Tabellen werden beim ersten Aufruf angelegt, ein Mandant ohne Lauf bekommt deshalb eine leere Liste."}},"/api/v1/mrp/runs/{id}":{"get":{"responses":{"200":{"description":"Lauf mit Vorschlägen","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"runTyp":{"type":"string","enum":["manuell","cron","forecast"]},"status":{"type":"string","enum":["pending","running","completed","failed"]},"gestartetAm":{"type":"string"},"abgeschlossenAm":{"type":["string","null"]},"vorschlaegeAnzahl":{"type":"number"},"parameter":{"type":"object","additionalProperties":{}},"vorschlaege":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"runId":{"type":"string","format":"uuid"},"artikelId":{"type":["string","null"],"format":"uuid"},"artikelBezeichnung":{"type":"string"},"bestandAktuell":{"type":"number"},"mindestbestand":{"type":"number"},"forecast30tage":{"type":"number"},"bedarfAusStueckliste":{"type":"number"},"vorschlagMenge":{"type":"number"},"vorschlagLieferantId":{"type":["string","null"],"format":"uuid"},"vorschlagTermin":{"type":["string","null"]},"status":{"type":"string","enum":["open","approved","rejected","converted","expired"]},"bestellungId":{"type":["string","null"],"format":"uuid"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","runId","artikelId","artikelBezeichnung","bestandAktuell","mindestbestand","forecast30tage","bedarfAusStueckliste","vorschlagMenge","vorschlagLieferantId","vorschlagTermin","status","bestellungId","notizen","createdAt"],"additionalProperties":false}}},"required":["id","runTyp","status","gestartetAm","abgeschlossenAm","vorschlaegeAnzahl","parameter","vorschlaege"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","runTyp":"manuell","status":"pending","gestartetAm":"string","abgeschlossenAm":"string","vorschlaegeAnzahl":0,"parameter":{},"vorschlaege":[{"id":"00000000-0000-4000-8000-000000000000","runId":"00000000-0000-4000-8000-000000000000","artikelId":"00000000-0000-4000-8000-000000000000","artikelBezeichnung":"string","bestandAktuell":0,"mindestbestand":0,"forecast30tage":0,"bedarfAusStueckliste":0,"vorschlagMenge":0,"vorschlagLieferantId":"00000000-0000-4000-8000-000000000000","vorschlagTermin":"string","status":"open","bestellungId":"00000000-0000-4000-8000-000000000000","notizen":"string","createdAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Lauf nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"run_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbankfehler. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain). Auch eine unlesbare `:id` landet hier: sie wird nicht als UUID geprüft, sondern läuft in den SQL-Fehler.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1MrpRunsById","tags":["MRP"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Detail eines MRP-Laufs inkl. aller Vorschläge. Ungeblättert — die Vorschläge eines Laufs kommen vollständig mit.","summary":"Detail eines MRP-Laufs inkl. aller Vorschläge","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/mrp/vorschlaege":{"get":{"responses":{"200":{"description":"Vorschlag-Liste mit Blätter-Angaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"runId":{"type":"string","format":"uuid"},"artikelId":{"type":["string","null"],"format":"uuid"},"artikelBezeichnung":{"type":"string"},"bestandAktuell":{"type":"number"},"mindestbestand":{"type":"number"},"forecast30tage":{"type":"number"},"bedarfAusStueckliste":{"type":"number"},"vorschlagMenge":{"type":"number"},"vorschlagLieferantId":{"type":["string","null"],"format":"uuid"},"vorschlagTermin":{"type":["string","null"]},"status":{"type":"string","enum":["open","approved","rejected","converted","expired"]},"bestellungId":{"type":["string","null"],"format":"uuid"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","runId","artikelId","artikelBezeichnung","bestandAktuell","mindestbestand","forecast30tage","bedarfAusStueckliste","vorschlagMenge","vorschlagLieferantId","vorschlagTermin","status","bestellungId","notizen","createdAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","runId":"00000000-0000-4000-8000-000000000000","artikelId":"00000000-0000-4000-8000-000000000000","artikelBezeichnung":"string","bestandAktuell":0,"mindestbestand":0,"forecast30tage":0,"bedarfAusStueckliste":0,"vorschlagMenge":0,"vorschlagLieferantId":"00000000-0000-4000-8000-000000000000","vorschlagTermin":"string","status":"open","bestellungId":"00000000-0000-4000-8000-000000000000","notizen":"string","createdAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbankfehler. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1MrpVorschlaege","tags":["MRP"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":100}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["open","approved","rejected","converted","expired"]}},{"in":"query","name":"runId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"lieferantId","schema":{"type":"string","format":"uuid"}}],"summary":"Liste der MRP-Vorschläge mit Filtern (Status, Lauf, Lieferant)","description":"Liest `mrp_vorschlaege` laufübergreifend, sortiert nach Anlagezeitpunkt absteigend. Die Filter `status` (open, approved, rejected, converted, expired), `runId` und `lieferantId` wirken zusammen — ohne Angabe kommt alles. `limit` (Vorgabe 100, höchstens 200) und `offset` blättern, `pagination.total` zählt alle Treffer des Filters. Jede Zeile stellt Ist-Bestand, Mindestbestand, 30-Tage-Prognose und Stücklistenbedarf der vorgeschlagenen Menge gegenüber. Das Lesen verlangt keine besondere Rolle; Genehmigen, Ablehnen und Umwandeln in eine Bestellung dagegen mindestens `manager`."}},"/api/v1/mrp/vorschlaege/bulk-convert":{"post":{"responses":{"200":{"description":"Bulk-Konvertierung durchlaufen — auch bei 0 erfolgreichen Zeilen","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bestellungId":{"type":["string","null"],"format":"uuid"},"error":{"type":"string"}},"required":["id","bestellungId"],"additionalProperties":false}},"converted":{"type":"integer"}},"required":["results","converted"],"additionalProperties":false},"example":{"results":[{"id":"00000000-0000-4000-8000-000000000000","bestellungId":"00000000-0000-4000-8000-000000000000","error":"string"}],"converted":0}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Einkaufsmodul nicht eingerichtet (`purchase_orders_table_missing`) oder Datenbankfehler (`database_unavailable`). Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"purchase_orders_table_missing"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"postApiV1MrpVorschlaegeBulk-convert","tags":["MRP"],"parameters":[],"description":"Konvertiert mehrere Vorschläge gleichzeitig in Einkaufsbestellungen. TEILERFOLG IST DER NORMALFALL: jeder Vorschlag wird einzeln versucht, und der Aufruf antwortet 200 auch dann, wenn KEIN einziger durchging — maßgeblich ist `converted` und das `error`-Feld je Zeile, nicht der Statuscode. Hat ein Vorschlag keinen Lieferanten, wird die Bestellung trotzdem angelegt und ihr `supplier_id` mit einer FRISCH ERZEUGTEN Zufalls-UUID gefüllt (`COALESCE($2, gen_random_uuid())`) — sie zeigt auf keinen existierenden Lieferanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1}},"required":["ids"]},"example":{"ids":["00000000-0000-4000-8000-000000000000"]}}}},"summary":"Konvertiert mehrere Vorschläge gleichzeitig in Einkaufsbestellungen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/mrp/vorschlaege/{id}":{"patch":{"responses":{"200":{"description":"Vorschlag aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"runId":{"type":"string","format":"uuid"},"artikelId":{"type":["string","null"],"format":"uuid"},"artikelBezeichnung":{"type":"string"},"bestandAktuell":{"type":"number"},"mindestbestand":{"type":"number"},"forecast30tage":{"type":"number"},"bedarfAusStueckliste":{"type":"number"},"vorschlagMenge":{"type":"number"},"vorschlagLieferantId":{"type":["string","null"],"format":"uuid"},"vorschlagTermin":{"type":["string","null"]},"status":{"type":"string","enum":["open","approved","rejected","converted","expired"]},"bestellungId":{"type":["string","null"],"format":"uuid"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","runId","artikelId","artikelBezeichnung","bestandAktuell","mindestbestand","forecast30tage","bedarfAusStueckliste","vorschlagMenge","vorschlagLieferantId","vorschlagTermin","status","bestellungId","notizen","createdAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","runId":"00000000-0000-4000-8000-000000000000","artikelId":"00000000-0000-4000-8000-000000000000","artikelBezeichnung":"string","bestandAktuell":0,"mindestbestand":0,"forecast30tage":0,"bedarfAusStueckliste":0,"vorschlagMenge":0,"vorschlagLieferantId":"00000000-0000-4000-8000-000000000000","vorschlagTermin":"string","status":"open","bestellungId":"00000000-0000-4000-8000-000000000000","notizen":"string","createdAt":"string"}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Vorschlag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"vorschlag_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbankfehler. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"patchApiV1MrpVorschlaegeById","tags":["MRP"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Bearbeitet einen MRP-Vorschlag (Menge, Lieferant, Termin, Notizen). Weggelassene Felder bleiben stehen — der Handler liest den Datensatz vorher und schreibt die alten Werte zurück. Der `status` ist hier NICHT änderbar; dafür gibt es approve/reject/convert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"vorschlagMenge":{"type":"number","minimum":0},"vorschlagLieferantId":{"type":["string","null"],"format":"uuid"},"vorschlagTermin":{"type":["string","null"],"format":"date"},"notizen":{"type":["string","null"]}}},"example":{"vorschlagMenge":0,"vorschlagLieferantId":"00000000-0000-4000-8000-000000000000","vorschlagTermin":"2026-01-01","notizen":"string"}}}},"summary":"Bearbeitet einen MRP-Vorschlag (Menge, Lieferant, Termin, Notizen)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/mrp/vorschlaege/{id}/approve":{"post":{"responses":{"200":{"description":"Vorschlag genehmigt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"runId":{"type":"string","format":"uuid"},"artikelId":{"type":["string","null"],"format":"uuid"},"artikelBezeichnung":{"type":"string"},"bestandAktuell":{"type":"number"},"mindestbestand":{"type":"number"},"forecast30tage":{"type":"number"},"bedarfAusStueckliste":{"type":"number"},"vorschlagMenge":{"type":"number"},"vorschlagLieferantId":{"type":["string","null"],"format":"uuid"},"vorschlagTermin":{"type":["string","null"]},"status":{"type":"string","enum":["open","approved","rejected","converted","expired"]},"bestellungId":{"type":["string","null"],"format":"uuid"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","runId","artikelId","artikelBezeichnung","bestandAktuell","mindestbestand","forecast30tage","bedarfAusStueckliste","vorschlagMenge","vorschlagLieferantId","vorschlagTermin","status","bestellungId","notizen","createdAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","runId":"00000000-0000-4000-8000-000000000000","artikelId":"00000000-0000-4000-8000-000000000000","artikelBezeichnung":"string","bestandAktuell":0,"mindestbestand":0,"forecast30tage":0,"bedarfAusStueckliste":0,"vorschlagMenge":0,"vorschlagLieferantId":"00000000-0000-4000-8000-000000000000","vorschlagTermin":"string","status":"open","bestellungId":"00000000-0000-4000-8000-000000000000","notizen":"string","createdAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Vorschlag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"vorschlag_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbankfehler. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1MrpVorschlaegeByIdApprove","tags":["MRP"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Genehmigt einen MRP-Vorschlag (`status: approved`). Setzt den Status bedingungslos — auch ein bereits konvertierter oder abgelehnter Vorschlag wird zurück auf `approved` geschrieben.","summary":"Genehmigt einen MRP-Vorschlag (`status: approved`)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/mrp/vorschlaege/{id}/reject":{"post":{"responses":{"200":{"description":"Vorschlag abgelehnt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"runId":{"type":"string","format":"uuid"},"artikelId":{"type":["string","null"],"format":"uuid"},"artikelBezeichnung":{"type":"string"},"bestandAktuell":{"type":"number"},"mindestbestand":{"type":"number"},"forecast30tage":{"type":"number"},"bedarfAusStueckliste":{"type":"number"},"vorschlagMenge":{"type":"number"},"vorschlagLieferantId":{"type":["string","null"],"format":"uuid"},"vorschlagTermin":{"type":["string","null"]},"status":{"type":"string","enum":["open","approved","rejected","converted","expired"]},"bestellungId":{"type":["string","null"],"format":"uuid"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","runId","artikelId","artikelBezeichnung","bestandAktuell","mindestbestand","forecast30tage","bedarfAusStueckliste","vorschlagMenge","vorschlagLieferantId","vorschlagTermin","status","bestellungId","notizen","createdAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","runId":"00000000-0000-4000-8000-000000000000","artikelId":"00000000-0000-4000-8000-000000000000","artikelBezeichnung":"string","bestandAktuell":0,"mindestbestand":0,"forecast30tage":0,"bedarfAusStueckliste":0,"vorschlagMenge":0,"vorschlagLieferantId":"00000000-0000-4000-8000-000000000000","vorschlagTermin":"string","status":"open","bestellungId":"00000000-0000-4000-8000-000000000000","notizen":"string","createdAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Vorschlag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"vorschlag_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbankfehler. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1MrpVorschlaegeByIdReject","tags":["MRP"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Lehnt einen MRP-Vorschlag ab (`status: rejected`). Setzt den Status bedingungslos — auch ein bereits konvertierter Vorschlag wird auf `rejected` geschrieben, die daraus entstandene Bestellung bleibt bestehen.","summary":"Lehnt einen MRP-Vorschlag ab (`status: rejected`)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/mrp/vorschlaege/{id}/convert":{"post":{"responses":{"200":{"description":"Bestellung erstellt, Vorschlag auf `converted` gesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"vorschlag":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"runId":{"type":"string","format":"uuid"},"artikelId":{"type":["string","null"],"format":"uuid"},"artikelBezeichnung":{"type":"string"},"bestandAktuell":{"type":"number"},"mindestbestand":{"type":"number"},"forecast30tage":{"type":"number"},"bedarfAusStueckliste":{"type":"number"},"vorschlagMenge":{"type":"number"},"vorschlagLieferantId":{"type":["string","null"],"format":"uuid"},"vorschlagTermin":{"type":["string","null"]},"status":{"type":"string","enum":["open","approved","rejected","converted","expired"]},"bestellungId":{"type":["string","null"],"format":"uuid"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","runId","artikelId","artikelBezeichnung","bestandAktuell","mindestbestand","forecast30tage","bedarfAusStueckliste","vorschlagMenge","vorschlagLieferantId","vorschlagTermin","status","bestellungId","notizen","createdAt"],"additionalProperties":false},"bestellungId":{"type":"string","format":"uuid"},"orderNumber":{"type":"string"}},"required":["vorschlag","bestellungId","orderNumber"],"additionalProperties":false},"example":{"vorschlag":{"id":"00000000-0000-4000-8000-000000000000","runId":"00000000-0000-4000-8000-000000000000","artikelId":"00000000-0000-4000-8000-000000000000","artikelBezeichnung":"string","bestandAktuell":0,"mindestbestand":0,"forecast30tage":0,"bedarfAusStueckliste":0,"vorschlagMenge":0,"vorschlagLieferantId":"00000000-0000-4000-8000-000000000000","vorschlagTermin":"string","status":"open","bestellungId":"00000000-0000-4000-8000-000000000000","notizen":"string","createdAt":"string"},"bestellungId":"00000000-0000-4000-8000-000000000000","orderNumber":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Vorschlag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"vorschlag_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Einkaufsmodul nicht eingerichtet (`purchase_orders_table_missing`) oder Datenbankfehler (`database_unavailable`). Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"purchase_orders_table_missing"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}]}}}}},"operationId":"postApiV1MrpVorschlaegeByIdConvert","tags":["MRP"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Wandelt einen Bedarfsvorschlag in eine Einkaufsbestellung","description":"Konvertiert einen MRP-Vorschlag in eine Einkaufsbestellung (Status `draft`, Preise auf 0). Antwortet 200, nicht 201 — obwohl ein Datensatz entsteht. Zwei Dinge, die man der Antwort nicht ansieht: der Status des Vorschlags wird NICHT geprüft (ein bereits konvertierter wird erneut konvertiert und erzeugt eine zweite Bestellung), und ohne Lieferanten am Vorschlag bekommt die Bestellung eine FRISCH ERZEUGTE Zufalls-UUID als `supplier_id` (`COALESCE($2, gen_random_uuid())`), die auf keinen existierenden Lieferanten zeigt."}},"/api/v1/mrp/stats":{"get":{"responses":{"200":{"description":"MRP-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"offeneVorschlaege":{"type":"integer"},"genehmigteVorschlaege":{"type":"integer"},"konvertierteVorschlaege":{"type":"integer"},"abgelehnte":{"type":"integer"},"letzterLauf":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","running","completed","failed"]},"gestartetAm":{"type":"string"},"abgeschlossenAm":{"type":"null"},"vorschlaegeAnzahl":{"type":"number"},"parameter":{"type":"object","additionalProperties":{}}},"required":["id","status","gestartetAm","abgeschlossenAm","vorschlaegeAnzahl","parameter"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["offeneVorschlaege","genehmigteVorschlaege","konvertierteVorschlaege","abgelehnte","letzterLauf","meta"],"additionalProperties":false},"example":{"offeneVorschlaege":0,"genehmigteVorschlaege":0,"konvertierteVorschlaege":0,"abgelehnte":0,"letzterLauf":{"id":"00000000-0000-4000-8000-000000000000","status":"pending","gestartetAm":"string","abgeschlossenAm":null,"vorschlaegeAnzahl":0,"parameter":{}},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbankfehler. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1MrpStats","tags":["MRP"],"parameters":[],"summary":"Kennzahlen zur Bedarfsplanung und der zuletzt gestartete Lauf","description":"MRP-Kennzahlen: offene, genehmigte, konvertierte und abgelehnte Vorschläge sowie der zuletzt gestartete Lauf. `letzterLauf` ist eine VERKÜRZTE Form des Laufs: die Abfrage liest nur vier Spalten, deshalb fehlt `runTyp` in der Antwort und `abgeschlossenAm` ist immer `null`, `parameter` immer `{}` — auch bei einem abgeschlossenen Lauf."}},"/api/v1/lieferanten-bewertung/kriterien":{"get":{"responses":{"200":{"description":"Kriterien-Liste — nur `data`, weder Paginierung noch meta","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"name":{},"kategorie":{},"gewichtung":{"type":"number"},"skalaMin":{"type":"number"},"skalaMax":{"type":"number"},"beschreibung":{},"aktiv":{},"createdAt":{},"updatedAt":{}},"required":["gewichtung","skalaMin","skalaMax"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"gewichtung":0,"skalaMin":0,"skalaMax":0}]}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1Lieferanten-bewertungKriterien","tags":["Lieferanten"],"parameters":[],"description":"Listet alle Bewertungskriterien. Auto-Seed wenn leer.","summary":"Listet alle Bewertungskriterien","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Kriterium angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"kategorie":{},"gewichtung":{"type":"number"},"skalaMin":{"type":"number"},"skalaMax":{"type":"number"},"beschreibung":{},"aktiv":{},"createdAt":{},"updatedAt":{}},"required":["gewichtung","skalaMin","skalaMax"],"additionalProperties":false},"example":{"gewichtung":0,"skalaMin":0,"skalaMax":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1Lieferanten-bewertungKriterien","tags":["Lieferanten"],"parameters":[],"description":"Legt ein neues Bewertungskriterium an. Die `gewichtung` (0–100) geht als Gewicht in die Score-Berechnung jeder Bewertung ein; ob die Gewichte aller Kriterien zusammen 100 ergeben, prüft der Server NICHT — der Gesamtscore ist ein gewichteter Mittelwert und bleibt auch bei anderer Summe gültig. Ohne Angabe läuft die Skala von 1 bis 5 und das Kriterium ist aktiv. Nur für Rolle `manager` oder höher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"kategorie":{"type":"string","enum":["qualitaet","liefertreue","preis","service","nachhaltigkeit"]},"gewichtung":{"type":"number","minimum":0,"maximum":100},"skalaMin":{"type":"integer","default":1},"skalaMax":{"type":"integer","default":5},"beschreibung":{"type":["string","null"]},"aktiv":{"type":"boolean","default":true}},"required":["name","kategorie","gewichtung"]},"example":{"name":"string","kategorie":"qualitaet","gewichtung":0,"skalaMin":0,"skalaMax":0,"beschreibung":"string","aktiv":true}}}},"summary":"Legt ein neues Bewertungskriterium an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/lieferanten-bewertung/kriterien/{id}":{"put":{"responses":{"200":{"description":"Kriterium aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"kategorie":{},"gewichtung":{"type":"number"},"skalaMin":{"type":"number"},"skalaMax":{"type":"number"},"beschreibung":{},"aktiv":{},"createdAt":{},"updatedAt":{}},"required":["gewichtung","skalaMin","skalaMax"],"additionalProperties":false},"example":{"gewichtung":0,"skalaMin":0,"skalaMax":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Kriterium nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1Lieferanten-bewertungKriterienById","tags":["Lieferanten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aktualisiert ein Bewertungskriterium. Der Aufruf schreibt alle Spalten neu und übernimmt nicht mitgeschickte Felder aus dem bestehenden Datensatz — ein Teil-Rumpf führt also zu keinem Datenverlust. Eine geänderte `gewichtung` wirkt erst auf künftige Bewertungen; bereits gespeicherte Scores werden nicht neu gerechnet. Eine unbekannte Kennung ergibt 404. Nur für Rolle `manager` oder höher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"kategorie":{"type":"string","enum":["qualitaet","liefertreue","preis","service","nachhaltigkeit"]},"gewichtung":{"type":"number","minimum":0,"maximum":100},"skalaMin":{"type":"integer","default":1},"skalaMax":{"type":"integer","default":5},"beschreibung":{"type":["string","null"]},"aktiv":{"type":"boolean","default":true}}},"example":{"name":"string","kategorie":"qualitaet","gewichtung":0,"skalaMin":0,"skalaMax":0,"beschreibung":"string","aktiv":true}}}},"summary":"Aktualisiert ein Bewertungskriterium","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Kriterium deaktiviert (aktiv=false) — nicht geloescht, nur stillgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Kriterium nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1Lieferanten-bewertungKriterienById","tags":["Lieferanten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Deaktiviert ein Bewertungskriterium (aktiv=false). Die Zeile bleibt erhalten — gesetzt werden nur `aktiv` und `updated_at`, gelöscht wird nichts. Bereits erfasste Bewertungen behalten ihren Score; in neuen Bewertungen zählt das Kriterium nicht mehr mit, weil die Score-Berechnung ausschließlich aktive Kriterien gewichtet. Eine unbekannte Kennung ergibt 404. Nur für Rolle `manager` oder höher.","summary":"Deaktiviert ein Bewertungskriterium (aktiv=false)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/lieferanten-bewertung/bewertungen":{"get":{"responses":{"200":{"description":"Bewertungs-Liste — mit `pagination`, aber ohne `meta`","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"lieferantId":{},"periodeJahr":{"type":["number","null"]},"periodeQuartal":{"type":["number","null"]},"bewerterId":{},"bewertetAm":{},"bewertungen":{},"scoreGesamt":{"type":["number","null"]},"klassifizierung":{},"kommentar":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeQuartal","scoreGesamt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"periodeJahr":0,"periodeQuartal":0,"scoreGesamt":0}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1Lieferanten-bewertungBewertungen","tags":["Lieferanten"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"lieferantId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"periodeJahr","schema":{"type":"integer"}},{"in":"query","name":"klassifizierung","schema":{"type":"string","enum":["A","B","C"]}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","finalized"]}}],"description":"Listet Lieferantenbewertungen mit optionalen Filtern. Gelesen wird `lieferanten_bewertungen` des Mandanten, nach Bewertungsdatum absteigend. Filterbar nach `lieferantId`, `periodeJahr`, `klassifizierung` (A/B/C) und `status`; `limit` liegt zwischen 1 und 200 (Vorgabe 50), `offset` beginnt bei 0. `pagination.total` zählt mit denselben Bedingungen wie die Liste. Entwürfe und finalisierte Bewertungen kommen hier gemeinsam — anders als in der Statistik.","summary":"Listet Lieferantenbewertungen mit optionalen Filtern","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Bewertung angelegt — `scoreGesamt` und `klassifizierung` rechnet der Server, gesendete Werte werden nicht uebernommen","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"lieferantId":{},"periodeJahr":{"type":["number","null"]},"periodeQuartal":{"type":["number","null"]},"bewerterId":{},"bewertetAm":{},"bewertungen":{},"scoreGesamt":{"type":["number","null"]},"klassifizierung":{},"kommentar":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeQuartal","scoreGesamt"],"additionalProperties":false},"example":{"periodeJahr":0,"periodeQuartal":0,"scoreGesamt":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1Lieferanten-bewertungBewertungen","tags":["Lieferanten"],"parameters":[],"description":"Legt eine neue Lieferantenbewertung an. Server berechnet score_gesamt und Klassifizierung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lieferantId":{"type":"string","format":"uuid"},"periodeJahr":{"type":"integer","minimum":2000,"maximum":2100},"periodeQuartal":{"type":["integer","null"],"minimum":1,"maximum":4},"bewerter_id":{"type":"string","format":"uuid"},"bewertungen":{"type":"array","items":{"type":"object","properties":{"kriteriumId":{"type":"string","format":"uuid"},"score":{"type":"number","minimum":0,"maximum":10},"kommentar":{"type":["string","null"]}},"required":["kriteriumId","score"]},"minItems":1},"kommentar":{"type":["string","null"]},"status":{"type":"string","enum":["draft","finalized"],"default":"draft"}},"required":["lieferantId","periodeJahr","bewertungen"]},"example":{"lieferantId":"00000000-0000-4000-8000-000000000000","periodeJahr":2000,"periodeQuartal":1,"bewerter_id":"00000000-0000-4000-8000-000000000000","bewertungen":[{"kriteriumId":"00000000-0000-4000-8000-000000000000","score":0,"kommentar":"string"}],"kommentar":"string","status":"draft"}}}},"summary":"Legt eine neue Lieferantenbewertung an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/lieferanten-bewertung/bewertungen/{id}":{"get":{"responses":{"200":{"description":"Bewertungs-Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"lieferantId":{},"periodeJahr":{"type":["number","null"]},"periodeQuartal":{"type":["number","null"]},"bewerterId":{},"bewertetAm":{},"bewertungen":{},"scoreGesamt":{"type":["number","null"]},"klassifizierung":{},"kommentar":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeQuartal","scoreGesamt"],"additionalProperties":false},"example":{"periodeJahr":0,"periodeQuartal":0,"scoreGesamt":0}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Bewertung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1Lieferanten-bewertungBewertungenById","tags":["Lieferanten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liefert eine einzelne Bewertung. Der Datensatz steht flach in der Antwort, ohne `data`-Umschlag, und enthält im Feld `bewertungen` die Einzelbewertungen je Kriterium so, wie sie beim Anlegen gespeichert wurden. Die Kriterien selbst — Name, Kategorie, Gewichtung — kommen nicht mit; die liefert `GET /api/v1/lieferanten-bewertung/kriterien`. Eine unbekannte Kennung ergibt 404.","summary":"Liefert eine einzelne Bewertung","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Bewertung aktualisiert — `scoreGesamt` rechnet der Server neu","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"lieferantId":{},"periodeJahr":{"type":["number","null"]},"periodeQuartal":{"type":["number","null"]},"bewerterId":{},"bewertetAm":{},"bewertungen":{},"scoreGesamt":{"type":["number","null"]},"klassifizierung":{},"kommentar":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeQuartal","scoreGesamt"],"additionalProperties":false},"example":{"periodeJahr":0,"periodeQuartal":0,"scoreGesamt":0}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Bewertung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1Lieferanten-bewertungBewertungenById","tags":["Lieferanten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aktualisiert eine Bewertung. ACHTUNG: der Handler prueft den Status NICHT — auch eine finalisierte Bewertung laesst sich hier aendern (im Gegensatz zum Loeschen, das mit 409 abbricht).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lieferantId":{"type":"string","format":"uuid"},"periodeJahr":{"type":"integer","minimum":2000,"maximum":2100},"periodeQuartal":{"type":["integer","null"],"minimum":1,"maximum":4},"bewerter_id":{"type":"string","format":"uuid"},"bewertungen":{"type":"array","items":{"type":"object","properties":{"kriteriumId":{"type":"string","format":"uuid"},"score":{"type":"number","minimum":0,"maximum":10},"kommentar":{"type":["string","null"]}},"required":["kriteriumId","score"]},"minItems":1},"kommentar":{"type":["string","null"]},"status":{"type":"string","enum":["draft","finalized"],"default":"draft"}}},"example":{"lieferantId":"00000000-0000-4000-8000-000000000000","periodeJahr":2000,"periodeQuartal":1,"bewerter_id":"00000000-0000-4000-8000-000000000000","bewertungen":[{"kriteriumId":"00000000-0000-4000-8000-000000000000","score":0,"kommentar":"string"}],"kommentar":"string","status":"draft"}}}},"summary":"Aktualisiert eine Bewertung","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Bewertung geloescht — hard delete, die Zeile ist danach weg","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Bewertung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Bewertung ist finalisiert und wird nicht geloescht (`cannot_delete_finalized`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1Lieferanten-bewertungBewertungenById","tags":["Lieferanten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Löscht eine Bewertung (hard delete, nur draft erlaubt). Die Zeile wird endgültig aus `lieferanten_bewertungen` entfernt — es gibt kein `deleted_at` und keinen Weg zurück. Eine finalisierte Bewertung lehnt der Aufruf mit 409 `cannot_delete_finalized` ab, eine unbekannte mit 404. Zurück kommt nur eine Quittung mit Meldungstext, kein Datensatz. Nur für Rolle `manager` oder höher.","summary":"Löscht eine Bewertung (hard delete, nur draft erlaubt)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/lieferanten-bewertung/bewertungen/{id}/finalize":{"post":{"responses":{"200":{"description":"Bewertung finalisiert — ab jetzt zaehlt sie in die Statistik","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"lieferantId":{},"periodeJahr":{"type":["number","null"]},"periodeQuartal":{"type":["number","null"]},"bewerterId":{},"bewertetAm":{},"bewertungen":{},"scoreGesamt":{"type":["number","null"]},"klassifizierung":{},"kommentar":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeQuartal","scoreGesamt"],"additionalProperties":false},"example":{"periodeJahr":0,"periodeQuartal":0,"scoreGesamt":0}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Bewertung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"409":{"description":"Bereits finalisiert (`already_finalized`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1Lieferanten-bewertungBewertungenByIdFinalize","tags":["Lieferanten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt Bewertung auf status=finalized. Erst damit zählt sie in `GET /api/v1/lieferanten-bewertung/stats`, die ausschließlich finalisierte Bewertungen auswertet, und sie lässt sich danach nicht mehr löschen. Score und Klassifizierung werden dabei NICHT neu berechnet — sie bleiben so, wie sie beim Anlegen oder letzten Ändern entstanden sind. Eine bereits finalisierte Bewertung ergibt 409 `already_finalized`, eine unbekannte 404.","summary":"Setzt Bewertung auf status=finalized","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/lieferanten-bewertung/lieferant/{lieferantId}/history":{"get":{"responses":{"200":{"description":"Bewertungsverlauf — vierter Umschlag: `lieferantId` neben `data`","content":{"application/json":{"schema":{"type":"object","properties":{"lieferantId":{"type":"string"},"data":{"type":"array","items":{"type":"object","properties":{"id":{},"lieferantId":{},"periodeJahr":{"type":["number","null"]},"periodeQuartal":{"type":["number","null"]},"bewerterId":{},"bewertetAm":{},"bewertungen":{},"scoreGesamt":{"type":["number","null"]},"klassifizierung":{},"kommentar":{},"status":{},"createdAt":{},"updatedAt":{}},"required":["periodeJahr","periodeQuartal","scoreGesamt"],"additionalProperties":false}}},"required":["lieferantId","data"],"additionalProperties":false},"example":{"lieferantId":"string","data":[{"periodeJahr":0,"periodeQuartal":0,"scoreGesamt":0}]}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1Lieferanten-bewertungLieferantByLieferantIdHistory","tags":["Lieferanten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"lieferantId","required":true}],"description":"Liefert den Bewertungsverlauf eines Lieferanten, chronologisch. Sortiert nach Jahr, dann Quartal (Bewertungen ohne Quartal zuletzt), dann Bewertungsdatum — aufsteigend, älteste zuerst. Die Liste ist ungekappt und kennt keine Blätterung; Entwürfe sind enthalten. Die Antwort trägt `lieferantId` neben `data`. Ein Lieferant ohne Bewertungen ergibt eine leere Liste, kein 404.","summary":"Liefert den Bewertungsverlauf eines Lieferanten, chronologisch","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/lieferanten-bewertung/stats":{"get":{"responses":{"200":{"description":"Statistiken — zaehlen NUR finalisierte Bewertungen. Entwuerfe bleiben aussen vor, eine 0 heisst also „keine finalisierte in dieser Klasse\", nicht „nichts erfasst\".","content":{"application/json":{"schema":{"type":"object","properties":{"klassifizierungAnzahl":{"type":"object","properties":{"A":{"type":"number"},"B":{"type":"number"},"C":{"type":"number"}},"required":["A","B","C"]},"top5":{"type":"array","items":{"type":"object","properties":{"lieferantId":{},"scoreGesamt":{"type":["number","null"]},"klassifizierung":{},"periodeJahr":{"type":["number","null"]},"periodeQuartal":{"type":["number","null"]}},"required":["scoreGesamt","periodeJahr","periodeQuartal"],"additionalProperties":false}},"bottom5":{"type":"array","items":{"type":"object","properties":{"lieferantId":{},"scoreGesamt":{"type":["number","null"]},"klassifizierung":{},"periodeJahr":{"type":["number","null"]},"periodeQuartal":{"type":["number","null"]}},"required":["scoreGesamt","periodeJahr","periodeQuartal"],"additionalProperties":false}}},"required":["klassifizierungAnzahl","top5","bottom5"],"additionalProperties":false},"example":{"klassifizierungAnzahl":{"A":0,"B":0,"C":0},"top5":[{"scoreGesamt":0,"periodeJahr":0,"periodeQuartal":0}],"bottom5":[{"scoreGesamt":0,"periodeJahr":0,"periodeQuartal":0}]}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1Lieferanten-bewertungStats","tags":["Lieferanten"],"parameters":[],"description":"KPIs: A/B/C-Verteilung, Top-5 und Bottom-5 Lieferanten (nach score_gesamt). Gezählt wird ausschließlich über finalisierte Bewertungen, Entwürfe bleiben außen vor. Für die beiden Ranglisten zählt je Lieferant nur seine JÜNGSTE finalisierte Bewertung; danach wird nach Gesamtscore sortiert und auf fünf gekürzt. Bei weniger als fünf bewerteten Lieferanten stehen in Top-5 und Bottom-5 deshalb dieselben Einträge, nur in umgekehrter Reihenfolge.","summary":"KPIs: A/B/C-Verteilung, Top-5 und Bottom-5 Lieferanten (nach score_gesamt)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/einkauf/eingangsrechnungen/{invId}/match":{"post":{"responses":{"201":{"description":"Der Abgleich ist gerechnet und gespeichert. Beachte die gemischte Schreibweise: die Abweichungen kommen in snake_case (`po_qty`, `qty_diff_pct`).","content":{"application/json":{"schema":{"type":"object","properties":{"match_id":{"type":"string","description":"Kennung des gespeicherten Abgleichs. FEHLT, wenn das INSERT keine Zeile zurueckgab — das Ergebnis kommt dann trotzdem als 201."},"status":{"type":"string","enum":["passed","mismatch_quantity","mismatch_amount","mismatch_both","pending_approval"],"description":"Ergebnis des Abgleichs: `passed` alles im Rahmen, `mismatch_quantity` nur die Menge, `mismatch_amount` nur der Betrag, `mismatch_both` beides, `pending_approval` wartet auf eine Entscheidung."},"diff":{"type":"object","properties":{"positions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Der Abgleich-Schluessel dieser Position."},"description":{"type":"string","description":"Anzeigetext. Fehlt, wenn keiner vorliegt."},"po_qty":{"type":["number","null"],"description":"Bestellte Menge. `null`, wenn die Position nicht bestellt war."},"gr_qty":{"type":["number","null"],"description":"Gelieferte Menge. `null`, wenn kein Wareneingang vorliegt."},"inv_qty":{"type":["number","null"],"description":"Berechnete Menge. `null`, wenn nicht berechnet."},"po_price":{"type":["number","null"],"description":"Bestellter Preis. `null`, wenn nicht bestellt."},"inv_price":{"type":["number","null"],"description":"Berechneter Preis. `null`, wenn nicht berechnet."},"qty_diff_pct":{"type":["number","null"],"description":"Mengenabweichung in PROZENT. `null`, wenn eine Seite fehlt. Der Ersatzwert `999` steht fuer „von null abweichend\", also fuer eine nicht ausrechenbare Steigerung."},"price_diff_pct":{"type":["number","null"],"description":"Preisabweichung in PROZENT. `null` und `999` wie bei `qty_diff_pct`."},"qty_in_tolerance":{"type":"boolean","description":"Liegt die Mengenabweichung innerhalb der Mandanten-Toleranz?"},"price_in_tolerance":{"type":"boolean","description":"Liegt die Preisabweichung innerhalb der Mandanten-Toleranz?"},"missing_in_po":{"type":"boolean","description":"Die Position steht nicht in der Bestellung."},"missing_in_gr":{"type":"boolean","description":"Die Position steht nicht im Wareneingang."},"missing_in_inv":{"type":"boolean","description":"Die Position steht nicht in der Rechnung."}},"required":["key","po_qty","gr_qty","inv_qty","po_price","inv_price","qty_diff_pct","price_diff_pct","qty_in_tolerance","price_in_tolerance","missing_in_po","missing_in_gr","missing_in_inv"],"additionalProperties":false},"description":"Ein Eintrag je Position, die in mindestens EINEM der drei Belege vorkommt."},"invoice_total":{"type":"number","description":"Rechnungssumme, aus den Rechnungspositionen gerechnet."},"auto_approve_threshold":{"type":"number","description":"Die geltende Grenze in Euro, unterhalb derer selbsttaetig freigegeben wird."},"auto_approved":{"type":"boolean","description":"Wurde wegen Unterschreiten der Grenze selbsttaetig freigegeben?"}},"required":["positions","invoice_total","auto_approve_threshold","auto_approved"],"additionalProperties":false,"description":"Die Abweichungen je Position samt Rahmenzahlen."},"tolerances":{"type":"object","properties":{"qty_pct":{"type":"number","minimum":0,"maximum":100,"description":"Erlaubte Mengenabweichung in Prozent."},"price_pct":{"type":"number","minimum":0,"maximum":100,"description":"Erlaubte Preisabweichung in Prozent."},"auto_approve_below_eur":{"type":"number","minimum":0,"description":"Betragsgrenze in Euro, unterhalb derer selbsttaetig freigegeben wird."}},"required":["qty_pct","price_pct","auto_approve_below_eur"],"additionalProperties":false,"description":"Die Toleranzen, mit denen gerechnet wurde."}},"required":["status","diff","tolerances"],"additionalProperties":false},"example":{"match_id":"string","status":"passed","diff":{"positions":[{"key":"string","description":"string","po_qty":0,"gr_qty":0,"inv_qty":0,"po_price":0,"inv_price":0,"qty_diff_pct":0,"price_diff_pct":0,"qty_in_tolerance":true,"price_in_tolerance":true,"missing_in_po":true,"missing_in_gr":true,"missing_in_inv":true}],"invoice_total":0,"auto_approve_threshold":0,"auto_approved":true},"tolerances":{"qty_pct":0,"price_pct":0,"auto_approve_below_eur":0}}}}},"400":{"description":"Eingabe ungueltig, oder Mandantenkennung unbrauchbar."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"403":{"description":"Rolle reicht nicht (Antwort der Rechte-Schicht)."},"404":{"description":"Die Rechnung gibt es nicht oder sie hat keine Positionen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invoice_not_found_or_empty","description":"Fester Fehlerschluessel fuer ZWEI Faelle: die Rechnung gibt es nicht, ODER sie hat keine Positionen. Welcher davon zutrifft, sagt die Antwort nicht."}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Das Ergebnis liess sich nicht speichern. Als Text."},"503":{"description":"Datenbank nicht erreichbar (`database unavailable`), als Text."}},"operationId":"postApiV1EinkaufEingangsrechnungenByInvIdMatch","tags":["einkauf","matching"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"invId","required":true}],"summary":"Drei-Wege-Abgleich durchfuehren","description":"Führt einen 3-way-Match (PO/GR/Inv) durch und persistiert das Ergebnis. Ohne `bestellung_id` laeuft der Abgleich mit LEERER Bestellseite — dann gilt jede Rechnungsposition als nicht bestellt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bestellung_id":{"type":"string","minLength":1},"wareneingang_id":{"type":"string","minLength":1}}},"example":{"bestellung_id":"string","wareneingang_id":"string"}}}}},"get":{"responses":{"200":{"description":"Der zuletzt gespeicherte Abgleich samt den drei Belegen nebeneinander. GAB ES NOCH KEINEN, kommt ebenfalls 200 — dann stehen `data` UND `sideBySide` auf `null`. Das ist kein Fehler, sondern „noch nicht abgeglichen\". Die Belegdaten werden dabei FRISCH geladen, koennen also von den gespeicherten Abweichungen abweichen.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":["object","null"],"properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Abgleichs."},"eingangsrechnungId":{"type":"string","minLength":1,"description":"Die abgeglichene Eingangsrechnung."},"bestellungId":{"type":["string","null"],"description":"Die herangezogene Bestellung. `null`, wenn ohne Bestellbezug abgeglichen wurde."},"wareneingangId":{"type":["string","null"],"description":"Der herangezogene Wareneingang. `null`, wenn keiner angegeben war."},"status":{"type":"string","enum":["passed","mismatch_quantity","mismatch_amount","mismatch_both","pending_approval"],"description":"Ergebnis des Abgleichs: `passed` alles im Rahmen, `mismatch_quantity` nur die Menge, `mismatch_amount` nur der Betrag, `mismatch_both` beides, `pending_approval` wartet auf eine Entscheidung."},"diff":{"type":"object","additionalProperties":{},"description":"Die gespeicherten Abweichungen. Inhaltlich die Form von `diff` beim Anlegen — hier aber unveraendert aus der JSONB-Spalte gelesen, weshalb der Vertrag die Felder nicht einzeln zusagt. Leeres Objekt, wenn nichts hinterlegt ist."},"toleranceQtyPct":{"type":["number","null"],"description":"Die BEIM ABGLEICH geltende Mengentoleranz in Prozent — nicht die heutige."},"tolerancePricePct":{"type":["number","null"],"description":"Die BEIM ABGLEICH geltende Preistoleranz in Prozent — nicht die heutige."},"matchedBy":{"type":["string","null"],"description":"Wer den Abgleich angestossen hat. `null`, wenn unbekannt."},"matchedAt":{"type":"string","format":"date-time","description":"Zeitpunkt des Abgleichs als ISO-8601-Zeitstempel in UTC."},"approvedBy":{"type":["string","null"],"description":"Wer entschieden hat. `null`, solange nicht entschieden wurde."},"approvedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Entscheidung. Auch eine ABLEHNUNG setzt ihn — das Feld heisst nur so."},"notes":{"type":["string","null"],"description":"Freitext. Beim Ablehnen steht hier die Begruendung."}},"required":["id","eingangsrechnungId","bestellungId","wareneingangId","status","diff","toleranceQtyPct","tolerancePricePct","matchedBy","matchedAt","approvedBy","approvedAt","notes"],"additionalProperties":false,"description":"Der zuletzt gespeicherte Abgleich. `null`, wenn fuer diese Rechnung noch keiner lief."},"sideBySide":{"type":["object","null"],"properties":{"po":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Der Abgleich-Schluessel: die Artikelnummer, ersatzweise die Bezeichnung. Ueber ihn werden Bestellung, Wareneingang und Rechnung einander zugeordnet."},"description":{"type":"string","description":"Anzeigetext der Position. Fehlt, wenn keiner vorliegt."},"qty":{"type":"number","description":"Menge dieser Position im jeweiligen Beleg."},"price":{"type":"number","description":"Einzelpreis dieser Position im jeweiligen Beleg."}},"required":["key","qty","price"],"additionalProperties":false},"description":"Positionen der Bestellung. Leer ohne Bestellbezug."},"gr":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Der Abgleich-Schluessel: die Artikelnummer, ersatzweise die Bezeichnung. Ueber ihn werden Bestellung, Wareneingang und Rechnung einander zugeordnet."},"description":{"type":"string","description":"Anzeigetext der Position. Fehlt, wenn keiner vorliegt."},"qty":{"type":"number","description":"Menge dieser Position im jeweiligen Beleg."},"price":{"type":"number","description":"Einzelpreis dieser Position im jeweiligen Beleg."}},"required":["key","qty","price"],"additionalProperties":false},"description":"Positionen des Wareneingangs. Leer ohne Wareneingang."},"inv":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Der Abgleich-Schluessel: die Artikelnummer, ersatzweise die Bezeichnung. Ueber ihn werden Bestellung, Wareneingang und Rechnung einander zugeordnet."},"description":{"type":"string","description":"Anzeigetext der Position. Fehlt, wenn keiner vorliegt."},"qty":{"type":"number","description":"Menge dieser Position im jeweiligen Beleg."},"price":{"type":"number","description":"Einzelpreis dieser Position im jeweiligen Beleg."}},"required":["key","qty","price"],"additionalProperties":false},"description":"Positionen der Eingangsrechnung."}},"required":["po","gr","inv"],"additionalProperties":false,"description":"Die drei Belege nebeneinander, frisch geladen. `null` GENAU DANN, wenn auch `data` `null` ist — beide Felder fallen gemeinsam weg."}},"required":["data","sideBySide"],"additionalProperties":false},"example":{"data":{"id":"string","eingangsrechnungId":"string","bestellungId":"string","wareneingangId":"string","status":"passed","diff":{},"toleranceQtyPct":0,"tolerancePricePct":0,"matchedBy":"string","matchedAt":"2026-01-01T12:00:00.000Z","approvedBy":"string","approvedAt":"2026-01-01T12:00:00.000Z","notes":"string"},"sideBySide":{"po":[{"key":"string","description":"string","qty":0,"price":0}],"gr":[{"key":"string","description":"string","qty":0,"price":0}],"inv":[{"key":"string","description":"string","qty":0,"price":0}]}}}}},"400":{"description":"Mandantenkennung unbrauchbar (`invalid tenant slug`), als Text."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"403":{"description":"Rolle reicht nicht (Antwort der Rechte-Schicht)."},"500":{"description":"Der Abgleich liess sich nicht laden. Als Text."},"503":{"description":"Datenbank nicht erreichbar (`database unavailable`), als Text."}},"operationId":"getApiV1EinkaufEingangsrechnungenByInvIdMatch","tags":["einkauf","matching"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"invId","required":true}],"summary":"Letzten Abgleich einer Rechnung lesen","description":"Liefert das letzte Match-Result inkl. Side-by-Side-Daten"}},"/api/v1/einkauf/match/{matchId}/approve":{"post":{"responses":{"200":{"description":"Knappe Quittung. Der Abgleich selbst kommt NICHT zurueck — dafuer noch einmal lesen.","content":{"application/json":{"schema":{"type":"object","properties":{"match_id":{"type":"string","minLength":1,"description":"Der entschiedene Abgleich, aus dem Pfad uebernommen."},"status":{"type":"string","const":"approved","description":"Immer `approved`."}},"required":["match_id","status"],"additionalProperties":false},"example":{"match_id":"string","status":"approved"}}}},"400":{"description":"Eingabe ungueltig, oder Mandantenkennung unbrauchbar."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"403":{"description":"Rolle reicht nicht (Antwort der Rechte-Schicht)."},"404":{"description":"Kein Abgleich mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"match_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Die Freigabe liess sich nicht schreiben. Als Text."},"503":{"description":"Datenbank nicht erreichbar (`database unavailable`), als Text."}},"operationId":"postApiV1EinkaufMatchByMatchIdApprove","tags":["einkauf","matching"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"matchId","required":true}],"summary":"Abgleich freigeben","description":"Approve eines Match-Mismatch durch Buchhaltung. Der bisherige Zustand wird NICHT geprueft — auch ein bereits abgelehnter Abgleich laesst sich so freigeben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string","maxLength":2000}}},"example":{"notes":"string"}}}}}},"/api/v1/einkauf/match/{matchId}/reject":{"post":{"responses":{"200":{"description":"Knappe Quittung samt zurueckgegebener Begruendung. Der Abgleich selbst kommt NICHT zurueck — dafuer noch einmal lesen.","content":{"application/json":{"schema":{"type":"object","properties":{"match_id":{"type":"string","minLength":1,"description":"Der entschiedene Abgleich, aus dem Pfad uebernommen."},"status":{"type":"string","const":"rejected","description":"Immer `rejected`."},"reason":{"type":"string","minLength":1,"description":"Die mitgegebene Begruendung. Sie ERSETZT eine bisherige Notiz am Abgleich."}},"required":["match_id","status","reason"],"additionalProperties":false},"example":{"match_id":"string","status":"rejected","reason":"string"}}}},"400":{"description":"Begruendung fehlt oder ist leer, oder Mandantenkennung unbrauchbar."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"403":{"description":"Rolle reicht nicht (Antwort der Rechte-Schicht)."},"404":{"description":"Kein Abgleich mit dieser Kennung im Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"match_not_found","description":"Fester Fehlerschluessel. Ein Klartext folgt hier nicht."}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Die Ablehnung liess sich nicht schreiben. Als Text."},"503":{"description":"Datenbank nicht erreichbar (`database unavailable`), als Text."}},"operationId":"postApiV1EinkaufMatchByMatchIdReject","tags":["einkauf","matching"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"matchId","required":true}],"summary":"Abgleich ablehnen","description":"Reject eines Match-Mismatch mit Begründung. Die Begruendung ERSETZT eine bisherige Notiz am Abgleich; der bisherige Zustand wird nicht geprueft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","minLength":1,"maxLength":2000}},"required":["reason"]},"example":{"reason":"string"}}}}}},"/api/v1/einkauf/lieferanten/{id}/scorecard":{"get":{"responses":{"200":{"description":"Kennzahlen, Monatsverlauf und — falls vorhanden — der Mandantenschnitt zum Vergleich. Ohne Bestellungen im Zeitraum stehen alle Kennzahlen auf `0`; das heisst „keine Daten\", nicht „schlechter Lieferant\".","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"liefertreue_pct":{"type":"number","description":"Anteil termingerechter Lieferungen in PROZENT. Bestellungen ohne Wunsch- oder Eingangsdatum gelten als NICHT termingerecht."},"mengentreue_pct":{"type":"number","description":"Anteil vollstaendig gelieferter Mengen in Prozent."},"qualitaet_score":{"type":"number","description":"Qualitaetsnote von 0 bis 100 aus den Pruefergebnissen."},"reklamationsquote_pct":{"type":"number","description":"Anteil beanstandeter Bestellungen in Prozent."},"avg_lead_time_days":{"type":"number","description":"Durchschnittliche Lieferzeit in Tagen."},"order_count":{"type":"integer","minimum":0,"description":"Anzahl der Bestellungen im Zeitraum."},"total_spend":{"type":"number","description":"Einkaufsvolumen im Zeitraum, in Euro."},"trend_chart":{"type":"array","items":{"type":"object","properties":{"month":{"type":"string","pattern":"^\\d{4}-\\d{2}$","description":"Monat im Muster `JJJJ-MM`."},"order_count":{"type":"integer","minimum":0,"description":"Bestellungen in diesem Monat."},"on_time_pct":{"type":"number","description":"Termintreue dieses Monats in Prozent."},"spend":{"type":"number","description":"Einkaufsvolumen dieses Monats in Euro."}},"required":["month","order_count","on_time_pct","spend"],"additionalProperties":false},"description":"Monatsverlauf ueber den gewaehlten Zeitraum."},"tenant_avg":{"type":["object","null"],"properties":{"liefertreue_pct":{"type":"number","description":"Anteil termingerechter Lieferungen in PROZENT. Bestellungen ohne Wunsch- oder Eingangsdatum gelten als NICHT termingerecht."},"mengentreue_pct":{"type":"number","description":"Anteil vollstaendig gelieferter Mengen in Prozent."},"qualitaet_score":{"type":"number","description":"Qualitaetsnote von 0 bis 100 aus den Pruefergebnissen."},"reklamationsquote_pct":{"type":"number","description":"Anteil beanstandeter Bestellungen in Prozent."},"avg_lead_time_days":{"type":"number","description":"Durchschnittliche Lieferzeit in Tagen."},"order_count":{"type":"integer","minimum":0,"description":"Anzahl der Bestellungen im Zeitraum."},"total_spend":{"type":"number","description":"Einkaufsvolumen im Zeitraum, in Euro."}},"additionalProperties":false,"description":"Der Mandantenschnitt zum Vergleich. Kann fehlen oder `null` sein — dann gibt es keine Vergleichsbasis. Auch einzelne Kennzahlen koennen darin fehlen."}},"required":["liefertreue_pct","mengentreue_pct","qualitaet_score","reklamationsquote_pct","avg_lead_time_days","order_count","total_spend","trend_chart"],"additionalProperties":false,"description":"Kennzahlen des Lieferanten samt Verlauf."},"meta":{"type":"object","properties":{"period":{"type":"string","enum":["3m","6m","12m","24m"],"description":"Der ausgewertete Zeitraum, unveraendert aus der Abfrage uebernommen."},"lieferantId":{"type":"string","minLength":1,"description":"Der ausgewertete Lieferant."}},"required":["period","lieferantId"],"additionalProperties":false,"description":"Angaben zur Abfrage."}},"required":["data","meta"],"additionalProperties":false},"example":{"data":{"liefertreue_pct":0,"mengentreue_pct":0,"qualitaet_score":0,"reklamationsquote_pct":0,"avg_lead_time_days":0,"order_count":0,"total_spend":0,"trend_chart":[],"tenant_avg":{"liefertreue_pct":0,"mengentreue_pct":0,"qualitaet_score":0,"reklamationsquote_pct":0,"avg_lead_time_days":0,"order_count":0,"total_spend":0}},"meta":{"period":"3m","lieferantId":"string"}}}}},"400":{"description":"Ungueltiger `period`-Wert, oder Mandantenkennung unbrauchbar."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"403":{"description":"Rolle reicht nicht (Antwort der Rechte-Schicht)."},"500":{"description":"Die Kennzahlen liessen sich nicht laden. Als Text."}},"operationId":"getApiV1EinkaufLieferantenByIdScorecard","tags":["einkauf","scorecard"],"parameters":[{"in":"query","name":"period","schema":{"type":"string","enum":["3m","6m","12m","24m"],"default":"12m"}},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lieferanten-Kennzahlen lesen","description":"Lieferanten-Scorecard mit KPIs + Trend-Chart"}},"/api/v1/admin/tenant-tolerances":{"get":{"responses":{"200":{"description":"Die geltenden Toleranzen. Hat der Mandant noch keine eigenen gesetzt, kommen die Hausvorgaben — die Antwort unterscheidet beides NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"tolerance_qty_pct":{"type":"number","minimum":0,"maximum":100,"description":"Erlaubte Mengenabweichung in Prozent."},"tolerance_price_pct":{"type":"number","minimum":0,"maximum":100,"description":"Erlaubte Preisabweichung in Prozent."},"auto_approve_below_eur":{"type":"number","minimum":0,"description":"Betragsgrenze in Euro, unterhalb derer selbsttaetig freigegeben wird."}},"required":["tolerance_qty_pct","tolerance_price_pct","auto_approve_below_eur"],"additionalProperties":false,"description":"Die geltenden Toleranzen. Ohne eigene Einstellung liefert der Endpunkt die Vorgaben."}},"required":["data"],"additionalProperties":false},"example":{"data":{"tolerance_qty_pct":0,"tolerance_price_pct":0,"auto_approve_below_eur":0}}}}},"400":{"description":"Mandantenkennung unbrauchbar (`invalid tenant slug`), als Text."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"403":{"description":"Rolle reicht nicht (Antwort der Rechte-Schicht)."}},"operationId":"getApiV1AdminTenant-tolerances","tags":["admin","einkauf"],"parameters":[],"summary":"Abgleich-Toleranzen lesen","description":"Liefert die aktuellen Tenant-Match-Toleranzen"},"patch":{"responses":{"200":{"description":"Die Toleranzen im Zustand nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"tolerance_qty_pct":{"type":"number","minimum":0,"maximum":100,"description":"Erlaubte Mengenabweichung in Prozent."},"tolerance_price_pct":{"type":"number","minimum":0,"maximum":100,"description":"Erlaubte Preisabweichung in Prozent."},"auto_approve_below_eur":{"type":"number","minimum":0,"description":"Betragsgrenze in Euro, unterhalb derer selbsttaetig freigegeben wird."}},"required":["tolerance_qty_pct","tolerance_price_pct","auto_approve_below_eur"],"additionalProperties":false,"description":"Die geltenden Toleranzen. Ohne eigene Einstellung liefert der Endpunkt die Vorgaben."}},"required":["data"],"additionalProperties":false},"example":{"data":{"tolerance_qty_pct":0,"tolerance_price_pct":0,"auto_approve_below_eur":0}}}}},"400":{"description":"Eingabe ungueltig, oder Mandantenkennung unbrauchbar."},"401":{"description":"Kein Mandantenkontext (`tenant context missing`), als Text."},"403":{"description":"Rolle reicht nicht (Antwort der Rechte-Schicht)."},"500":{"description":"Die Toleranzen liessen sich nicht schreiben. Als Text."}},"operationId":"patchApiV1AdminTenant-tolerances","tags":["admin","einkauf"],"parameters":[],"summary":"Abgleich-Toleranzen aendern","description":"Patch der Tenant-Match-Toleranzen. Nicht mitgeschickte Felder bleiben unveraendert. Die Aenderung wirkt nur auf KUENFTIGE Abgleiche — gespeicherte Ergebnisse behalten die Toleranzen, mit denen sie gerechnet wurden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tolerance_qty_pct":{"type":"number","minimum":0,"maximum":100},"tolerance_price_pct":{"type":"number","minimum":0,"maximum":100},"auto_approve_below_eur":{"type":"number","minimum":0,"maximum":1000000}}},"example":{"tolerance_qty_pct":0,"tolerance_price_pct":0,"auto_approve_below_eur":0}}}}}},"/api/v1/qm/stats":{"get":{"responses":{"200":{"description":"QM Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"pruefmittelGesamt":{"type":"number"},"pruefmerkmaleGesamt":{"type":"number"},"aktivePruefplaene":{"type":"number"},"prueforteGesamt":{"type":"number"},"kalibrierungFaellig":{"type":"number"},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["pruefmittelGesamt","pruefmerkmaleGesamt","aktivePruefplaene","prueforteGesamt","kalibrierungFaellig","meta"],"additionalProperties":false},"example":{"pruefmittelGesamt":0,"pruefmerkmaleGesamt":0,"aktivePruefplaene":0,"prueforteGesamt":0,"kalibrierungFaellig":0,"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1QmStats","tags":["QM"],"parameters":[],"summary":"QM KPIs: Prüfmittel, -merkmale, -pläne, -orte","description":"Zählt in einem Durchgang die Prüfmittel, Prüfmerkmale und Prüforte des Mandanten sowie die Prüfpläne mit `aktiv = TRUE`. `kalibrierungFaellig` zählt die Prüfmittel, deren `naechste_kalibrierung` gesetzt ist und höchstens 30 Tage in der Zukunft liegt (überfällige zählen mit). Die vier QM-Tabellen werden beim ersten Aufruf angelegt, ein frischer Mandant bekommt also Nullen statt eines Fehlers."}},"/api/v1/qm/pruefmittel":{"get":{"responses":{"200":{"description":"Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"nummer":{"type":"string"},"bezeichnung":{"type":"string"},"typ":{"type":["string","null"]},"kalibrierIntervallTage":{"type":["number","null"]},"letzteKalibrierung":{},"naechsteKalibrierung":{},"status":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","nummer","bezeichnung","typ","kalibrierIntervallTage","status"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","nummer":"string","bezeichnung":"string","typ":"string","kalibrierIntervallTage":0,"status":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1QmPruefmittel","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste Prüfmittel","description":"Liest `qm_pruefmittel` des Mandanten, sortiert nach `nummer` aufsteigend. Blättert über `limit` (1-200, Standard 50) und `offset`; `pagination.total` ist die Gesamtzahl der Zeilen. Die Tabelle kennt kein `deleted_at` — gelöschte Prüfmittel sind wirklich weg."},"post":{"responses":{"201":{"description":"Angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"nummer":{"type":"string"},"bezeichnung":{"type":"string"},"typ":{"type":["string","null"]},"kalibrierIntervallTage":{"type":["number","null"]},"letzteKalibrierung":{},"naechsteKalibrierung":{},"status":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","nummer","bezeichnung","typ","kalibrierIntervallTage","status"],"additionalProperties":false},"example":{"id":"string","nummer":"string","bezeichnung":"string","typ":"string","kalibrierIntervallTage":0,"status":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1QmPruefmittel","tags":["QM"],"parameters":[],"summary":"Prüfmittel anlegen","description":"Legt eine Zeile in `qm_pruefmittel` an und gibt sie mit vergebener `id` zurück (201). Pflicht sind `nummer` und `bezeichnung`; `status` steht ohne Angabe auf `aktiv`. Die Nummer wird NICHT automatisch vergeben und nicht auf Eindeutigkeit geprüft. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"nummer":{"type":"string","minLength":1,"maxLength":50},"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"typ":{"type":"string","maxLength":100},"kalibrierIntervallTage":{"type":"integer","minimum":1},"letzteKalibrierung":{"type":"string","format":"date"},"naechsteKalibrierung":{"type":"string","format":"date"},"status":{"type":"string","maxLength":30,"default":"aktiv"}},"required":["nummer","bezeichnung"]},"example":{"nummer":"string","bezeichnung":"string","typ":"string","kalibrierIntervallTage":1,"letzteKalibrierung":"2026-01-01","naechsteKalibrierung":"2026-01-01","status":"string"}}}}}},"/api/v1/qm/pruefmittel/{id}":{"get":{"responses":{"200":{"description":"Prüfmittel","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"nummer":{"type":"string"},"bezeichnung":{"type":"string"},"typ":{"type":["string","null"]},"kalibrierIntervallTage":{"type":["number","null"]},"letzteKalibrierung":{},"naechsteKalibrierung":{},"status":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","nummer","bezeichnung","typ","kalibrierIntervallTage","status"],"additionalProperties":false},"example":{"id":"string","nummer":"string","bezeichnung":"string","typ":"string","kalibrierIntervallTage":0,"status":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1QmPruefmittelById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelnes Prüfmittel","description":"Liest genau eine Zeile aus `qm_pruefmittel` über die id im Pfad. Eine unbekannte id beantwortet der Handler mit 404 und `{ error: \"not_found\" }`."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"nummer":{"type":"string"},"bezeichnung":{"type":"string"},"typ":{"type":["string","null"]},"kalibrierIntervallTage":{"type":["number","null"]},"letzteKalibrierung":{},"naechsteKalibrierung":{},"status":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","nummer","bezeichnung","typ","kalibrierIntervallTage","status"],"additionalProperties":false},"example":{"id":"string","nummer":"string","bezeichnung":"string","typ":"string","kalibrierIntervallTage":0,"status":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1QmPruefmittelById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Prüfmittel aktualisieren","description":"Teilkörper erlaubt: nicht gesendete Felder behalten ihren gespeicherten Wert, ein ausdrücklich gesendetes `null` löscht ihn. Setzt `updated_at` auf NOW() und gibt die geschriebene Zeile zurück. Unbekannte id → 404. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"nummer":{"type":"string","minLength":1,"maxLength":50},"bezeichnung":{"type":"string","minLength":1,"maxLength":255},"typ":{"type":"string","maxLength":100},"kalibrierIntervallTage":{"type":"integer","minimum":1},"letzteKalibrierung":{"type":"string","format":"date"},"naechsteKalibrierung":{"type":"string","format":"date"},"status":{"type":"string","maxLength":30,"default":"aktiv"}}},"example":{"nummer":"string","bezeichnung":"string","typ":"string","kalibrierIntervallTage":1,"letzteKalibrierung":"2026-01-01","naechsteKalibrierung":"2026-01-01","status":"string"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1QmPruefmittelById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Prüfmittel löschen","description":"Entfernt die Zeile endgültig (`DELETE`, kein Soft-Delete). Prüfmerkmale, die über `pruefmittel_id` darauf zeigen, werden dabei nicht mitgepflegt. Unbekannte id → 404. Erfordert mindestens die Rolle Manager."}},"/api/v1/qm/pruefmerkmale":{"get":{"responses":{"200":{"description":"Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"typ":{"type":"string","enum":["numerisch","attributiv","visuell"]},"einheit":{"type":["string","null"]},"sollwert":{"type":["number","null"]},"toleranzUnten":{"type":["number","null"]},"toleranzOben":{"type":["number","null"]},"pruefmittelId":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","name","typ","einheit","sollwert","toleranzUnten","toleranzOben","pruefmittelId"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","typ":"numerisch","einheit":"string","sollwert":0,"toleranzUnten":0,"toleranzOben":0,"pruefmittelId":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1QmPruefmerkmale","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste Prüfmerkmale","description":"Liest `qm_pruefmerkmale` des Mandanten, sortiert nach `name` aufsteigend, geblättert über `limit` (1-200, Standard 50) und `offset`. `sollwert` und die beiden Toleranzgrenzen kommen als Zahl oder `null`; `pruefmittelId` nennt das zugeordnete Prüfmittel, sofern eines hinterlegt ist."},"post":{"responses":{"201":{"description":"Angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"typ":{"type":"string","enum":["numerisch","attributiv","visuell"]},"einheit":{"type":["string","null"]},"sollwert":{"type":["number","null"]},"toleranzUnten":{"type":["number","null"]},"toleranzOben":{"type":["number","null"]},"pruefmittelId":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","name","typ","einheit","sollwert","toleranzUnten","toleranzOben","pruefmittelId"],"additionalProperties":false},"example":{"id":"string","name":"string","typ":"numerisch","einheit":"string","sollwert":0,"toleranzUnten":0,"toleranzOben":0,"pruefmittelId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1QmPruefmerkmale","tags":["QM"],"parameters":[],"summary":"Prüfmerkmal anlegen","description":"Legt ein Prüfmerkmal an (201). `typ` ist auf `numerisch`, `attributiv` oder `visuell` beschränkt — dieselbe Auswahl erzwingt auch die Datenbank per CHECK-Bedingung. Einheit, Sollwert, Toleranzgrenzen und die Zuordnung zu einem Prüfmittel sind optional. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"typ":{"type":"string","enum":["numerisch","attributiv","visuell"]},"einheit":{"type":"string","maxLength":50},"sollwert":{"type":"number"},"toleranzUnten":{"type":"number"},"toleranzOben":{"type":"number"},"pruefmittelId":{"type":"string","format":"uuid"}},"required":["name","typ"]},"example":{"name":"string","typ":"numerisch","einheit":"string","sollwert":0,"toleranzUnten":0,"toleranzOben":0,"pruefmittelId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/qm/pruefmerkmale/{id}":{"get":{"responses":{"200":{"description":"Prüfmerkmal","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"typ":{"type":"string","enum":["numerisch","attributiv","visuell"]},"einheit":{"type":["string","null"]},"sollwert":{"type":["number","null"]},"toleranzUnten":{"type":["number","null"]},"toleranzOben":{"type":["number","null"]},"pruefmittelId":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","name","typ","einheit","sollwert","toleranzUnten","toleranzOben","pruefmittelId"],"additionalProperties":false},"example":{"id":"string","name":"string","typ":"numerisch","einheit":"string","sollwert":0,"toleranzUnten":0,"toleranzOben":0,"pruefmittelId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1QmPruefmerkmaleById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelnes Prüfmerkmal","description":"Liest genau eine Zeile aus `qm_pruefmerkmale` über die id im Pfad. Eine unbekannte id beantwortet der Handler mit 404 und `{ error: \"not_found\" }`."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"typ":{"type":"string","enum":["numerisch","attributiv","visuell"]},"einheit":{"type":["string","null"]},"sollwert":{"type":["number","null"]},"toleranzUnten":{"type":["number","null"]},"toleranzOben":{"type":["number","null"]},"pruefmittelId":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","name","typ","einheit","sollwert","toleranzUnten","toleranzOben","pruefmittelId"],"additionalProperties":false},"example":{"id":"string","name":"string","typ":"numerisch","einheit":"string","sollwert":0,"toleranzUnten":0,"toleranzOben":0,"pruefmittelId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1QmPruefmerkmaleById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Prüfmerkmal aktualisieren","description":"Teilkörper erlaubt: nicht gesendete Felder behalten ihren gespeicherten Wert, ein ausdrücklich gesendetes `null` löscht ihn. Setzt `updated_at` auf NOW(). Unbekannte id → 404. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"typ":{"type":"string","enum":["numerisch","attributiv","visuell"]},"einheit":{"type":"string","maxLength":50},"sollwert":{"type":"number"},"toleranzUnten":{"type":"number"},"toleranzOben":{"type":"number"},"pruefmittelId":{"type":"string","format":"uuid"}}},"example":{"name":"string","typ":"numerisch","einheit":"string","sollwert":0,"toleranzUnten":0,"toleranzOben":0,"pruefmittelId":"00000000-0000-4000-8000-000000000000"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1QmPruefmerkmaleById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Prüfmerkmal löschen","description":"Entfernt die Zeile endgültig (`DELETE`, kein Soft-Delete). Prüfpläne, die die id in ihrer Liste `pruefmerkmaleIds` führen, behalten den Verweis — er wird hier nicht aufgeräumt. Unbekannte id → 404. Erfordert mindestens die Rolle Manager."}},"/api/v1/qm/pruefplaene":{"get":{"responses":{"200":{"description":"Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"artikelId":{"type":["string","null"]},"prozessTyp":{"type":["string","null"]},"pruefmerkmaleIds":{"type":"array","items":{"type":"string"}},"frequenz":{"type":["string","null"]},"aktiv":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["id","name","artikelId","prozessTyp","pruefmerkmaleIds","frequenz","aktiv"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","artikelId":"string","prozessTyp":"string","pruefmerkmaleIds":["string"],"frequenz":"string","aktiv":true}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1QmPruefplaene","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste Prüfpläne","description":"Liest `qm_pruefplaene` des Mandanten, sortiert nach `name` aufsteigend, geblättert über `limit` (1-200, Standard 50) und `offset`. Die Liste enthält aktive UND inaktive Pläne; gefiltert wird hier nicht. `pruefmerkmaleIds` ist das gespeicherte JSONB-Feld mit den zugeordneten Prüfmerkmal-Ids."},"post":{"responses":{"201":{"description":"Angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"artikelId":{"type":["string","null"]},"prozessTyp":{"type":["string","null"]},"pruefmerkmaleIds":{"type":"array","items":{"type":"string"}},"frequenz":{"type":["string","null"]},"aktiv":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["id","name","artikelId","prozessTyp","pruefmerkmaleIds","frequenz","aktiv"],"additionalProperties":false},"example":{"id":"string","name":"string","artikelId":"string","prozessTyp":"string","pruefmerkmaleIds":["string"],"frequenz":"string","aktiv":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1QmPruefplaene","tags":["QM"],"parameters":[],"summary":"Prüfplan anlegen","description":"Legt einen Prüfplan an (201). `pruefmerkmaleIds` wird als JSONB gespeichert und ist ohne Angabe eine leere Liste; die enthaltenen Ids werden NICHT gegen `qm_pruefmerkmale` geprüft. `aktiv` steht ohne Angabe auf true. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"artikelId":{"type":"string","format":"uuid"},"prozessTyp":{"type":"string","maxLength":50},"pruefmerkmaleIds":{"type":"array","items":{"type":"string","format":"uuid"},"default":[]},"frequenz":{"type":"string","maxLength":100},"aktiv":{"type":"boolean","default":true}},"required":["name"]},"example":{"name":"string","artikelId":"00000000-0000-4000-8000-000000000000","prozessTyp":"string","pruefmerkmaleIds":["00000000-0000-4000-8000-000000000000"],"frequenz":"string","aktiv":true}}}}}},"/api/v1/qm/pruefplaene/{id}":{"get":{"responses":{"200":{"description":"Prüfplan","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"artikelId":{"type":["string","null"]},"prozessTyp":{"type":["string","null"]},"pruefmerkmaleIds":{"type":"array","items":{"type":"string"}},"frequenz":{"type":["string","null"]},"aktiv":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["id","name","artikelId","prozessTyp","pruefmerkmaleIds","frequenz","aktiv"],"additionalProperties":false},"example":{"id":"string","name":"string","artikelId":"string","prozessTyp":"string","pruefmerkmaleIds":["string"],"frequenz":"string","aktiv":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1QmPruefplaeneById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelner Prüfplan","description":"Liest genau eine Zeile aus `qm_pruefplaene` über die id im Pfad. Eine unbekannte id beantwortet der Handler mit 404 und `{ error: \"not_found\" }`."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"artikelId":{"type":["string","null"]},"prozessTyp":{"type":["string","null"]},"pruefmerkmaleIds":{"type":"array","items":{"type":"string"}},"frequenz":{"type":["string","null"]},"aktiv":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["id","name","artikelId","prozessTyp","pruefmerkmaleIds","frequenz","aktiv"],"additionalProperties":false},"example":{"id":"string","name":"string","artikelId":"string","prozessTyp":"string","pruefmerkmaleIds":["string"],"frequenz":"string","aktiv":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1QmPruefplaeneById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Prüfplan aktualisieren","description":"Teilkörper erlaubt: nicht gesendete Felder behalten ihren gespeicherten Wert. Wird `pruefmerkmaleIds` mitgeschickt, ERSETZT die gesendete Liste die gespeicherte vollständig — zusammengeführt wird nicht. Setzt `updated_at` auf NOW(). Unbekannte id → 404. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"artikelId":{"type":"string","format":"uuid"},"prozessTyp":{"type":"string","maxLength":50},"pruefmerkmaleIds":{"type":"array","items":{"type":"string","format":"uuid"},"default":[]},"frequenz":{"type":"string","maxLength":100},"aktiv":{"type":"boolean","default":true}}},"example":{"name":"string","artikelId":"00000000-0000-4000-8000-000000000000","prozessTyp":"string","pruefmerkmaleIds":["00000000-0000-4000-8000-000000000000"],"frequenz":"string","aktiv":true}}}}},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1QmPruefplaeneById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Prüfplan löschen","description":"Entfernt die Zeile endgültig (`DELETE`, kein Soft-Delete). Die im Plan referenzierten Prüfmerkmale bleiben bestehen. Unbekannte id → 404. Erfordert mindestens die Rolle Manager."}},"/api/v1/qm/prueforte":{"get":{"responses":{"200":{"description":"Liste","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"abteilung":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","name","beschreibung","abteilung"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","beschreibung":"string","abteilung":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1QmPrueforte","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste Prüforte","description":"Liest `qm_prueforte` des Mandanten, sortiert nach `name` aufsteigend, geblättert über `limit` (1-200, Standard 50) und `offset`. Ein Prüfort trägt neben dem Namen eine Beschreibung und eine Abteilung, beide dürfen null sein."},"post":{"responses":{"201":{"description":"Angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"abteilung":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","name","beschreibung","abteilung"],"additionalProperties":false},"example":{"id":"string","name":"string","beschreibung":"string","abteilung":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1QmPrueforte","tags":["QM"],"parameters":[],"summary":"Prüfort anlegen","description":"Legt einen Prüfort an (201). Pflicht ist allein `name`; Beschreibung und Abteilung sind optional und werden sonst als null gespeichert. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"beschreibung":{"type":"string"},"abteilung":{"type":"string","maxLength":100}},"required":["name"]},"example":{"name":"string","beschreibung":"string","abteilung":"string"}}}}}},"/api/v1/qm/prueforte/{id}":{"get":{"responses":{"200":{"description":"Prüfort","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"abteilung":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","name","beschreibung","abteilung"],"additionalProperties":false},"example":{"id":"string","name":"string","beschreibung":"string","abteilung":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1QmPrueforteById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einzelner Prüfort","description":"Liest genau eine Zeile aus `qm_prueforte` über die id im Pfad. Eine unbekannte id beantwortet der Handler mit 404 und `{ error: \"not_found\" }`."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"abteilung":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","name","beschreibung","abteilung"],"additionalProperties":false},"example":{"id":"string","name":"string","beschreibung":"string","abteilung":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1QmPrueforteById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Prüfort aktualisieren","description":"Teilkörper erlaubt: nicht gesendete Felder behalten ihren gespeicherten Wert, ein ausdrücklich gesendetes `null` löscht ihn. Setzt `updated_at` auf NOW(). Unbekannte id → 404. Erfordert mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"beschreibung":{"type":"string"},"abteilung":{"type":"string","maxLength":100}}},"example":{"name":"string","beschreibung":"string","abteilung":"string"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1QmPrueforteById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Prüfort löschen","description":"Entfernt die Zeile endgültig aus `qm_prueforte` (`DELETE`, kein Soft-Delete). Unbekannte id → 404. Erfordert mindestens die Rolle Manager."}},"/api/v1/qm/pruefungen":{"post":{"responses":{"201":{"description":"Die angelegte Pruefung — noch ohne Ergebnisse.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"pruefplanId":{"type":"string"},"fertigungsauftragId":{"type":["string","null"]},"chargeId":{"type":["string","null"]},"lieferscheinId":{"type":["string","null"]},"geprueftVon":{"type":["string","null"],"description":"Nutzer, der die Pruefung anlegte"},"geprueftAm":{"type":["string","null"],"description":"Gesetzt erst beim Abschluss"},"status":{"type":"string","description":"offen | bestanden | nacharbeit | ausschuss"},"begruendung":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","pruefplanId","fertigungsauftragId","chargeId","lieferscheinId","geprueftVon","geprueftAm","status","begruendung","createdAt","updatedAt"]},"example":{"id":"string","pruefplanId":"string","fertigungsauftragId":"string","chargeId":"string","lieferscheinId":"string","geprueftVon":"string","geprueftAm":"string","status":"string","begruendung":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Der Pruefplan existiert nicht (`pruefplan_not_found`)"}},"operationId":"postApiV1QmPruefungen","tags":["QM"],"parameters":[],"description":"Legt eine Pruefung gegen einen Pruefplan an. Neben `pruefplan_id` muss MINDESTENS einer der Bezuege `fertigungsauftrag_id`, `charge_id` oder `lieferschein_id` gesetzt sein, sonst 400. Der Pruefplan wird auf Existenz geprueft (404), sofern es die Stammdatentabelle im Mandanten schon gibt — fehlt sie, laeuft der Aufruf ohne diese Pruefung durch. Die Pruefung startet im Status `offen` und wird auf den angemeldeten Nutzer geschrieben; ein Eintrag im Aktivitaetsverlauf entsteht nebenbei. Ab Rolle `user`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"pruefplan_id":{"type":"string","minLength":1},"fertigungsauftrag_id":{"type":"string","minLength":1},"charge_id":{"type":"string","minLength":1},"lieferschein_id":{"type":"string","minLength":1}},"required":["pruefplan_id"]},"example":{"pruefplan_id":"string","fertigungsauftrag_id":"string","charge_id":"string","lieferschein_id":"string"}}}},"summary":"Legt eine Pruefung gegen einen Pruefplan an","x-nemix-summary-source":"description:first-sentence"},"get":{"responses":{"200":{"description":"Die gefilterten Pruefungen samt Blaetterung.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"pruefplanId":{"type":"string"},"fertigungsauftragId":{"type":["string","null"]},"chargeId":{"type":["string","null"]},"lieferscheinId":{"type":["string","null"]},"geprueftVon":{"type":["string","null"],"description":"Nutzer, der die Pruefung anlegte"},"geprueftAm":{"type":["string","null"],"description":"Gesetzt erst beim Abschluss"},"status":{"type":"string","description":"offen | bestanden | nacharbeit | ausschuss"},"begruendung":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","pruefplanId","fertigungsauftragId","chargeId","lieferscheinId","geprueftVon","geprueftAm","status","begruendung","createdAt","updatedAt"]}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer","description":"Treffer OHNE Limit — die ganze Filtermenge"}},"required":["limit","offset","total"]}},"required":["data","pagination"]},"example":{"data":[{"id":"string","pruefplanId":"string","fertigungsauftragId":"string","chargeId":"string","lieferscheinId":"string","geprueftVon":"string","geprueftAm":"string","status":"string","begruendung":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1QmPruefungen","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["offen","bestanden","nacharbeit","ausschuss"]}},{"in":"query","name":"pruefplan_id","schema":{"type":"string"}},{"in":"query","name":"fertigungsauftrag_id","schema":{"type":"string"}},{"in":"query","name":"date_from","schema":{"type":"string","format":"date"}},{"in":"query","name":"date_to","schema":{"type":"string","format":"date"}}],"description":"Listet die Pruefungen des Mandanten, neueste zuerst (nach Anlagedatum). Eingrenzen ueber `status`, `pruefplan_id`, `fertigungsauftrag_id` sowie `date_from`/`date_to` auf das Anlagedatum. Geblaettert wird ueber `limit` (1..200, Vorgabe 50) und `offset`; `pagination.total` zaehlt die Treffer OHNE Limit. Die Ergebnisse je Merkmal stehen NICHT in der Liste — dafuer die Detailansicht. Ab Rolle `user`.","summary":"Listet die Pruefungen des Mandanten, neueste zuerst (nach Anlagedatum)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/qm/pruefungen/{id}":{"get":{"responses":{"200":{"description":"Die Pruefung, um ihre Ergebnisse erweitert.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"pruefplanId":{"type":"string"},"fertigungsauftragId":{"type":["string","null"]},"chargeId":{"type":["string","null"]},"lieferscheinId":{"type":["string","null"]},"geprueftVon":{"type":["string","null"],"description":"Nutzer, der die Pruefung anlegte"},"geprueftAm":{"type":["string","null"],"description":"Gesetzt erst beim Abschluss"},"status":{"type":"string","description":"offen | bestanden | nacharbeit | ausschuss"},"begruendung":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"ergebnisse":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"pruefungId":{"type":"string"},"pruefmerkmalId":{"type":"string"},"istWert":{"type":["number","null"],"description":"Der gemessene Wert"},"sollWert":{"type":["number","null"],"description":"Aus dem Pruefmerkmal uebernommen"},"toleranzUnten":{"type":["number","null"]},"toleranzOben":{"type":["number","null"]},"inToleranz":{"type":"boolean","description":"Das Urteil — siehe Beschreibung des Aufrufs"},"fotoUrl":{"type":["string","null"]},"kommentar":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","pruefungId","pruefmerkmalId","istWert","sollWert","toleranzUnten","toleranzOben","inToleranz","fotoUrl","kommentar","createdAt"]},"description":"Aelteste zuerst"}},"required":["id","pruefplanId","fertigungsauftragId","chargeId","lieferscheinId","geprueftVon","geprueftAm","status","begruendung","createdAt","updatedAt","ergebnisse"]},"example":{"id":"string","pruefplanId":"string","fertigungsauftragId":"string","chargeId":"string","lieferscheinId":"string","geprueftVon":"string","geprueftAm":"string","status":"string","begruendung":"string","createdAt":"string","updatedAt":"string","ergebnisse":[{"id":"string","pruefungId":"string","pruefmerkmalId":"string","istWert":0,"sollWert":0,"toleranzUnten":0,"toleranzOben":0,"inToleranz":true,"fotoUrl":"string","kommentar":"string","createdAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"getApiV1QmPruefungenById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest eine Pruefung samt ALLER erfassten Merkmal-Ergebnisse, aelteste zuerst. Die Ergebnisse stehen unter `ergebnisse` neben den Feldern der Pruefung, nicht in einer eigenen Huelle. Sie tragen den gemessenen Wert, den zum Erfassungszeitpunkt gueltigen Sollwert samt Toleranzband und das Urteil `inToleranz`. Ab Rolle `user`.","summary":"Liest eine Pruefung samt ALLER erfassten Merkmal-Ergebnisse, aelteste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/qm/pruefungen/{id}/ergebnis":{"post":{"responses":{"200":{"description":"Das erfasste bzw. ueberschriebene Ergebnis.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"pruefungId":{"type":"string"},"pruefmerkmalId":{"type":"string"},"istWert":{"type":["number","null"],"description":"Der gemessene Wert"},"sollWert":{"type":["number","null"],"description":"Aus dem Pruefmerkmal uebernommen"},"toleranzUnten":{"type":["number","null"]},"toleranzOben":{"type":["number","null"]},"inToleranz":{"type":"boolean","description":"Das Urteil — siehe Beschreibung des Aufrufs"},"fotoUrl":{"type":["string","null"]},"kommentar":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","pruefungId","pruefmerkmalId","istWert","sollWert","toleranzUnten","toleranzOben","inToleranz","fotoUrl","kommentar","createdAt"]},"example":{"id":"string","pruefungId":"string","pruefmerkmalId":"string","istWert":0,"sollWert":0,"toleranzUnten":0,"toleranzOben":0,"inToleranz":true,"fotoUrl":"string","kommentar":"string","createdAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Pruefung oder Pruefmerkmal existiert nicht"},"422":{"description":"Die Pruefung ist bereits abgeschlossen (`pruefung_locked`)"}},"operationId":"postApiV1QmPruefungenByIdErgebnis","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Erfasst das Ergebnis EINES Pruefmerkmals. Je Paar aus Pruefung und Merkmal gibt es genau eine Zeile: ein zweiter Aufruf ueberschreibt Wert, Sollwert, Toleranzen und Urteil — `foto_url` und `kommentar` bleiben dabei erhalten, wenn der neue Aufruf sie weglaesst. Sollwert und Toleranzband kommen aus dem Pruefmerkmal und werden mitgeschrieben, damit ein spaeterer Stammdatenwechsel das alte Urteil nicht verfaelscht. Das Urteil `inToleranz` entsteht so: ein ausdrueckliches `in_toleranz` im Rumpf gewinnt immer (das ist der einzige Weg, eine Sichtpruefung als „nicht in Ordnung\" festzuhalten); sonst wird `ist_wert` gegen das Band gerechnet; ohne Wert UND ohne Band gilt das Merkmal als bestanden. Erfassen geht nur, solange die Pruefung `offen` ist (sonst 422). Ab Rolle `user`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"pruefmerkmal_id":{"type":"string","minLength":1},"ist_wert":{"type":["number","null"]},"in_toleranz":{"type":"boolean"},"foto_url":{"type":"string","format":"uri"},"kommentar":{"type":"string","maxLength":2000}},"required":["pruefmerkmal_id"]},"example":{"pruefmerkmal_id":"string","ist_wert":0,"in_toleranz":true,"foto_url":"https://example.com","kommentar":"string"}}}},"summary":"Erfasst das Ergebnis EINES Pruefmerkmals","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/qm/pruefungen/{id}/abschluss":{"post":{"responses":{"200":{"description":"Die abgeschlossene Pruefung, um `ncrId` erweitert.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"pruefplanId":{"type":"string"},"fertigungsauftragId":{"type":["string","null"]},"chargeId":{"type":["string","null"]},"lieferscheinId":{"type":["string","null"]},"geprueftVon":{"type":["string","null"],"description":"Nutzer, der die Pruefung anlegte"},"geprueftAm":{"type":["string","null"],"description":"Gesetzt erst beim Abschluss"},"status":{"type":"string","description":"offen | bestanden | nacharbeit | ausschuss"},"begruendung":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"ncrId":{"type":["string","null"],"description":"Id der automatisch erzeugten Abweichung; null bei `bestanden`"}},"required":["id","pruefplanId","fertigungsauftragId","chargeId","lieferscheinId","geprueftVon","geprueftAm","status","begruendung","createdAt","updatedAt","ncrId"]},"example":{"id":"string","pruefplanId":"string","fertigungsauftragId":"string","chargeId":"string","lieferscheinId":"string","geprueftVon":"string","geprueftAm":"string","status":"string","begruendung":"string","createdAt":"string","updatedAt":"string","ncrId":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"},"422":{"description":"Bereits abgeschlossen, Pflicht-Merkmale ohne Ergebnis (`missing` nennt sie) oder Ausschuss ohne Foto."}},"operationId":"postApiV1QmPruefungenByIdAbschluss","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Pruefung abschliessen — Nacharbeit und Ausschuss erzeugen eine Abweichung","description":"Schliesst eine offene Pruefung mit `bestanden`, `nacharbeit` oder `ausschuss` ab und setzt dabei den Pruefzeitpunkt. Vorher wird geprueft: alle Merkmale des Pruefplans muessen ein Ergebnis haben (sonst 422 mit der Liste der fehlenden), und bei `ausschuss` muss mindestens ein Ergebnis ein Foto tragen (sonst 422). Bei `nacharbeit` und `ausschuss` entsteht automatisch eine Abweichung — `minor` bzw. `critical` — deren Id als `ncrId` zurueckkommt; sie und der Statuswechsel landen in EINER Transaktion, es gibt also keinen abgeschlossenen Beleg ohne Abweichung. Ein zweiter Aufruf auf dieselbe Pruefung ergibt 422 statt einer zweiten Abweichung. Ab Rolle `user`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["bestanden","nacharbeit","ausschuss"]},"begruendung":{"type":"string","maxLength":2000}},"required":["status"]},"example":{"status":"bestanden","begruendung":"string"}}}}}},"/api/v1/qm/dashboard":{"get":{"responses":{"200":{"description":"Die Kennzahlen und der Tagesverlauf der letzten 90 Tage.","content":{"application/json":{"schema":{"type":"object","properties":{"period_days":{"type":"number","const":90},"total_pruefungen":{"type":"integer"},"offen":{"type":"integer"},"bestanden":{"type":"integer"},"nacharbeit":{"type":"integer"},"ausschuss":{"type":"integer"},"ppm":{"type":"integer","description":"Ausschuss je Million, gerundet; 0 ohne Pruefungen"},"oee_quality_pct":{"type":"number","description":"bestanden / abgeschlossen, in Prozent"},"reklamationsrate_pct":{"type":"number","description":"RMA-Faelle / abgeschlossene Pruefungen, in Prozent"},"ncr_count":{"type":"integer"},"trend":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","description":"YYYY-MM-DD"},"bestanden":{"type":"integer"},"nacharbeit":{"type":"integer"},"ausschuss":{"type":"integer"},"offen":{"type":"integer"}},"required":["date","bestanden","nacharbeit","ausschuss","offen"]},"description":"Nur Tage MIT Pruefungen — Luecken sind keine Nullen"},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"]}},"required":["period_days","total_pruefungen","offen","bestanden","nacharbeit","ausschuss","ppm","oee_quality_pct","reklamationsrate_pct","ncr_count","trend","meta"]},"example":{"period_days":90,"total_pruefungen":0,"offen":0,"bestanden":0,"nacharbeit":0,"ausschuss":0,"ppm":0,"oee_quality_pct":0,"reklamationsrate_pct":0,"ncr_count":0,"trend":[{"date":"string","bestanden":0,"nacharbeit":0,"ausschuss":0,"offen":0}],"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1QmDashboard","tags":["QM"],"parameters":[],"description":"Kennzahlen ueber die letzten 90 Tage — der Zeitraum ist fest, es gibt keine Parameter. `ppm` ist der Ausschussanteil je Million ueber ALLE Pruefungen des Zeitraums, offene eingeschlossen. `oee_quality_pct` und `reklamationsrate_pct` rechnen dagegen nur gegen die ABGESCHLOSSENEN. Die Reklamationsrate ist ein Ersatzmass: sie zaehlt RMA-Faelle gegen abgeschlossene Pruefungen; fehlt die RMA-Tabelle im Mandanten, ist sie 0 statt eines Fehlers. Der Trend enthaelt nur Tage MIT Pruefungen — eine Luecke ist kein Nullwert. Ab Rolle `user`.","summary":"Kennzahlen ueber die letzten 90 Tage","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/qm/nonconformance":{"get":{"responses":{"200":{"description":"Die gefilterten Abweichungen samt Blaetterung.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"pruefungId":{"type":["string","null"]},"chargeId":{"type":["string","null"]},"fertigungsauftragId":{"type":["string","null"]},"lieferscheinId":{"type":["string","null"]},"artikelId":{"type":["string","null"]},"severity":{"type":"string","description":"minor | major | critical"},"beschreibung":{"type":["string","null"]},"fotoUrl":{"type":["string","null"]},"createdBy":{"type":["string","null"]},"resolvedAt":{"type":["string","null"],"description":"Null, solange die Abweichung offen ist"},"resolvedBy":{"type":["string","null"]},"resolutionNotes":{"type":["string","null"]},"capaId":{"type":["string","null"],"description":"Verknuepfte Massnahme, falls eine angelegt wurde"},"quelle":{"type":"string","description":"Woher die Abweichung stammt, Vorgabe `pruefung`"},"createdAt":{"type":"string"}},"required":["id","pruefungId","chargeId","fertigungsauftragId","lieferscheinId","artikelId","severity","beschreibung","fotoUrl","createdBy","resolvedAt","resolvedBy","resolutionNotes","capaId","quelle","createdAt"]}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer","description":"Treffer OHNE Limit — die ganze Filtermenge"}},"required":["limit","offset","total"]}},"required":["data","pagination"]},"example":{"data":[{"id":"string","pruefungId":"string","chargeId":"string","fertigungsauftragId":"string","lieferscheinId":"string","artikelId":"string","severity":"string","beschreibung":"string","fotoUrl":"string","createdBy":"string","resolvedAt":"string","resolvedBy":"string","resolutionNotes":"string","capaId":"string","quelle":"string","createdAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1QmNonconformance","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"severity","schema":{"type":"string","enum":["minor","major","critical"]}},{"in":"query","name":"resolved","schema":{"type":"string","enum":["true","false","1","0","yes","no","on","off"]}}],"description":"Listet die Abweichungen (Non-Conformance-Reports) des Mandanten, neueste zuerst. Die meisten entstehen automatisch beim Abschluss einer Pruefung mit `nacharbeit` oder `ausschuss`. `severity` grenzt auf `minor`, `major` oder `critical` ein, `resolved` auf erledigte bzw. offene. Geblaettert wird ueber `limit` (1..200, Vorgabe 50) und `offset`; `pagination.total` zaehlt die Treffer OHNE Limit. Ab Rolle `user`.","summary":"Listet die Abweichungen (Non-Conformance-Reports) des Mandanten, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/qm/capa":{"post":{"responses":{"201":{"description":"Die angelegte CAPA, flach — ohne Hülle.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"capaNr":{"type":"string","description":"Jahres-Zähler im Format CAPA-YYYY-NNNN."},"sourceTyp":{"type":"string","enum":["reklamation","pruefung","audit"]},"sourceId":{"type":["string","null"],"description":"Kennung des auslösenden Satzes (Reklamation, Prüfung, Audit). Frei gesetzt, nicht per Fremdschlüssel geprüft."},"ursache":{"type":["string","null"]},"massnahmeTyp":{"type":"string","enum":["korrektiv","praeventiv"]},"massnahmeBeschreibung":{"type":["string","null"]},"verantwortlichMitarbeiterId":{"type":["string","null"]},"faelligAm":{"type":["string","null"]},"status":{"type":"string","enum":["identifiziert","zugewiesen","umgesetzt","wirksamkeit_geprueft","abgeschlossen"]},"wirksamkeitGeprueftAm":{"type":["string","null"],"description":"Wird von `/verify` auf die Serverzeit gesetzt; vorher null."},"wirksamkeitErgebnis":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdBy":{"type":["string","null"],"description":"Benutzerkennung aus der Sitzung; null, wenn keine im Kontext lag."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","capaNr","sourceTyp","sourceId","ursache","massnahmeTyp","massnahmeBeschreibung","verantwortlichMitarbeiterId","faelligAm","status","wirksamkeitGeprueftAm","wirksamkeitErgebnis","notes","createdBy","createdAt","updatedAt"]},"example":{"id":"string","capaNr":"string","sourceTyp":"reklamation","sourceId":"string","ursache":"string","massnahmeTyp":"korrektiv","massnahmeBeschreibung":"string","verantwortlichMitarbeiterId":"string","faelligAm":"string","status":"identifiziert","wirksamkeitGeprueftAm":"string","wirksamkeitErgebnis":"string","notes":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Der Rumpf hält das Schema nicht ein."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"`database_unavailable` — Datenbank nicht erreichbar."}},"operationId":"postApiV1QmCapa","tags":["QM"],"parameters":[],"summary":"Neue CAPA anlegen","description":"Legt einen Satz in `qm_capa` des Mandanten-Schemas an. Die Tabelle wird\nbeim ersten Aufruf selbst angelegt; eine Migration ist nicht nötig.\n\nDer Anfangsstatus hängt vom Rumpf ab: mit `verantwortlich_mitarbeiter_id`\nstartet die CAPA bereits auf `zugewiesen`, ohne auf `identifiziert`. Das\nhat Folgen — `POST /capa/{id}/assign` verlangt `identifiziert` und\nantwortet auf eine so angelegte CAPA mit 422. Der nächste mögliche\nSchritt ist dann `/complete`.\n\nDie CAPA-Nummer vergibt der Server: `CAPA-JAHR-NNNN`, gezählt über die\nvorhandenen Sätze des laufenden Jahres. Sie wird nicht aus einer\nDatenbank-Sequenz gezogen und ist im Rumpf nicht überschreibbar.\n\n`created_by` kommt aus der Sitzung und ist null, wenn keine Kennung im\nKontext liegt. Zusätzlich wird ein Eintrag `qm.capa.created` in\n`activity_log` geschrieben — nach bestem Bemühen: schlägt das fehl, wird\nes nur protokolliert und die CAPA gilt trotzdem als angelegt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"source_typ":{"type":"string","enum":["reklamation","pruefung","audit"]},"source_id":{"type":"string","minLength":1},"ursache":{"type":"string","maxLength":4000},"massnahme_typ":{"type":"string","enum":["korrektiv","praeventiv"]},"massnahme_beschreibung":{"type":"string","maxLength":4000},"verantwortlich_mitarbeiter_id":{"type":"string","minLength":1},"faellig_am":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["source_typ","massnahme_typ"]},"example":{"source_typ":"reklamation","source_id":"string","ursache":"string","massnahme_typ":"korrektiv","massnahme_beschreibung":"string","verantwortlich_mitarbeiter_id":"string","faellig_am":"2026-01-01"}}}}},"get":{"responses":{"200":{"description":"Seite der Treffer plus Gesamtzahl.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"capaNr":{"type":"string","description":"Jahres-Zähler im Format CAPA-YYYY-NNNN."},"sourceTyp":{"type":"string","enum":["reklamation","pruefung","audit"]},"sourceId":{"type":["string","null"],"description":"Kennung des auslösenden Satzes (Reklamation, Prüfung, Audit). Frei gesetzt, nicht per Fremdschlüssel geprüft."},"ursache":{"type":["string","null"]},"massnahmeTyp":{"type":"string","enum":["korrektiv","praeventiv"]},"massnahmeBeschreibung":{"type":["string","null"]},"verantwortlichMitarbeiterId":{"type":["string","null"]},"faelligAm":{"type":["string","null"]},"status":{"type":"string","enum":["identifiziert","zugewiesen","umgesetzt","wirksamkeit_geprueft","abgeschlossen"]},"wirksamkeitGeprueftAm":{"type":["string","null"],"description":"Wird von `/verify` auf die Serverzeit gesetzt; vorher null."},"wirksamkeitErgebnis":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdBy":{"type":["string","null"],"description":"Benutzerkennung aus der Sitzung; null, wenn keine im Kontext lag."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","capaNr","sourceTyp","sourceId","ursache","massnahmeTyp","massnahmeBeschreibung","verantwortlichMitarbeiterId","faelligAm","status","wirksamkeitGeprueftAm","wirksamkeitErgebnis","notes","createdBy","createdAt","updatedAt"]}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer","description":"Treffer aller gesetzten Filter, ohne limit/offset."}},"required":["limit","offset","total"]}},"required":["data","pagination"]},"example":{"data":[{"id":"string","capaNr":"string","sourceTyp":"reklamation","sourceId":"string","ursache":"string","massnahmeTyp":"korrektiv","massnahmeBeschreibung":"string","verantwortlichMitarbeiterId":"string","faelligAm":"string","status":"identifiziert","wirksamkeitGeprueftAm":"string","wirksamkeitErgebnis":"string","notes":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"`limit` über 200, `offset` negativ oder unbekannter Filterwert."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"`database_unavailable` — Datenbank nicht erreichbar."}},"operationId":"getApiV1QmCapa","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["identifiziert","zugewiesen","umgesetzt","wirksamkeit_geprueft","abgeschlossen"]}},{"in":"query","name":"source_typ","schema":{"type":"string","enum":["reklamation","pruefung","audit"]}},{"in":"query","name":"verantwortlich","schema":{"type":"string"}}],"summary":"CAPA-Liste mit Filter (status, source_typ, verantwortlich)","description":"Blättert über `qm_capa` des Mandanten, neueste zuerst (`created_at`\nabsteigend). `limit` steht ohne Angabe auf 50 und ist bei 200 gedeckelt,\n`offset` auf 0.\n\nAlle drei Filter sind freiwillig und werden mit UND verknüpft:\n`verantwortlich` vergleicht `verantwortlich_mitarbeiter_id` exakt, es ist\nkeine Suche. Es gibt keinen Zeitraum- und keinen Textfilter.\n\nDie Liste enthält AUCH abgeschlossene CAPAs. Die Tabelle kennt kein\n`deleted_at` und die Routen löschen nichts — ein einmal angelegter Satz\nbleibt dauerhaft in der Liste, nur der Status wandert.\n\n`pagination.total` zählt die Treffer derselben Filter ohne\n`limit`/`offset`."}},"/api/v1/qm/capa/{id}":{"get":{"responses":{"200":{"description":"Der CAPA-Satz, flach — ohne Hülle.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"capaNr":{"type":"string","description":"Jahres-Zähler im Format CAPA-YYYY-NNNN."},"sourceTyp":{"type":"string","enum":["reklamation","pruefung","audit"]},"sourceId":{"type":["string","null"],"description":"Kennung des auslösenden Satzes (Reklamation, Prüfung, Audit). Frei gesetzt, nicht per Fremdschlüssel geprüft."},"ursache":{"type":["string","null"]},"massnahmeTyp":{"type":"string","enum":["korrektiv","praeventiv"]},"massnahmeBeschreibung":{"type":["string","null"]},"verantwortlichMitarbeiterId":{"type":["string","null"]},"faelligAm":{"type":["string","null"]},"status":{"type":"string","enum":["identifiziert","zugewiesen","umgesetzt","wirksamkeit_geprueft","abgeschlossen"]},"wirksamkeitGeprueftAm":{"type":["string","null"],"description":"Wird von `/verify` auf die Serverzeit gesetzt; vorher null."},"wirksamkeitErgebnis":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdBy":{"type":["string","null"],"description":"Benutzerkennung aus der Sitzung; null, wenn keine im Kontext lag."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","capaNr","sourceTyp","sourceId","ursache","massnahmeTyp","massnahmeBeschreibung","verantwortlichMitarbeiterId","faelligAm","status","wirksamkeitGeprueftAm","wirksamkeitErgebnis","notes","createdBy","createdAt","updatedAt"]},"example":{"id":"string","capaNr":"string","sourceTyp":"reklamation","sourceId":"string","ursache":"string","massnahmeTyp":"korrektiv","massnahmeBeschreibung":"string","verantwortlichMitarbeiterId":"string","faelligAm":"string","status":"identifiziert","wirksamkeitGeprueftAm":"string","wirksamkeitErgebnis":"string","notes":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"`not_found` — keine CAPA mit dieser Kennung im Mandanten."},"503":{"description":"`database_unavailable` — Datenbank nicht erreichbar."}},"operationId":"getApiV1QmCapaById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"CAPA-Detail","description":"Liest einen einzelnen Satz über seine Kennung. Die Antwort ist dieselbe\nflache Form wie beim Anlegen und bei den Statuswechseln — nicht in `data`\ngehüllt, kein Verlauf und keine Verweise auf den auslösenden Satz.\n\nEs gibt keinen Zugriff über die CAPA-Nummer; gebunden wird nur `id`.\n\nDer Status im Feld `status` sagt zugleich, welcher Übergang als nächster\nzulässig ist: `identifiziert` → `/assign`, `zugewiesen` → `/complete`,\n`umgesetzt` → `/verify`, `wirksamkeit_geprueft` → `/close`. Bei\n`abgeschlossen` ist keiner mehr möglich."}},"/api/v1/qm/capa/{id}/assign":{"post":{"responses":{"200":{"description":"Die CAPA nach dem Übergang, flach — ohne Hülle.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"capaNr":{"type":"string","description":"Jahres-Zähler im Format CAPA-YYYY-NNNN."},"sourceTyp":{"type":"string","enum":["reklamation","pruefung","audit"]},"sourceId":{"type":["string","null"],"description":"Kennung des auslösenden Satzes (Reklamation, Prüfung, Audit). Frei gesetzt, nicht per Fremdschlüssel geprüft."},"ursache":{"type":["string","null"]},"massnahmeTyp":{"type":"string","enum":["korrektiv","praeventiv"]},"massnahmeBeschreibung":{"type":["string","null"]},"verantwortlichMitarbeiterId":{"type":["string","null"]},"faelligAm":{"type":["string","null"]},"status":{"type":"string","enum":["identifiziert","zugewiesen","umgesetzt","wirksamkeit_geprueft","abgeschlossen"]},"wirksamkeitGeprueftAm":{"type":["string","null"],"description":"Wird von `/verify` auf die Serverzeit gesetzt; vorher null."},"wirksamkeitErgebnis":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdBy":{"type":["string","null"],"description":"Benutzerkennung aus der Sitzung; null, wenn keine im Kontext lag."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","capaNr","sourceTyp","sourceId","ursache","massnahmeTyp","massnahmeBeschreibung","verantwortlichMitarbeiterId","faelligAm","status","wirksamkeitGeprueftAm","wirksamkeitErgebnis","notes","createdBy","createdAt","updatedAt"]},"example":{"id":"string","capaNr":"string","sourceTyp":"reklamation","sourceId":"string","ursache":"string","massnahmeTyp":"korrektiv","massnahmeBeschreibung":"string","verantwortlichMitarbeiterId":"string","faelligAm":"string","status":"identifiziert","wirksamkeitGeprueftAm":"string","wirksamkeitErgebnis":"string","notes":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"`mitarbeiter_id` fehlt oder `faellig_am` ist nicht JJJJ-MM-TT."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"`not_found` — keine CAPA mit dieser Kennung im Mandanten."},"422":{"description":"`invalid_transition` — der Satz steht nicht auf `identifiziert`."},"503":{"description":"`database_unavailable` — Datenbank nicht erreichbar."}},"operationId":"postApiV1QmCapaByIdAssign","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"CAPA zuweisen (Status=zugewiesen)","description":"Setzt zuständigen Mitarbeiter und Fälligkeitsdatum — beide sind PFLICHT —\nund hebt den Status auf `zugewiesen`.\n\nVORZUSTAND: nur `identifiziert`. Jeder andere Status ergibt 422 mit\n`{ error: \"invalid_transition\", current, next }`; `current` nennt den\ntatsächlichen Status. Die Kette ist streng sequenziell, es gibt keinen\nSprung und keinen Weg zurück.\n\nDeshalb schlägt diese Route bei einer CAPA fehl, die schon MIT\n`verantwortlich_mitarbeiter_id` angelegt wurde: die startet auf\n`zugewiesen`. Ein Wechsel des Zuständigen im Nachhinein ist über die\nCAPA-Routen nicht vorgesehen.\n\nGeschrieben wird zusätzlich `updated_at`, und ein Eintrag\n`qm.capa.assigned` landet nach bestem Bemühen im `activity_log`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mitarbeiter_id":{"type":"string","minLength":1},"faellig_am":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["mitarbeiter_id","faellig_am"]},"example":{"mitarbeiter_id":"string","faellig_am":"2026-01-01"}}}}}},"/api/v1/qm/capa/{id}/complete":{"post":{"responses":{"200":{"description":"Die CAPA nach dem Übergang, flach — ohne Hülle.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"capaNr":{"type":"string","description":"Jahres-Zähler im Format CAPA-YYYY-NNNN."},"sourceTyp":{"type":"string","enum":["reklamation","pruefung","audit"]},"sourceId":{"type":["string","null"],"description":"Kennung des auslösenden Satzes (Reklamation, Prüfung, Audit). Frei gesetzt, nicht per Fremdschlüssel geprüft."},"ursache":{"type":["string","null"]},"massnahmeTyp":{"type":"string","enum":["korrektiv","praeventiv"]},"massnahmeBeschreibung":{"type":["string","null"]},"verantwortlichMitarbeiterId":{"type":["string","null"]},"faelligAm":{"type":["string","null"]},"status":{"type":"string","enum":["identifiziert","zugewiesen","umgesetzt","wirksamkeit_geprueft","abgeschlossen"]},"wirksamkeitGeprueftAm":{"type":["string","null"],"description":"Wird von `/verify` auf die Serverzeit gesetzt; vorher null."},"wirksamkeitErgebnis":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdBy":{"type":["string","null"],"description":"Benutzerkennung aus der Sitzung; null, wenn keine im Kontext lag."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","capaNr","sourceTyp","sourceId","ursache","massnahmeTyp","massnahmeBeschreibung","verantwortlichMitarbeiterId","faelligAm","status","wirksamkeitGeprueftAm","wirksamkeitErgebnis","notes","createdBy","createdAt","updatedAt"]},"example":{"id":"string","capaNr":"string","sourceTyp":"reklamation","sourceId":"string","ursache":"string","massnahmeTyp":"korrektiv","massnahmeBeschreibung":"string","verantwortlichMitarbeiterId":"string","faelligAm":"string","status":"identifiziert","wirksamkeitGeprueftAm":"string","wirksamkeitErgebnis":"string","notes":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"`not_found` — keine CAPA mit dieser Kennung im Mandanten."},"422":{"description":"`invalid_transition` — der Satz steht nicht auf `zugewiesen`."},"503":{"description":"`database_unavailable` — Datenbank nicht erreichbar."}},"operationId":"postApiV1QmCapaByIdComplete","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"CAPA umsetzen (Status=umgesetzt)","description":"Meldet die Maßnahme als durchgeführt und hebt den Status auf `umgesetzt`.\n\nVORZUSTAND: nur `zugewiesen`. Sonst 422 mit\n`{ error: \"invalid_transition\", current, next }`.\n\n`notes` ist freiwillig und wird nur geschrieben, wenn das Feld im Rumpf\nsteht — ein leerer Rumpf `{}` ist erlaubt und wechselt nur den Status.\nEin übergebener Text ERSETZT die bisherige Notiz, er wird nicht angehängt;\ndasselbe Feld beschreibt später auch `/close`.\n\nWas tatsächlich getan wurde, prüft niemand — es gibt kein Pflichtfeld für\ndie Umsetzung. Der Nachweis der Wirksamkeit folgt erst in `/verify`.\n\nEin Eintrag `qm.capa.completed` landet nach bestem Bemühen im\n`activity_log`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string","maxLength":4000}}},"example":{"notes":"string"}}}}}},"/api/v1/qm/capa/{id}/verify":{"post":{"responses":{"200":{"description":"Die CAPA nach dem Übergang, flach — ohne Hülle.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"capaNr":{"type":"string","description":"Jahres-Zähler im Format CAPA-YYYY-NNNN."},"sourceTyp":{"type":"string","enum":["reklamation","pruefung","audit"]},"sourceId":{"type":["string","null"],"description":"Kennung des auslösenden Satzes (Reklamation, Prüfung, Audit). Frei gesetzt, nicht per Fremdschlüssel geprüft."},"ursache":{"type":["string","null"]},"massnahmeTyp":{"type":"string","enum":["korrektiv","praeventiv"]},"massnahmeBeschreibung":{"type":["string","null"]},"verantwortlichMitarbeiterId":{"type":["string","null"]},"faelligAm":{"type":["string","null"]},"status":{"type":"string","enum":["identifiziert","zugewiesen","umgesetzt","wirksamkeit_geprueft","abgeschlossen"]},"wirksamkeitGeprueftAm":{"type":["string","null"],"description":"Wird von `/verify` auf die Serverzeit gesetzt; vorher null."},"wirksamkeitErgebnis":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdBy":{"type":["string","null"],"description":"Benutzerkennung aus der Sitzung; null, wenn keine im Kontext lag."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","capaNr","sourceTyp","sourceId","ursache","massnahmeTyp","massnahmeBeschreibung","verantwortlichMitarbeiterId","faelligAm","status","wirksamkeitGeprueftAm","wirksamkeitErgebnis","notes","createdBy","createdAt","updatedAt"]},"example":{"id":"string","capaNr":"string","sourceTyp":"reklamation","sourceId":"string","ursache":"string","massnahmeTyp":"korrektiv","massnahmeBeschreibung":"string","verantwortlichMitarbeiterId":"string","faelligAm":"string","status":"identifiziert","wirksamkeitGeprueftAm":"string","wirksamkeitErgebnis":"string","notes":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"`wirksamkeit_ergebnis` fehlt, ist leer oder länger als 4000 Zeichen."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"`not_found` — keine CAPA mit dieser Kennung im Mandanten."},"422":{"description":"`invalid_transition` — der Satz steht nicht auf `umgesetzt`."},"503":{"description":"`database_unavailable` — Datenbank nicht erreichbar."}},"operationId":"postApiV1QmCapaByIdVerify","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"CAPA Wirksamkeit prüfen (Status=wirksamkeit_geprueft)","description":"Hält das Ergebnis der Wirksamkeitsprüfung fest und hebt den Status auf\n`wirksamkeit_geprueft`.\n\nVORZUSTAND: nur `umgesetzt`. Sonst 422 mit\n`{ error: \"invalid_transition\", current, next }`.\n\n`wirksamkeit_ergebnis` ist PFLICHT und ein freier Text — es gibt kein\nJa/Nein-Feld. Die Route bewertet den Text nicht: auch ein Ergebnis, das\ndie Maßnahme als unwirksam beschreibt, führt in denselben Status. Eine\nunwirksame CAPA lässt sich über diese Routen weder zurücksetzen noch neu\nzuweisen; dafür ist eine neue CAPA anzulegen.\n\n`wirksamkeit_geprueft_am` setzt der Server auf seine eigene Uhrzeit; ein\nabweichender Prüfzeitpunkt ist nicht übergebbar.\n\nEin Eintrag `qm.capa.verified` landet nach bestem Bemühen im\n`activity_log`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"wirksamkeit_ergebnis":{"type":"string","minLength":1,"maxLength":4000}},"required":["wirksamkeit_ergebnis"]},"example":{"wirksamkeit_ergebnis":"string"}}}}}},"/api/v1/qm/capa/{id}/close":{"post":{"responses":{"200":{"description":"Die CAPA nach dem Übergang, flach — ohne Hülle.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"capaNr":{"type":"string","description":"Jahres-Zähler im Format CAPA-YYYY-NNNN."},"sourceTyp":{"type":"string","enum":["reklamation","pruefung","audit"]},"sourceId":{"type":["string","null"],"description":"Kennung des auslösenden Satzes (Reklamation, Prüfung, Audit). Frei gesetzt, nicht per Fremdschlüssel geprüft."},"ursache":{"type":["string","null"]},"massnahmeTyp":{"type":"string","enum":["korrektiv","praeventiv"]},"massnahmeBeschreibung":{"type":["string","null"]},"verantwortlichMitarbeiterId":{"type":["string","null"]},"faelligAm":{"type":["string","null"]},"status":{"type":"string","enum":["identifiziert","zugewiesen","umgesetzt","wirksamkeit_geprueft","abgeschlossen"]},"wirksamkeitGeprueftAm":{"type":["string","null"],"description":"Wird von `/verify` auf die Serverzeit gesetzt; vorher null."},"wirksamkeitErgebnis":{"type":["string","null"]},"notes":{"type":["string","null"]},"createdBy":{"type":["string","null"],"description":"Benutzerkennung aus der Sitzung; null, wenn keine im Kontext lag."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","capaNr","sourceTyp","sourceId","ursache","massnahmeTyp","massnahmeBeschreibung","verantwortlichMitarbeiterId","faelligAm","status","wirksamkeitGeprueftAm","wirksamkeitErgebnis","notes","createdBy","createdAt","updatedAt"]},"example":{"id":"string","capaNr":"string","sourceTyp":"reklamation","sourceId":"string","ursache":"string","massnahmeTyp":"korrektiv","massnahmeBeschreibung":"string","verantwortlichMitarbeiterId":"string","faelligAm":"string","status":"identifiziert","wirksamkeitGeprueftAm":"string","wirksamkeitErgebnis":"string","notes":"string","createdBy":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"`not_found` — keine CAPA mit dieser Kennung im Mandanten."},"422":{"description":"`invalid_transition` — der Satz steht nicht auf `wirksamkeit_geprueft` (auch der zweite Abschluss)."},"503":{"description":"`database_unavailable` — Datenbank nicht erreichbar."}},"operationId":"postApiV1QmCapaByIdClose","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"CAPA abschließen (Status=abgeschlossen)","description":"Schließt den Vorgang ab und hebt den Status auf `abgeschlossen`.\n\nVORZUSTAND: nur `wirksamkeit_geprueft`. Sonst 422 mit\n`{ error: \"invalid_transition\", current, next }`. Der Abschluss ist damit\nohne vorherige Wirksamkeitsprüfung nicht erreichbar.\n\nDer Aufruf ist NICHT wiederholbar: `abgeschlossen` ist ein Endzustand\nohne ausgehenden Übergang, ein zweiter Aufruf ergibt 422.\n\n`notes` ist freiwillig und ersetzt eine bei `/complete` gesetzte Notiz,\nwenn das Feld im Rumpf steht. Ohne das Feld bleibt sie unverändert.\n\nDer Satz bleibt vollständig stehen und taucht weiter in `GET /capa` auf —\nabgeschlossen heißt nicht gelöscht oder ausgeblendet.\n\nEin Eintrag `qm.capa.closed` landet nach bestem Bemühen im `activity_log`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string","maxLength":4000}}},"example":{"notes":"string"}}}}}},"/api/v1/qm/spc":{"get":{"responses":{"200":{"description":"SPC-Daten","content":{"application/json":{"schema":{"type":"object","properties":{"pruefmerkmal_id":{"type":"string"},"from":{"type":["string","null"]},"to":{"type":["string","null"]},"measurement_count":{"type":"integer"},"points":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"value":{"type":"number"},"range":{"type":"number"},"timestamp":{"type":["string","null"]},"source_id":{"type":["string","null"]}},"required":["index","value","range","timestamp"]}},"mean":{"type":"number"},"std":{"type":"number"},"ucl":{"type":"number"},"lcl":{"type":"number"},"range_mean":{"type":"number"},"range_ucl":{"type":"number"},"violations":{"type":"array","items":{"type":"object","properties":{"point_index":{"type":"integer"},"rule":{"anyOf":[{"type":"number","const":1},{"type":"number","const":2},{"type":"number","const":3},{"type":"number","const":4}]},"severity":{"type":"string","enum":["minor","major","critical"]}},"required":["point_index","rule","severity"]}}},"required":["pruefmerkmal_id","from","to","measurement_count","points","mean","std","ucl","lcl","range_mean","range_ucl","violations"]},"example":{"pruefmerkmal_id":"string","from":"string","to":"string","measurement_count":0,"points":[{"index":0,"value":0,"range":0,"timestamp":"string","source_id":"string"}],"mean":0,"std":0,"ucl":0,"lcl":0,"range_mean":0,"range_ucl":0,"violations":[{"point_index":0,"rule":1,"severity":"minor"}]}}}},"400":{"description":"Abfrageparameter ungueltig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1QmSpc","tags":["QM"],"parameters":[{"in":"query","name":"pruefmerkmal","schema":{"type":"string","minLength":1},"required":true},{"in":"query","name":"from","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}(T.*)?$"},"required":false},{"in":"query","name":"to","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}(T.*)?$"},"required":false}],"summary":"SPC-Karte eines Pruefmerkmals mit Eingriffsgrenzen","description":"SPC-Karte für ein Prüfmerkmal: X-Chart-Punkte, UCL/LCL + Western-Electric-Violations. `pruefmerkmal` ist Pflicht; `from`/`to` grenzen den Zeitraum ueber das Anlagedatum ein, ohne Angabe zaehlt alles. Beruecksichtigt werden nur Ergebnisse MIT Istwert; je Pruefung entsteht eine Stichprobe, aus der Mittelwert und Spannweite gebildet werden — `measurement_count` zaehlt dagegen die Einzelmesswerte, ist also in der Regel groeszer als die Zahl der Punkte. UCL und LCL sind die NATUERLICHEN Grenzen aus den Daten (Mittelwert ± 3σ), nicht die Toleranz des Merkmals: ein Wert innerhalb der Grenzen ist nicht automatisch in der Spezifikation. Rein lesend, es wird nichts gespeichert; ab Rolle „user\"."}},"/api/v1/qm/audit":{"get":{"responses":{"200":{"description":"Eintraege der Seite plus Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Eintrags"},"entityTyp":{"type":"string","description":"Art des betroffenen Datensatzes"},"entityId":{"type":"string","description":"Kennung des betroffenen Datensatzes"},"action":{"type":"string","description":"Was geschehen ist"},"userId":{"type":["string","null"],"description":"Wer es ausgeloest hat; null bei Systemvorgaengen"},"altWert":{"description":"Stand VOR der Aenderung als JSON"},"neuWert":{"description":"Stand NACH der Aenderung als JSON"},"timestamp":{"type":"string","description":"Zeitpunkt des Vorgangs"},"signaturHash":{"type":"string","description":"Signatur des Eintrags — belegt, dass er nicht nachtraeglich geaendert wurde"}},"required":["id","entityTyp","entityId","action","userId","timestamp","signaturHash"]},"description":"Die Eintraege der Seite, neueste zuerst"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":1000,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Treffer der Filter"}},"required":["limit","offset","total"],"description":"Seitenangaben"}},"required":["data","pagination"]},"example":{"data":[{"id":"string","entityTyp":"string","entityId":"string","action":"string","userId":"string","timestamp":"string","signaturHash":"string"}],"pagination":{"limit":1,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1QmAudit","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":1000,"default":100}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"entity_typ","schema":{"type":"string"}},{"in":"query","name":"entity_id","schema":{"type":"string"}},{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}}],"summary":"QM-Audit-Protokoll blaettern und filtern","description":"Blaettert durch das anhaengende QM-Audit-Protokoll `qm_audit_log`, neueste zuerst. `entity_typ` und `entity_id` filtern exakt, `from` und `to` grenzen den Zeitpunkt ein (einschliesslich); `limit` (1-1000, Vorgabe 100) und `offset` blaettern, `total` zaehlt alle Treffer der Filter. Rein lesend — geschrieben wird ausschliesslich ueber `logQmAudit`, und Eintraege lassen sich weder aendern noch loeschen. Fehlt die Tabelle, wird sie beim Aufruf leer angelegt."}},"/api/v1/qm/audit/export":{"post":{"responses":{"200":{"description":"Der CSV-Text im JSON-Rumpf plus Zeilenzahl und verwendete Filter","content":{"application/json":{"schema":{"type":"object","properties":{"rowCount":{"type":"integer","minimum":0,"description":"Anzahl der ausgegebenen Zeilen ohne Kopfzeile"},"from":{"type":"string","description":"Uebergebene Untergrenze des Zeitraums"},"to":{"type":"string","description":"Uebergebene Obergrenze des Zeitraums"},"entity_typ_filter":{"type":["string","null"],"description":"Gesetzter Typfilter; null wenn keiner uebergeben wurde"},"csv":{"type":"string","description":"Der vollstaendige CSV-Text im JSON-Rumpf — kommagetrennt, mit Kopfzeile, aelteste zuerst"},"download_url":{"type":"null","description":"Immer null — ein Datei-Download existiert noch nicht, der Inhalt steht in `csv`"}},"required":["rowCount","from","to","entity_typ_filter","csv","download_url"]},"example":{"rowCount":0,"from":"string","to":"string","entity_typ_filter":"string","csv":"string","download_url":null}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Export fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1QmAuditExport","tags":["QM"],"parameters":[{"in":"query","name":"from","schema":{"type":"string","minLength":1},"required":true},{"in":"query","name":"to","schema":{"type":"string","minLength":1},"required":true},{"in":"query","name":"entity_typ_filter","schema":{"type":"string"},"required":false}],"summary":"QM-Audit-Protokoll als CSV-Text im JSON exportieren","description":"Erzeugt den CSV-Text fuer eine Inspektion und gibt ihn IM JSON-RUMPF unter `csv` zurueck — es kommt KEIN Dateidownload und kein `text/csv`. `download_url` ist immer null. Die Parameter stehen in der QUERY, nicht im Rumpf: `from` und `to` sind Pflicht, `entity_typ_filter` freiwillig. Der Export ist nicht begrenzt und nicht geblaettert — er umfasst ALLE Eintraege des Zeitraums, aelteste zuerst, mit Kopfzeile und kommagetrennt. Trotz POST wird nichts geschrieben. Erfordert mindestens die Rolle `manager` (das Lesen der Liste dagegen nur `user`)."}},"/api/v1/qm-prozesse/stats":{"get":{"responses":{"200":{"description":"Die sechs Kennzahlen ueber den gesamten Bestand","content":{"application/json":{"schema":{"type":"object","properties":{"pruefungenOffen":{"type":"integer","minimum":0,"description":"Pruefungen im Status open"},"pruefungenInProgress":{"type":"integer","minimum":0,"description":"Pruefungen im Status in_progress"},"pruefungenBestanden":{"type":"integer","minimum":0,"description":"Pruefungen im Status passed"},"pruefungenNichtBestanden":{"type":"integer","minimum":0,"description":"Pruefungen im Status failed"},"offeneRueckrufe":{"type":"integer","minimum":0,"description":"Rueckrufe, deren Status NICHT `abgeschlossen` lautet — jeder andere Freitext zaehlt als offen"},"ausgestellteZertifikate":{"type":"integer","minimum":0,"description":"Alle jemals ausgestellten Zertifikate"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, fuer den gezaehlt wurde"},"source":{"type":"string","const":"db","description":"Herkunft der Zahlen"}},"required":["tenantId","source"],"description":"Angaben zur Abfrage"}},"required":["pruefungenOffen","pruefungenInProgress","pruefungenBestanden","pruefungenNichtBestanden","offeneRueckrufe","ausgestellteZertifikate","meta"]},"example":{"pruefungenOffen":0,"pruefungenInProgress":0,"pruefungenBestanden":0,"pruefungenNichtBestanden":0,"offeneRueckrufe":0,"ausgestellteZertifikate":0,"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Qm-prozesseStats","tags":["QM"],"parameters":[],"summary":"Sechs QM-Kennzahlen ueber den gesamten Bestand","description":"Zaehlt sechs Kennzahlen mit je einer eigenen Abfrage ueber `qm_pruefungen`, `qm_rueckrufe` und `qm_zertifikate`. Es gibt KEINEN Zeitraum-Parameter — gezaehlt wird immer der gesamte Bestand. Als offener Rueckruf gilt jeder, dessen Status nicht genau `abgeschlossen` lautet; da der Status Freitext ist, zaehlt auch ein Tippfehler als offen. Fehlende Tabellen legt die Abfrage beim Aufruf leer an, es kommen dann Nullen."}},"/api/v1/qm-prozesse/pruefungen":{"get":{"responses":{"200":{"description":"Pruefungen der Seite plus Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Pruefung"},"prozessTyp":{"type":"string","description":"we, endpruefung, wa, retoure, erstmuster, stabilitaet oder rueckruf"},"belegTyp":{"type":["string","null"],"description":"Art des geprueften Belegs; null wenn keiner benannt ist"},"belegId":{"type":["string","null"],"description":"Kennung des geprueften Belegs; null wenn keiner benannt ist"},"pruefplanId":{"type":["string","null"],"description":"Zugrunde liegender Pruefplan; null wenn keiner benannt ist"},"prüferId":{"type":["string","null"],"description":"Wer prueft; null wenn niemand zugewiesen ist"},"status":{"type":"string","description":"open, in_progress, passed, failed oder conditional"},"ergebnis":{"type":"null","description":"Pruefergebnis als JSON; null solange keins erfasst ist"},"bemerkungen":{"type":["string","null"],"description":"Bemerkung; null wenn keine erfasst ist"},"startedAt":{"type":["string","null"],"description":"Beginn der Pruefung; null solange nicht begonnen"},"completedAt":{"type":["string","null"],"description":"Abschluss der Pruefung; null solange nicht abgeschlossen"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","prozessTyp","belegTyp","belegId","pruefplanId","prüferId","status","bemerkungen","startedAt","completedAt","createdAt","updatedAt"]},"description":"Die Pruefungen der Seite, neueste zuerst"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Treffer der Filter"}},"required":["limit","offset","total"],"description":"Seitenangaben"}},"required":["data","pagination"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","prozessTyp":"string","belegTyp":"string","belegId":"string","pruefplanId":"string","prüferId":"string","status":"string","ergebnis":null,"bemerkungen":"string","startedAt":"string","completedAt":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":1,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Qm-prozessePruefungen","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"prozessTyp","schema":{"type":"string"}}],"description":"Blaettert durch `qm_pruefungen` des Mandanten, neueste zuerst. `status` und `prozessTyp` filtern exakt; `limit` (1-200, Vorgabe 50) und `offset` blaettern, `total` zaehlt alle Treffer der Filter. Es gibt kein Soft-Delete: eine angelegte Pruefung bleibt in der Liste. Fehlende Tabellen legt der Aufruf leer an.","summary":"Blaettert durch `qm_pruefungen` des Mandanten, neueste zuerst","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Die angelegte Pruefung im Status open, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Pruefung"},"prozessTyp":{"type":"string","description":"we, endpruefung, wa, retoure, erstmuster, stabilitaet oder rueckruf"},"belegTyp":{"type":["string","null"],"description":"Art des geprueften Belegs; null wenn keiner benannt ist"},"belegId":{"type":["string","null"],"description":"Kennung des geprueften Belegs; null wenn keiner benannt ist"},"pruefplanId":{"type":["string","null"],"description":"Zugrunde liegender Pruefplan; null wenn keiner benannt ist"},"prüferId":{"type":["string","null"],"description":"Wer prueft; null wenn niemand zugewiesen ist"},"status":{"type":"string","description":"open, in_progress, passed, failed oder conditional"},"ergebnis":{"type":"null","description":"Pruefergebnis als JSON; null solange keins erfasst ist"},"bemerkungen":{"type":["string","null"],"description":"Bemerkung; null wenn keine erfasst ist"},"startedAt":{"type":["string","null"],"description":"Beginn der Pruefung; null solange nicht begonnen"},"completedAt":{"type":["string","null"],"description":"Abschluss der Pruefung; null solange nicht abgeschlossen"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","prozessTyp","belegTyp","belegId","pruefplanId","prüferId","status","bemerkungen","startedAt","completedAt","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","prozessTyp":"string","belegTyp":"string","belegId":"string","pruefplanId":"string","prüferId":"string","status":"string","ergebnis":null,"bemerkungen":"string","startedAt":"string","completedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Manager-Rolle erforderlich"},"503":{"description":"Anlegen fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Qm-prozessePruefungen","tags":["QM"],"parameters":[],"description":"Legt eine Pruefung an. Der Status ist immer `open` und laesst sich beim Anlegen nicht setzen; `started_at` und `completed_at` bleiben leer. Ob Beleg, Pruefplan und Pruefer existieren, wird NICHT geprueft. Die Antwort ist die Pruefung SELBST, ohne umschliessendes Feld. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"prozessTyp":{"type":"string","enum":["we","endpruefung","wa","retoure","erstmuster","stabilitaet","rueckruf"]},"belegTyp":{"type":"string","maxLength":50},"belegId":{"type":"string","format":"uuid"},"pruefplanId":{"type":"string","format":"uuid"},"prüferId":{"type":"string","format":"uuid"},"bemerkungen":{"type":"string"}},"required":["prozessTyp"]},"example":{"prozessTyp":"we","belegTyp":"string","belegId":"00000000-0000-4000-8000-000000000000","pruefplanId":"00000000-0000-4000-8000-000000000000","prüferId":"00000000-0000-4000-8000-000000000000","bemerkungen":"string"}}}},"summary":"Legt eine Pruefung an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/qm-prozesse/pruefungen/{id}":{"patch":{"responses":{"200":{"description":"Die Pruefung nach der Aenderung, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Pruefung"},"prozessTyp":{"type":"string","description":"we, endpruefung, wa, retoure, erstmuster, stabilitaet oder rueckruf"},"belegTyp":{"type":["string","null"],"description":"Art des geprueften Belegs; null wenn keiner benannt ist"},"belegId":{"type":["string","null"],"description":"Kennung des geprueften Belegs; null wenn keiner benannt ist"},"pruefplanId":{"type":["string","null"],"description":"Zugrunde liegender Pruefplan; null wenn keiner benannt ist"},"prüferId":{"type":["string","null"],"description":"Wer prueft; null wenn niemand zugewiesen ist"},"status":{"type":"string","description":"open, in_progress, passed, failed oder conditional"},"ergebnis":{"type":"null","description":"Pruefergebnis als JSON; null solange keins erfasst ist"},"bemerkungen":{"type":["string","null"],"description":"Bemerkung; null wenn keine erfasst ist"},"startedAt":{"type":["string","null"],"description":"Beginn der Pruefung; null solange nicht begonnen"},"completedAt":{"type":["string","null"],"description":"Abschluss der Pruefung; null solange nicht abgeschlossen"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","prozessTyp","belegTyp","belegId","pruefplanId","prüferId","status","bemerkungen","startedAt","completedAt","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","prozessTyp":"string","belegTyp":"string","belegId":"string","pruefplanId":"string","prüferId":"string","status":"string","ergebnis":null,"bemerkungen":"string","startedAt":"string","completedAt":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Manager-Rolle erforderlich"},"404":{"description":"Keine Pruefung mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Aenderung fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"patchApiV1Qm-prozessePruefungenById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aendert Status, Ergebnis, Bemerkung sowie Beginn und Abschluss einer Pruefung. Jedes Feld ist freiwillig; nicht mitgegebene behalten ihren Wert, weil der Handler den aktuellen Stand vorher liest. `ergebnis` wird dabei ERSETZT, nicht zusammengefuehrt. `prozessTyp`, Beleg und Pruefplan lassen sich hier NICHT aendern. Statuswechsel sind frei — es gibt keine Reihenfolge, und ein Wechsel auf `passed` setzt `completed_at` nicht selbsttaetig. Lesen und Schreiben laufen ohne Transaktion. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","in_progress","passed","failed","conditional"]},"ergebnis":{"type":"object","additionalProperties":{}},"bemerkungen":{"type":"string"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"}}},"example":{"status":"open","ergebnis":{},"bemerkungen":"string","startedAt":"2026-01-01T12:00:00.000Z","completedAt":"2026-01-01T12:00:00.000Z"}}}},"summary":"Aendert Status, Ergebnis, Bemerkung sowie Beginn und Abschluss einer Pruefung","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/qm-prozesse/zertifikate/{pruefungId}/generate":{"post":{"responses":{"201":{"description":"Das ausgestellte Zertifikat, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Zertifikats"},"pruefungId":{"type":"string","description":"Die zugrunde liegende Pruefung"},"zertifikatNummer":{"type":"string","description":"Vergebene Nummer im Format CERT-JJJJMMTT-XXXXX; der letzte Teil ist zufaellig, nicht fortlaufend"},"ausgestelltAm":{"type":"string","description":"Ausstellungsdatum; ohne Angabe der heutige Tag"},"gueltigBis":{"type":["string","null"],"description":"Ablaufdatum; null wenn keins uebergeben wurde"},"pdfUrl":{"type":["string","null"],"description":"Adresse des Belegs; null wenn keine uebergeben wurde — die Route erzeugt KEIN PDF"},"customerId":{"type":["string","null"],"description":"Kunde, fuer den es ausgestellt ist; null wenn keiner benannt ist"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"}},"required":["id","pruefungId","zertifikatNummer","ausgestelltAm","gueltigBis","pdfUrl","customerId","createdAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","pruefungId":"string","zertifikatNummer":"string","ausgestelltAm":"string","gueltigBis":"string","pdfUrl":"string","customerId":"string","createdAt":"string"}}}},"400":{"description":"Die Pruefung ist weder passed noch conditional","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"pruefung_not_passed"},"message":{"type":"string","description":"Klartext-Begruendung"}},"required":["error","message"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Manager-Rolle erforderlich"},"404":{"description":"`pruefung_not_found`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Ausstellen fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Qm-prozesseZertifikateByPruefungIdGenerate","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"pruefungId","required":true}],"description":"Stellt ein Zertifikat zu einer Pruefung aus. Zulaessig NUR, wenn die Pruefung `passed` oder `conditional` ist — sonst 400 mit `pruefung_not_passed`. Die Nummer wird als `CERT-JJJJMMTT-XXXXX` vergeben, der letzte Teil ist zufaellig und NICHT fortlaufend; sie ist mandantenweit eindeutig, ein Zusammenstoss laesst den Aufruf scheitern. Es wird KEIN PDF erzeugt: `pdfUrl` ist eine Angabe des Aufrufers und bleibt sonst leer. Je Pruefung lassen sich MEHRERE Zertifikate ausstellen — das wird nicht verhindert. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"gueltigBis":{"type":"string","format":"date"},"pdfUrl":{"type":"string","format":"uri"},"customerId":{"type":"string","format":"uuid"}}},"example":{"gueltigBis":"2026-01-01","pdfUrl":"https://example.com","customerId":"00000000-0000-4000-8000-000000000000"}}}},"summary":"Stellt ein Zertifikat zu einer Pruefung aus","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/qm-prozesse/rueckrufe":{"get":{"responses":{"200":{"description":"Rueckrufe der Seite plus Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Rueckrufs"},"batchNummer":{"type":"string","description":"Betroffene Charge"},"artikelId":{"type":["string","null"],"description":"Betroffener Artikel; null wenn keiner benannt ist"},"grund":{"type":["string","null"],"description":"Grund des Rueckrufs; null wenn keiner erfasst ist"},"status":{"type":"string","description":"Freitext-Status; beim Anlegen `offen`. Es gibt KEINE feste Werteliste"},"erstelltAm":{"type":"string","description":"Zeitpunkt der Anlage"},"abgeschlossenAm":{"type":["string","null"],"description":"Abschlusszeitpunkt; null solange offen"},"anzahlBetroffen":{"type":["number","null"],"description":"Anzahl betroffener Einheiten; null wenn nicht erfasst"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","batchNummer","artikelId","grund","status","erstelltAm","abgeschlossenAm","anzahlBetroffen","createdAt","updatedAt"]},"description":"Die Rueckrufe der Seite, neueste zuerst"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Treffer der Filter"}},"required":["limit","offset","total"],"description":"Seitenangaben"}},"required":["data","pagination"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","batchNummer":"string","artikelId":"string","grund":"string","status":"string","erstelltAm":"string","abgeschlossenAm":"string","anzahlBetroffen":0,"createdAt":"string","updatedAt":"string"}],"pagination":{"limit":1,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Qm-prozesseRueckrufe","tags":["QM"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"prozessTyp","schema":{"type":"string"}}],"description":"Blaettert durch `qm_rueckrufe` des Mandanten, neueste zuerst. Nur `status` filtert (exakt, und der Status ist Freitext); ein mitgegebenes `prozessTyp` wird hier IGNORIERT. `limit` (1-200, Vorgabe 50) und `offset` blaettern, `total` zaehlt alle Treffer des Filters. Es gibt kein Soft-Delete. Fehlende Tabellen legt der Aufruf leer an.","summary":"Blaettert durch `qm_rueckrufe` des Mandanten, neueste zuerst","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Der angelegte Rueckruf im Status offen, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Rueckrufs"},"batchNummer":{"type":"string","description":"Betroffene Charge"},"artikelId":{"type":["string","null"],"description":"Betroffener Artikel; null wenn keiner benannt ist"},"grund":{"type":["string","null"],"description":"Grund des Rueckrufs; null wenn keiner erfasst ist"},"status":{"type":"string","description":"Freitext-Status; beim Anlegen `offen`. Es gibt KEINE feste Werteliste"},"erstelltAm":{"type":"string","description":"Zeitpunkt der Anlage"},"abgeschlossenAm":{"type":["string","null"],"description":"Abschlusszeitpunkt; null solange offen"},"anzahlBetroffen":{"type":["number","null"],"description":"Anzahl betroffener Einheiten; null wenn nicht erfasst"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","batchNummer","artikelId","grund","status","erstelltAm","abgeschlossenAm","anzahlBetroffen","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","batchNummer":"string","artikelId":"string","grund":"string","status":"string","erstelltAm":"string","abgeschlossenAm":"string","anzahlBetroffen":0,"createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Manager-Rolle erforderlich"},"503":{"description":"Anlegen fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Qm-prozesseRueckrufe","tags":["QM"],"parameters":[],"description":"Legt einen Rueckruf an. Der Status ist immer `offen` und laesst sich beim Anlegen nicht setzen. Ob es den Artikel gibt, wird NICHT geprueft, und die Chargennummer wird nicht auf Eindeutigkeit geprueft — derselbe Rueckruf laesst sich mehrfach anlegen. Es werden weder Benachrichtigungen verschickt noch Bestaende gesperrt. Die Antwort ist der Rueckruf SELBST, ohne umschliessendes Feld. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"batchNummer":{"type":"string","minLength":1,"maxLength":100},"artikelId":{"type":"string","format":"uuid"},"grund":{"type":"string"},"anzahlBetroffen":{"type":"integer","minimum":0}},"required":["batchNummer"]},"example":{"batchNummer":"string","artikelId":"00000000-0000-4000-8000-000000000000","grund":"string","anzahlBetroffen":0}}}},"summary":"Legt einen Rueckruf an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/qm-prozesse/rueckrufe/{id}":{"patch":{"responses":{"200":{"description":"Der Rueckruf nach der Aenderung, ohne umschliessendes Feld","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Rueckrufs"},"batchNummer":{"type":"string","description":"Betroffene Charge"},"artikelId":{"type":["string","null"],"description":"Betroffener Artikel; null wenn keiner benannt ist"},"grund":{"type":["string","null"],"description":"Grund des Rueckrufs; null wenn keiner erfasst ist"},"status":{"type":"string","description":"Freitext-Status; beim Anlegen `offen`. Es gibt KEINE feste Werteliste"},"erstelltAm":{"type":"string","description":"Zeitpunkt der Anlage"},"abgeschlossenAm":{"type":["string","null"],"description":"Abschlusszeitpunkt; null solange offen"},"anzahlBetroffen":{"type":["number","null"],"description":"Anzahl betroffener Einheiten; null wenn nicht erfasst"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","batchNummer","artikelId","grund","status","erstelltAm","abgeschlossenAm","anzahlBetroffen","createdAt","updatedAt"]},"example":{"id":"00000000-0000-4000-8000-000000000000","batchNummer":"string","artikelId":"string","grund":"string","status":"string","erstelltAm":"string","abgeschlossenAm":"string","anzahlBetroffen":0,"createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Manager-Rolle erforderlich"},"404":{"description":"Kein Rueckruf mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Aenderung fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"patchApiV1Qm-prozesseRueckrufeById","tags":["QM"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aendert Status, Grund, Abschlusszeitpunkt und Anzahl eines Rueckrufs. Jedes Feld ist freiwillig; nicht mitgegebene behalten ihren Wert, weil der Handler den aktuellen Stand vorher liest. Chargennummer und Artikel lassen sich hier NICHT aendern. `status` ist Freitext ohne Werteliste — nur `abgeschlossen` hat eine Bedeutung, denn `/stats` zaehlt alles andere als offen; ein Tippfehler bleibt damit unbemerkt in der Statistik. Ein gesetztes `abgeschlossenAm` schliesst den Rueckruf NICHT selbsttaetig. Lesen und Schreiben laufen ohne Transaktion. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","maxLength":30},"grund":{"type":"string"},"abgeschlossenAm":{"type":"string","format":"date-time"},"anzahlBetroffen":{"type":"integer","minimum":0}}},"example":{"status":"string","grund":"string","abgeschlossenAm":"2026-01-01T12:00:00.000Z","anzahlBetroffen":0}}}},"summary":"Aendert Status, Grund, Abschlusszeitpunkt und Anzahl eines Rueckrufs","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/anlagen/stats":{"get":{"responses":{"200":{"description":"Anlagen-Statistiken","content":{"application/json":{"schema":{"type":"object","properties":{"activeAssets":{"type":"integer"},"disposedAssets":{"type":"integer"},"totalPurchaseValue":{"type":"number"},"totalBookValue":{"type":"number"},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["activeAssets","disposedAssets","totalPurchaseValue","totalBookValue","meta"],"additionalProperties":false},"example":{"activeAssets":0,"disposedAssets":0,"totalPurchaseValue":0,"totalBookValue":0,"meta":{"tenantId":"string","source":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1AnlagenStats","tags":["anlagen"],"parameters":[],"summary":"KPIs zur Anlagenbuchhaltung","description":"Zaehlt aktive und ausgebuchte Anlagen und summiert Anschaffungs- und Buchwerte. Die beiden Summen beziehen sich NUR auf aktive Anlagen — ausgebuchte gehen allein in `disposedAssets` ein. Soft-geloeschte Zeilen (deleted_at) bleiben ueberall auszen vor. Fehlen die Anlagentabellen im Mandanten-Schema, legt der Aufruf sie an und antwortet mit Nullen."}},"/api/v1/anlagen/afa-tabellen":{"get":{"responses":{"200":{"description":"BMF AfA-Tabellen 2026, unveraendert aus der hinterlegten JSON-Datei","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string","description":"Kennung der Fassung, z. B. `BMF-2026`"},"lastUpdated":{"type":"string","description":"Stand der Tabelle als ISO-Datum"},"categories":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Kennung der AfA-Kategorie, z. B. `AV-1.1`"},"label":{"type":"string"},"nutzungsdauerJahre":{"type":"number"},"method":{"type":"string","description":"`linear`, `sofort` oder `pool`"},"note":{"type":"string"}},"required":["code","label","nutzungsdauerJahre","method"]}},"sonderabschreibung_7g_estg":{"type":"object","properties":{"rate":{"type":"number"},"rateRange":{"type":"string"},"kmuLimit":{"type":"number"},"note":{"type":"string"}},"required":["rate","rateRange","kmuLimit","note"]},"investitionsabzugsbetrag_7g":{"type":"object","properties":{"rate":{"type":"number"},"note":{"type":"string"},"obergrenze":{"type":"number"}},"required":["rate","note","obergrenze"]}},"required":["version","lastUpdated","categories","sonderabschreibung_7g_estg","investitionsabzugsbetrag_7g"]},"example":{"version":"string","lastUpdated":"string","categories":[{"code":"string","label":"string","nutzungsdauerJahre":0,"method":"string","note":"string"}],"sonderabschreibung_7g_estg":{"rate":0,"rateRange":"string","kmuLimit":0,"note":"string"},"investitionsabzugsbetrag_7g":{"rate":0,"note":"string","obergrenze":0}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1AnlagenAfa-tabellen","tags":["anlagen"],"parameters":[],"summary":"Liefert die BMF AfA-Tabellen 2026 (§7 EStG)","description":"Gibt eine im Paket hinterlegte JSON-Datei unveraendert zurueck. Sie wird EINMAL beim Start des Prozesses gelesen — eine geaenderte Datei wirkt erst nach einem Neustart. Der Endpunkt liest keine Datenbank und kennt keinen Mandanten: alle Mandanten erhalten denselben Inhalt, und die Nutzungsdauern sind Nachschlagewerte, keine bereits zugeordneten Werte einer Anlage."}},"/api/v1/anlagen/reports/anlagenverzeichnis-steuerlich/{jahr}":{"get":{"responses":{"200":{"description":"Anlagenverzeichnis als Plain-Text-Report (kein JSON) — X-PDF-Fallback: true","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Jahr ausserhalb 2000..2100","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1AnlagenReportsAnlagenverzeichnis-steuerlichByJahr","tags":["anlagen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"jahr","required":true}],"summary":"Steuerliches Anlagenverzeichnis als Textbericht","description":"Erzeugt eine feste Spaltentabelle als Text (Nr., Bezeichnung, Anschaffung, AK/HK, AfA-Kategorie, Nutzungsdauer, AfA des Jahres, steuerlicher Buchwert, Sonder-AfA, IAB) mit Summenzeile und Anlagenzahl. Enthalten sind ALLE nicht geloeschten Anlagen, auch ausgebuchte, nach Anlagennummer sortiert. Die AfA des Jahres wird linear neu gerechnet und ist 0, wenn das Jahr vor der Anschaffung oder nach der Nutzungsdauer liegt; fehlt eine steuerliche Nutzungsdauer bzw. ein steuerlicher Buchwert, treten die handelsrechtlichen Werte an ihre Stelle. Der Bericht ist reine Auskunft — er bucht nichts und speichert nichts. Weil keine PDF-Engine installiert ist, kommt Text statt PDF; die Kopfzeilen X-PDF-Fallback und X-PDF-Fallback-Reason sagen das an."}},"/api/v1/anlagen/reports/inventarliste":{"get":{"responses":{"200":{"description":"Inventarliste — anders als die Liste OHNE pagination, dafuer mit reportDate","content":{"application/json":{"schema":{"type":"object","properties":{"reportDate":{"type":"string"},"assets":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"assetNumber":{},"description":{},"purchaseDate":{},"purchaseValue":{"type":"number"},"usefulLifeYears":{"type":"number"},"depreciationMethod":{},"currentBookValue":{"type":"number"},"glAccount":{},"costCenter":{},"status":{},"createdAt":{},"updatedAt":{},"steuerUsefulLifeYears":{"type":["number","null"]},"steuerMethod":{"type":"string"},"steuerBookValue":{"type":["number","null"]},"afaCategory":{"type":["string","null"]},"sonderabschreibung7gUsed":{"type":"number"},"iabUsed":{"type":"number"},"isKmu":{"type":"boolean"}},"required":["id","purchaseValue","usefulLifeYears","currentBookValue","steuerUsefulLifeYears","steuerMethod","steuerBookValue","afaCategory","sonderabschreibung7gUsed","iabUsed","isKmu"],"additionalProperties":false}},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"total":{"type":"integer"}},"required":["tenantId","total"],"additionalProperties":false}},"required":["reportDate","assets","meta"],"additionalProperties":false},"example":{"reportDate":"string","assets":[{"id":"string","purchaseValue":0,"usefulLifeYears":0,"currentBookValue":0,"steuerUsefulLifeYears":0,"steuerMethod":"string","steuerBookValue":0,"afaCategory":"string","sonderabschreibung7gUsed":0,"iabUsed":0,"isKmu":true}],"meta":{"tenantId":"string","total":0}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1AnlagenReportsInventarliste","tags":["anlagen"],"parameters":[],"summary":"PDF-ready Inventarliste aller Anlagen","description":"Liefert ALLE nicht geloeschten Anlagen des Mandanten in EINER Antwort — ohne Blaetterung, ohne Filter und einschlieszlich ausgebuchter Anlagen —, sortiert nach Anlagennummer. Zusaetzlich zur Liste stehen das Erstellungsdatum (`reportDate`, heutiges Datum) und die Anzahl in `meta.total`. Reine Auskunft: es wird nichts gebucht und keine Datei erzeugt."}},"/api/v1/anlagen":{"get":{"responses":{"200":{"description":"Liste der Anlagen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"assetNumber":{},"description":{},"purchaseDate":{},"purchaseValue":{"type":"number"},"usefulLifeYears":{"type":"number"},"depreciationMethod":{},"currentBookValue":{"type":"number"},"glAccount":{},"costCenter":{},"status":{},"createdAt":{},"updatedAt":{},"steuerUsefulLifeYears":{"type":["number","null"]},"steuerMethod":{"type":"string"},"steuerBookValue":{"type":["number","null"]},"afaCategory":{"type":["string","null"]},"sonderabschreibung7gUsed":{"type":"number"},"iabUsed":{"type":"number"},"isKmu":{"type":"boolean"}},"required":["id","purchaseValue","usefulLifeYears","currentBookValue","steuerUsefulLifeYears","steuerMethod","steuerBookValue","afaCategory","sonderabschreibung7gUsed","iabUsed","isKmu"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"string","purchaseValue":0,"usefulLifeYears":0,"currentBookValue":0,"steuerUsefulLifeYears":0,"steuerMethod":"string","steuerBookValue":0,"afaCategory":"string","sonderabschreibung7gUsed":0,"iabUsed":0,"isKmu":true}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Anlagen","tags":["anlagen"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","disposed"]}},{"in":"query","name":"costCenter","schema":{"type":"string"}},{"in":"query","name":"glAccount","schema":{"type":"string"}}],"summary":"Listet Anlagen des Mandanten","description":"Blaettert ueber `limit` (1…200, Vorgabe 50) und `offset`, sortiert nach Anlagennummer aufsteigend. Filtern laesst sich nach `status` (active/disposed), `costCenter` und `glAccount` — mehrere Filter wirken zusammen (UND). Soft-geloeschte Anlagen erscheinen nie. `pagination.total` zaehlt die Treffer NACH den Filtern, aber ohne Blaetterung."},"post":{"responses":{"201":{"description":"Anlage angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"assetNumber":{},"description":{},"purchaseDate":{},"purchaseValue":{"type":"number"},"usefulLifeYears":{"type":"number"},"depreciationMethod":{},"currentBookValue":{"type":"number"},"glAccount":{},"costCenter":{},"status":{},"createdAt":{},"updatedAt":{},"steuerUsefulLifeYears":{"type":["number","null"]},"steuerMethod":{"type":"string"},"steuerBookValue":{"type":["number","null"]},"afaCategory":{"type":["string","null"]},"sonderabschreibung7gUsed":{"type":"number"},"iabUsed":{"type":"number"},"isKmu":{"type":"boolean"}},"required":["id","purchaseValue","usefulLifeYears","currentBookValue","steuerUsefulLifeYears","steuerMethod","steuerBookValue","afaCategory","sonderabschreibung7gUsed","iabUsed","isKmu"],"additionalProperties":false},"example":{"id":"string","purchaseValue":0,"usefulLifeYears":0,"currentBookValue":0,"steuerUsefulLifeYears":0,"steuerMethod":"string","steuerBookValue":0,"afaCategory":"string","sonderabschreibung7gUsed":0,"iabUsed":0,"isKmu":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1Anlagen","tags":["anlagen"],"parameters":[],"summary":"Legt eine neue Anlage an","description":"Nur ab Rolle „manager\". Ohne `assetNumber` vergibt der Endpunkt die naechste Nummer im Format ANL-00001 (Anzahl vorhandener Zeilen + 1); die Spalte ist eindeutig, ein bereits vergebener Wert wird abgewiesen. Fehlt `currentBookValue`, startet der Buchwert beim Anschaffungswert. Die steuerlichen Felder (`steuerUsefulLifeYears`, `steuerMethod`, `steuerBookValue`, `afaCategory`, `isKmu`) werden zwar geprueft, aber von diesem Endpunkt NICHT gespeichert — bis sie gesetzt sind, rechnen die steuerlichen Auswertungen mit den handelsrechtlichen Werten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"assetNumber":{"type":"string","minLength":1,"maxLength":50},"description":{"type":"string","minLength":1,"maxLength":500},"purchaseDate":{"type":"string","format":"date"},"purchaseValue":{"type":"number","minimum":0},"usefulLifeYears":{"type":"integer","minimum":1,"maximum":100},"depreciationMethod":{"type":"string","enum":["linear","declining"],"default":"linear"},"currentBookValue":{"type":"number","minimum":0},"glAccount":{"type":"string","maxLength":20},"costCenter":{"type":"string","maxLength":50},"status":{"type":"string","enum":["active","disposed"],"default":"active"},"steuerUsefulLifeYears":{"type":"integer","minimum":1,"maximum":100},"steuerMethod":{"type":"string","enum":["linear","sofort","pool"]},"steuerBookValue":{"type":"number","minimum":0},"afaCategory":{"type":"string","maxLength":20},"isKmu":{"type":"boolean"}},"required":["description","purchaseDate","purchaseValue","usefulLifeYears"]},"example":{"assetNumber":"string","description":"string","purchaseDate":"2026-01-01","purchaseValue":0,"usefulLifeYears":1,"depreciationMethod":"linear","currentBookValue":0,"glAccount":"string","costCenter":"string","status":"active","steuerUsefulLifeYears":1,"steuerMethod":"linear","steuerBookValue":0,"afaCategory":"string","isKmu":true}}}}}},"/api/v1/anlagen/{id}":{"get":{"responses":{"200":{"description":"Anlage","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"assetNumber":{},"description":{},"purchaseDate":{},"purchaseValue":{"type":"number"},"usefulLifeYears":{"type":"number"},"depreciationMethod":{},"currentBookValue":{"type":"number"},"glAccount":{},"costCenter":{},"status":{},"createdAt":{},"updatedAt":{},"steuerUsefulLifeYears":{"type":["number","null"]},"steuerMethod":{"type":"string"},"steuerBookValue":{"type":["number","null"]},"afaCategory":{"type":["string","null"]},"sonderabschreibung7gUsed":{"type":"number"},"iabUsed":{"type":"number"},"isKmu":{"type":"boolean"}},"required":["id","purchaseValue","usefulLifeYears","currentBookValue","steuerUsefulLifeYears","steuerMethod","steuerBookValue","afaCategory","sonderabschreibung7gUsed","iabUsed","isKmu"],"additionalProperties":false},"example":{"id":"string","purchaseValue":0,"usefulLifeYears":0,"currentBookValue":0,"steuerUsefulLifeYears":0,"steuerMethod":"string","steuerBookValue":0,"afaCategory":"string","sonderabschreibung7gUsed":0,"iabUsed":0,"isKmu":true}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Anlage nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1AnlagenById","tags":["anlagen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Liefert eine einzelne Anlage","description":"Liest EINE Anlage anhand ihrer UUID — nicht anhand der Anlagennummer. Soft-geloeschte Anlagen gelten als nicht vorhanden und ergeben 404. Die Antwort ist derselbe Datensatz wie in der Liste, samt der steuerlichen Felder und der bereits gebuchten Betraege fuer Sonder-AfA und IAB."},"put":{"responses":{"200":{"description":"Anlage aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"assetNumber":{},"description":{},"purchaseDate":{},"purchaseValue":{"type":"number"},"usefulLifeYears":{"type":"number"},"depreciationMethod":{},"currentBookValue":{"type":"number"},"glAccount":{},"costCenter":{},"status":{},"createdAt":{},"updatedAt":{},"steuerUsefulLifeYears":{"type":["number","null"]},"steuerMethod":{"type":"string"},"steuerBookValue":{"type":["number","null"]},"afaCategory":{"type":["string","null"]},"sonderabschreibung7gUsed":{"type":"number"},"iabUsed":{"type":"number"},"isKmu":{"type":"boolean"}},"required":["id","purchaseValue","usefulLifeYears","currentBookValue","steuerUsefulLifeYears","steuerMethod","steuerBookValue","afaCategory","sonderabschreibung7gUsed","iabUsed","isKmu"],"additionalProperties":false},"example":{"id":"string","purchaseValue":0,"usefulLifeYears":0,"currentBookValue":0,"steuerUsefulLifeYears":0,"steuerMethod":"string","steuerBookValue":0,"afaCategory":"string","sonderabschreibung7gUsed":0,"iabUsed":0,"isKmu":true}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Anlage nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1AnlagenById","tags":["anlagen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aktualisiert eine Anlage","description":"Nur ab Rolle „manager\". Teil-Update: der Endpunkt liest den bestehenden Satz und schreibt fuer jedes nicht gesendete Feld dessen aktuellen Wert zurueck. Nicht aenderbar sind hier die Anlagennummer und alle steuerlichen Felder — sie bleiben unberuehrt, auch wenn sie im Rumpf stehen. Ein direkt gesetzter `currentBookValue` uebersteuert den aus den AfA-Laeufen fortgeschriebenen Buchwert; bereits gebuchte Laeufe werden dabei nicht korrigiert. 404 bei unbekannter oder soft-geloeschter Anlage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"assetNumber":{"type":"string","minLength":1,"maxLength":50},"description":{"type":"string","minLength":1,"maxLength":500},"purchaseDate":{"type":"string","format":"date"},"purchaseValue":{"type":"number","minimum":0},"usefulLifeYears":{"type":"integer","minimum":1,"maximum":100},"depreciationMethod":{"type":"string","enum":["linear","declining"],"default":"linear"},"currentBookValue":{"type":"number","minimum":0},"glAccount":{"type":"string","maxLength":20},"costCenter":{"type":"string","maxLength":50},"status":{"type":"string","enum":["active","disposed"],"default":"active"},"steuerUsefulLifeYears":{"type":"integer","minimum":1,"maximum":100},"steuerMethod":{"type":"string","enum":["linear","sofort","pool"]},"steuerBookValue":{"type":"number","minimum":0},"afaCategory":{"type":"string","maxLength":20},"isKmu":{"type":"boolean"}}},"example":{"assetNumber":"string","description":"string","purchaseDate":"2026-01-01","purchaseValue":0,"usefulLifeYears":1,"depreciationMethod":"linear","currentBookValue":0,"glAccount":"string","costCenter":"string","status":"active","steuerUsefulLifeYears":1,"steuerMethod":"linear","steuerBookValue":0,"afaCategory":"string","isKmu":true}}}}}},"/api/v1/anlagen/{id}/abschreibung-run":{"post":{"responses":{"200":{"description":"AfA-Buchung durchgeführt — run nennt Buchwert VOR und NACH der Buchung","content":{"application/json":{"schema":{"type":"object","properties":{"run":{"type":"object","properties":{"id":{},"assetId":{"type":"string"},"periode":{"type":"string"},"amount":{"type":"number"},"bookValueBefore":{"type":"number"},"bookValueAfter":{"type":"number"}},"required":["assetId","periode","amount","bookValueBefore","bookValueAfter"],"additionalProperties":false},"message":{"type":"string"}},"required":["run","message"],"additionalProperties":false},"example":{"run":{"assetId":"string","periode":"string","amount":0,"bookValueBefore":0,"bookValueAfter":0},"message":"string"}}}},"400":{"description":"Nicht buchbar: asset_disposed · period_already_posted · fully_depreciated · no_depreciable_amount — message nennt den Grund","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Anlage nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"423":{"description":"Buchungsperiode geschlossen — es wurde NICHTS geschrieben. Die Periode wird vor dem Lauf geprueft, die Anlage bleibt unveraendert und derselbe Aufruf kann nach dem Oeffnen der Periode wiederholt werden. (Bis 01.09.2026 kam dieser Code ERST nach dem Schreiben — dann war die Anlage abgeschrieben, ohne Buchung, und `period_already_posted` blockierte jeden zweiten Versuch.)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1AnlagenByIdAbschreibung-run","tags":["anlagen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Führt die AfA-Buchung für eine Periode durch","description":"Nur ab Rolle „manager\". `periode` hat das Format YYYY-MM und darf je Anlage nur einmal gebucht werden. Der Betrag entsteht aus der Methode der Anlage: „linear\" gleichmaeszig ueber die Nutzungsdauer, „declining\" mit dem doppelten linearen Satz auf den aktuellen Buchwert. Er wird mit `partialMonths`/12 anteilig gekuerzt (Vorgabe 12) und nie hoeher gebucht als der Restbuchwert. Geschrieben werden ein Lauf in fixed_asset_depreciation_runs und der neue Buchwert der Anlage; danach entsteht die Journalbuchung 4830 an 0500 mit der Belegnummer AFA-<Anlagennummer>-<Periode>. Die Buchungsperiode wird VOR dem Schreiben geprueft — ist sie geschlossen, kommt 423 und es wurde nichts geaendert. Beide Schritte laufen weiterhin nicht in einer gemeinsamen Transaktion: scheitert die Journalbuchung aus einem anderen Grund, bleiben Lauf und neuer Buchwert bestehen, die Antwort meldet dann `journalGebucht: false` und nennt den Grund in `journalFehler`. Der Statuscode ist auch dann 200, weil der Lauf stattgefunden hat — wer nur ihn prueft, sieht das Problem nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"periode":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"},"partialMonths":{"type":"integer","minimum":1,"maximum":12,"default":12}},"required":["periode"]}}}}}},"/api/v1/anlagen/{id}/abschreibungs-plan":{"get":{"responses":{"200":{"description":"Handelsrechtlicher AfA-Plan — eine Zeile je Nutzungsjahr","content":{"application/json":{"schema":{"type":"object","properties":{"asset":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"assetNumber":{},"description":{},"purchaseDate":{},"purchaseValue":{"type":"number"},"usefulLifeYears":{"type":"number"},"depreciationMethod":{},"currentBookValue":{"type":"number"},"glAccount":{},"costCenter":{},"status":{},"createdAt":{},"updatedAt":{},"steuerUsefulLifeYears":{"type":["number","null"]},"steuerMethod":{"type":"string"},"steuerBookValue":{"type":["number","null"]},"afaCategory":{"type":["string","null"]},"sonderabschreibung7gUsed":{"type":"number"},"iabUsed":{"type":"number"},"isKmu":{"type":"boolean"}},"required":["id","purchaseValue","usefulLifeYears","currentBookValue","steuerUsefulLifeYears","steuerMethod","steuerBookValue","afaCategory","sonderabschreibung7gUsed","iabUsed","isKmu"],"additionalProperties":false},"plan":{"type":"array","items":{"type":"object","properties":{"year":{"type":"integer"},"depreciationAmount":{"type":"number"},"bookValueEnd":{"type":"number"},"accumulatedDepreciation":{"type":"number"}},"required":["year","depreciationAmount","bookValueEnd","accumulatedDepreciation"],"additionalProperties":false}},"meta":{"type":"object","properties":{"tenantId":{"type":"string"}},"required":["tenantId"],"additionalProperties":false}},"required":["asset","plan","meta"],"additionalProperties":false},"example":{"asset":{"id":"string","purchaseValue":0,"usefulLifeYears":0,"currentBookValue":0,"steuerUsefulLifeYears":0,"steuerMethod":"string","steuerBookValue":0,"afaCategory":"string","sonderabschreibung7gUsed":0,"iabUsed":0,"isKmu":true},"plan":[{"year":0,"depreciationAmount":0,"bookValueEnd":0,"accumulatedDepreciation":0}],"meta":{"tenantId":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Anlage nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1AnlagenByIdAbschreibungs-plan","tags":["anlagen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Liefert den AfA-Plan (Abschreibungsplan) für eine Anlage","description":"Rechnet eine Vorschau: eine Zeile je Nutzungsjahr, beginnend im Jahr des Anschaffungsdatums, mit Jahresbetrag, Restbuchwert am Jahresende und kumulierter Abschreibung. Gerechnet wird IMMER linear vom vollen Anschaffungswert — die Methode „declining\" und bereits gebuchte AfA-Laeufe bleiben auszen vor, der Plan zeigt also den Soll-Verlauf, nicht den aktuellen Stand. Nichts wird gespeichert oder gebucht."}},"/api/v1/anlagen/{id}/abschreibungs-plan-steuerlich":{"get":{"responses":{"200":{"description":"Steuerlicher AfA-Plan — je Jahr DREI Betraege (normal, §7g-Sonder, Summe), nicht einer wie im handelsrechtlichen Plan","content":{"application/json":{"schema":{"type":"object","properties":{"asset":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{},"assetNumber":{},"description":{},"purchaseDate":{},"purchaseValue":{"type":"number"},"usefulLifeYears":{"type":"number"},"depreciationMethod":{},"currentBookValue":{"type":"number"},"glAccount":{},"costCenter":{},"status":{},"createdAt":{},"updatedAt":{},"steuerUsefulLifeYears":{"type":["number","null"]},"steuerMethod":{"type":"string"},"steuerBookValue":{"type":["number","null"]},"afaCategory":{"type":["string","null"]},"sonderabschreibung7gUsed":{"type":"number"},"iabUsed":{"type":"number"},"isKmu":{"type":"boolean"}},"required":["id","purchaseValue","usefulLifeYears","currentBookValue","steuerUsefulLifeYears","steuerMethod","steuerBookValue","afaCategory","sonderabschreibung7gUsed","iabUsed","isKmu"],"additionalProperties":false},"plan":{"type":"array","items":{"type":"object","properties":{"year":{"type":"integer"},"normalDepreciation":{"type":"number"},"sonderabschreibungApplied":{"type":"number"},"totalDepreciation":{"type":"number"},"bookValueEnd":{"type":"number"},"accumulatedDepreciation":{"type":"number"}},"required":["year","normalDepreciation","sonderabschreibungApplied","totalDepreciation","bookValueEnd","accumulatedDepreciation"],"additionalProperties":false}},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"steuerUsefulLifeYears":{"type":"integer"},"sonder7gUsed":{"type":"number"},"note":{"type":"string"}},"required":["tenantId","steuerUsefulLifeYears","sonder7gUsed","note"],"additionalProperties":false}},"required":["asset","plan","meta"],"additionalProperties":false},"example":{"asset":{"id":"string","purchaseValue":0,"usefulLifeYears":0,"currentBookValue":0,"steuerUsefulLifeYears":0,"steuerMethod":"string","steuerBookValue":0,"afaCategory":"string","sonderabschreibung7gUsed":0,"iabUsed":0,"isKmu":true},"plan":[{"year":0,"normalDepreciation":0,"sonderabschreibungApplied":0,"totalDepreciation":0,"bookValueEnd":0,"accumulatedDepreciation":0}],"meta":{"tenantId":"string","steuerUsefulLifeYears":0,"sonder7gUsed":0,"note":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Anlage nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1AnlagenByIdAbschreibungs-plan-steuerlich","tags":["anlagen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Steuerlicher AfA-Plan (§7 EStG) inkl. Sonderabschreibung §7g","description":"Rechnet den Plan bei jedem Aufruf neu aus `\"<Mandantenschema>\".fixed_assets` aus und schreibt nichts — weder eine Abschreibungszeile noch eine Buchung im Journal. Gibt es die Anlage nicht (oder ist sie weich geloescht), antwortet der Endpunkt mit 404 und `error: asset_not_found`.\n\nGrundlage der Nutzungsdauer ist `steuer_useful_life_years`; ist sie nicht gesetzt, tritt die handelsrechtliche `useful_life_years` an ihre Stelle. Abgeschrieben wird linear ab dem Anschaffungsjahr. Die bereits gebuchte §7g-Sonderabschreibung (`sonderabschreibung_7g_used`) wird auf die ersten fuenf Jahre verteilt und je Jahr nur so weit angesetzt, wie nach der linearen AfA noch Buchwert da ist.\n\nDie Liste bricht ab, sobald der Buchwert 0 erreicht — sie kann also kuerzer sein als die Nutzungsdauer. Neben `plan` liefert die Antwort die Anlage selbst und unter `meta` die angesetzte Nutzungsdauer sowie die bislang gebuchte Sonderabschreibung."}},"/api/v1/anlagen/{id}/sonderabschreibung-7g":{"post":{"responses":{"200":{"description":"Sonderabschreibung gebucht — mindert den STEUERLICHEN Buchwert","content":{"application/json":{"schema":{"type":"object","properties":{"assetId":{"type":"string"},"jahr":{"type":"integer"},"prozent":{"type":"number"},"amount":{"type":"number"},"steuerBookValueBefore":{"type":"number"},"steuerBookValueAfter":{"type":"number"},"sonder7gUsedTotal":{"type":"number"},"message":{"type":"string"}},"required":["assetId","jahr","prozent","amount","steuerBookValueBefore","steuerBookValueAfter","sonder7gUsedTotal","message"],"additionalProperties":false},"example":{"assetId":"string","jahr":0,"prozent":0,"amount":0,"steuerBookValueBefore":0,"steuerBookValueAfter":0,"sonder7gUsedTotal":0,"message":"string"}}}},"400":{"description":"Limit ueberschritten — message nennt Hoechstbetrag und bereits Gebuchtes","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine KMU-Berechtigung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"404":{"description":"Anlage nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1AnlagenByIdSonderabschreibung-7g","tags":["anlagen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"§7g EStG Sonderabschreibung buchen (max. 20% des AK, nur KMU)","description":"Bucht `prozent` (0–20) der Anschaffungskosten als Sonderabschreibung nach §7g EStG. Der Vorgang schreibt an zwei Stellen: eine Zeile in `\"<Mandantenschema>\".fixed_asset_depreciation_runs` mit `run_type = sonder_7g` und der Periode `<jahr>-01`, und in `fixed_assets` die neuen Werte fuer `sonderabschreibung_7g_used` und `steuer_book_value`. Beide Schreibvorgaenge laufen nebeneinander, NICHT in einer Transaktion.\n\nBetroffen ist allein der STEUERLICHE Buchwert — der handelsrechtliche `current_book_value` bleibt stehen, und im Buchungsjournal entsteht nichts. Wer die Sonderabschreibung auch buchhalterisch abbilden will, muss das getrennt tun.\n\nVorausgesetzt wird mindestens die Rolle Manager. Eine unbekannte oder weich geloeschte Anlage ergibt 404 (`asset_not_found`). Steht `is_kmu` ausdruecklich auf `false`, wird mit 403 (`not_kmu`) abgelehnt; ein leeres Feld gilt dabei NICHT als Ablehnung. Wuerde die Summe aller §7g-Buchungen 20 % der Anschaffungskosten uebersteigen, kommt 400 (`sonder_limit_exceeded`) und es wird nichts geschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jahr":{"type":"integer","minimum":2000,"maximum":2100},"prozent":{"type":"number","minimum":0,"maximum":20}},"required":["jahr","prozent"]},"example":{"jahr":2000,"prozent":0}}}}}},"/api/v1/anlagen/{id}/iab":{"post":{"responses":{"200":{"description":"IAB gebucht — anders als die Sonderabschreibung OHNE Buchwerte in der Antwort, der Abzugsbetrag mindert den Buchwert nicht","content":{"application/json":{"schema":{"type":"object","properties":{"assetId":{"type":"string"},"jahr":{"type":"integer"},"prozent":{"type":"number"},"amount":{"type":"number"},"iabUsedTotal":{"type":"number"},"message":{"type":"string"}},"required":["assetId","jahr","prozent","amount","iabUsedTotal","message"],"additionalProperties":false},"example":{"assetId":"string","jahr":0,"prozent":0,"amount":0,"iabUsedTotal":0,"message":"string"}}}},"400":{"description":"Limit ueberschritten — message nennt Hoechstbetrag und bereits Gebuchtes","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"Anlage nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1AnlagenByIdIab","tags":["anlagen"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"§7g Abs.1 EStG Investitionsabzugsbetrag buchen (max. 40% des AK)","description":"Nur ab Rolle „manager\". Der Betrag ergibt sich aus `prozent` (0…40) des Anschaffungswerts und wird auf die Spalte iab_used ADDIERT — mehrere Aufrufe summieren sich, bis 40 % des Anschaffungswerts erreicht sind; darueber antwortet der Endpunkt mit 400 und nennt Hoechstbetrag und bereits Gebuchtes. Es entsteht KEINE Journalbuchung und keine Lauf-Zeile, und `jahr` wird nur zurueckgemeldet, nicht gespeichert — es gibt daher keine Zuordnung des Abzugsbetrags zu einem Jahr und keinen Weg, einen gebuchten Betrag ueber diesen Endpunkt zurueckzunehmen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jahr":{"type":"integer","minimum":2000,"maximum":2100},"prozent":{"type":"number","minimum":0,"maximum":40}},"required":["jahr","prozent"]},"example":{"jahr":2000,"prozent":0}}}}}},"/api/v1/elster/ust-va":{"get":{"responses":{"200":{"description":"Liste der UStVA-Runs mit Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Voranmeldungs-Laufs"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem der Lauf gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Kalenderjahr der Periode"},"quartal":{"type":["integer","null"],"minimum":1,"maximum":4,"description":"Quartal 1-4; null bei monatlicher Voranmeldung"},"monat":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat 1-12; null bei quartalsweiser Voranmeldung"},"zeitraumTyp":{"type":"string","enum":["monat","quartal"],"description":"Welche Periodenlaenge der Lauf meldet"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"summe19":{"type":"number","description":"Kz81 — Netto-Bemessungsgrundlage zum Regelsatz 19 %"},"summe7":{"type":"number","description":"Kz86 — Netto-Bemessungsgrundlage zum ermaessigten Satz 7 %"},"vorsteuer":{"type":"number","description":"Kz66 — abziehbare Vorsteuer als Betrag"},"zahllast":{"type":"number","description":"Errechnete Zahllast: Steuer aus Kz81/Kz86 minus Vorsteuer"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt des Laufs"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung am Lauf"}},"required":["id","tenantId","jahr","quartal","monat","zeitraumTyp","status","summe19","summe7","vorsteuer","zahllast","xmlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Ein UStVA-Lauf mit Periode, Summen und Einreichungsstand"},"description":"Die Laeufe der aktuellen Seite"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Laeufe, die dem Filter entsprechen"}},"required":["runs","total"]},"example":{"runs":[{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"quartal":1,"monat":1,"zeitraumTyp":"monat","status":"entwurf","summe19":0,"summe7":0,"vorsteuer":0,"zahllast":0,"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ElsterUst-va","tags":["elster"],"parameters":[{"in":"query","name":"jahr","schema":{"type":"integer","minimum":2000,"maximum":2099}},{"in":"query","name":"status","schema":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"]}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"summary":"Liste aller UStVA-Runs (Filter: jahr, status)","description":"Liest `ust_voranmeldung_runs` des Mandanten, sortiert nach Jahr, dann Quartal, dann Monat — jeweils absteigend, wobei nicht gesetzte Werte hinten stehen. Filtert wahlweise auf `jahr` und auf einen der drei Zustaende `entwurf`, `xml_generiert` und `eingereicht`. Geblaettert wird ueber `limit` (1-200, Standard 50) und `offset`; `total` zaehlt alle Treffer des Filters, nicht nur die Seite. Der Umschlag heisst `runs`, nicht `data`."},"post":{"responses":{"201":{"description":"Run angelegt, Summen aus journal_entries und buchungen berechnet","content":{"application/json":{"schema":{"type":"object","properties":{"run":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Voranmeldungs-Laufs"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem der Lauf gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Kalenderjahr der Periode"},"quartal":{"type":["integer","null"],"minimum":1,"maximum":4,"description":"Quartal 1-4; null bei monatlicher Voranmeldung"},"monat":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat 1-12; null bei quartalsweiser Voranmeldung"},"zeitraumTyp":{"type":"string","enum":["monat","quartal"],"description":"Welche Periodenlaenge der Lauf meldet"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"summe19":{"type":"number","description":"Kz81 — Netto-Bemessungsgrundlage zum Regelsatz 19 %"},"summe7":{"type":"number","description":"Kz86 — Netto-Bemessungsgrundlage zum ermaessigten Satz 7 %"},"vorsteuer":{"type":"number","description":"Kz66 — abziehbare Vorsteuer als Betrag"},"zahllast":{"type":"number","description":"Errechnete Zahllast: Steuer aus Kz81/Kz86 minus Vorsteuer"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt des Laufs"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung am Lauf"}},"required":["id","tenantId","jahr","quartal","monat","zeitraumTyp","status","summe19","summe7","vorsteuer","zahllast","xmlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Der angelegte Lauf mit den berechneten Summen"}},"required":["run"]},"example":{"run":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"quartal":1,"monat":1,"zeitraumTyp":"monat","status":"entwurf","summe19":0,"summe7":0,"vorsteuer":0,"zahllast":0,"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"400":{"description":"Zeitraum unvollstaendig oder Eingabe ungueltig (Klartext)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ElsterUst-va","tags":["elster"],"parameters":[],"description":"Neuen UStVA-Run anlegen. Berechnet Summen automatisch aus der buchungen-Tabelle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jahr":{"type":"integer","minimum":2000,"maximum":2099},"quartal":{"type":"integer","minimum":1,"maximum":4},"monat":{"type":"integer","minimum":1,"maximum":12},"zeitraum_typ":{"type":"string","enum":["monat","quartal"],"default":"monat"}},"required":["jahr"]},"example":{"jahr":2000,"quartal":1,"monat":1,"zeitraum_typ":"monat"}}}},"summary":"Neuen UStVA-Run anlegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/ust-va/{id}":{"get":{"responses":{"200":{"description":"Run-Detail mit Konten-Aufschluss und Datumsbereich der Periode","content":{"application/json":{"schema":{"type":"object","properties":{"run":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Voranmeldungs-Laufs"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem der Lauf gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Kalenderjahr der Periode"},"quartal":{"type":["integer","null"],"minimum":1,"maximum":4,"description":"Quartal 1-4; null bei monatlicher Voranmeldung"},"monat":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Monat 1-12; null bei quartalsweiser Voranmeldung"},"zeitraumTyp":{"type":"string","enum":["monat","quartal"],"description":"Welche Periodenlaenge der Lauf meldet"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"summe19":{"type":"number","description":"Kz81 — Netto-Bemessungsgrundlage zum Regelsatz 19 %"},"summe7":{"type":"number","description":"Kz86 — Netto-Bemessungsgrundlage zum ermaessigten Satz 7 %"},"vorsteuer":{"type":"number","description":"Kz66 — abziehbare Vorsteuer als Betrag"},"zahllast":{"type":"number","description":"Errechnete Zahllast: Steuer aus Kz81/Kz86 minus Vorsteuer"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt des Laufs"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung am Lauf"}},"required":["id","tenantId","jahr","quartal","monat","zeitraumTyp","status","summe19","summe7","vorsteuer","zahllast","xmlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Der Lauf selbst"},"kontenDetail":{"type":"array","items":{"type":"object","properties":{"habenKonto":{"type":["string","null"],"minLength":1,"description":"Habenkonto der Gruppe; null wenn die Buchungen keins tragen"},"sollKonto":{"type":["string","null"],"minLength":1,"description":"Sollkonto der Gruppe; null wenn die Buchungen keins tragen"},"buchungenAnzahl":{"type":"integer","minimum":0,"description":"Anzahl Buchungen in dieser Gruppe"},"betragSum":{"type":"number","description":"Summe der Betraege dieser Gruppe"}},"required":["habenKonto","sollKonto","buchungenAnzahl","betragSum"]},"description":"Aufschluesselung der Umsatzsteuer- und Vorsteuerkonten der Periode"},"periode":{"type":"object","properties":{"dateFrom":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Erster Tag der Periode (YYYY-MM-DD)"},"dateTo":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Letzter Tag der Periode (YYYY-MM-DD)"}},"required":["dateFrom","dateTo"],"description":"Der aus Jahr und Monat/Quartal errechnete Datumsbereich"}},"required":["run","kontenDetail","periode"]},"example":{"run":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"quartal":1,"monat":1,"zeitraumTyp":"monat","status":"entwurf","summe19":0,"summe7":0,"vorsteuer":0,"zahllast":0,"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"},"kontenDetail":[{"habenKonto":"string","sollKonto":"string","buchungenAnzahl":0,"betragSum":0}],"periode":{"dateFrom":"2026-01-01","dateTo":"2026-01-01"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"getApiV1ElsterUst-vaById","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"UStVA-Run Detail mit Aufschluss nach Konten","description":"Liefert den Lauf selbst und dazu einen Aufschluss nach Konten: die nicht stornierten Buchungen im Zeitraum der Periode, gruppiert nach Haben- und Sollkonto, je gezaehlt und summiert. Beruecksichtigt werden dabei NUR die Umsatzsteuer-Konten 1771 und 1776 im Haben sowie 1576 und 1577 im Soll — der Aufschluss ist also kein vollstaendiges Journal der Periode. `periode` nennt den Datumsbereich, den der Lauf abdeckt. Unbekannte id → 404."}},"/api/v1/elster/ust-va/{id}/generate-xml":{"post":{"responses":{"200":{"description":"XML erzeugt; validation nennt fehlende Pflichtmarker","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Laufs, fuer den erzeugt wurde"},"xml_path":{"type":"string","minLength":1,"description":"Ablageort der geschriebenen XML-Datei"},"validation":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn alle Pflichtmarker im XML stehen"},"errors":{"type":"array","items":{"type":"string","minLength":1},"description":"Klartext je fehlendem Pflichtmarker; leer wenn ok"}},"required":["ok","errors"],"description":"Ergebnis der Strukturpruefung — eine Marker-Pruefung, KEINE ERiC-Validierung"}},"required":["id","xml_path","validation"]},"example":{"id":"00000000-0000-4000-8000-000000000000","xml_path":"string","validation":{"ok":true,"errors":["string"]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Run nicht gefunden (Klartext)"}},"operationId":"postApiV1ElsterUst-vaByIdGenerate-xml","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Erzeugt das ELSTER-XML zur Umsatzsteuer-Voranmeldung","description":"ELSTER-XML für den UStVA-Run erzeugen, auf Platte ablegen und auf Pflichtmarker prüfen. Antwortet mit JSON (Pfad + Prüfergebnis), nicht mit der Datei — die liegt unter GET /:id/xml."}},"/api/v1/elster/ust-va/{id}/submit":{"post":{"responses":{"200":{"description":"Als eingereicht vermerkt — mit STUB-Quittung, ohne echte ELSTER-Übertragung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des eingereichten Laufs"},"status":{"type":"string","const":"eingereicht","description":"Neuer Stand des Laufs"},"elsterTransferTicket":{"type":"string","minLength":1,"description":"Quittungsnummer. Solange die ERiC-Anbindung fehlt, beginnt sie mit \"STUB-\" und stammt nicht vom Finanzamt."}},"required":["id","status","elsterTransferTicket"]},"example":{"id":"00000000-0000-4000-8000-000000000000","status":"eingereicht","elsterTransferTicket":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"ELSTER_SUBMIT_ENABLED nicht gesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"elster_submit_disabled","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext mit dem Hinweis auf den manuellen Upload"}},"required":["error","message"]}}}},"404":{"description":"Run nicht gefunden (Klartext)"}},"operationId":"postApiV1ElsterUst-vaByIdSubmit","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"UStVA bei ELSTER einreichen (GATED: erfordert ELSTER_SUBMIT_ENABLED=true). Andernfalls: XML-Download für manuellen Upload via Mein-ELSTER. ACHTUNG — die ERiC-Anbindung fehlt noch: der Lauf wird auf \"eingereicht\" gesetzt und bekommt eine selbst erzeugte Quittungsnummer mit Präfix \"STUB-\". Es geht nichts ans Finanzamt.","summary":"UStVA bei ELSTER einreichen (GATED: erfordert ELSTER_SUBMIT_ENABLED=true)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/ust-va/{id}/xml":{"get":{"responses":{"200":{"description":"XML-Datei als Anhang (Content-Type text/xml)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Run oder XML nicht gefunden (Klartext)"}},"operationId":"getApiV1ElsterUst-vaByIdXml","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"ELSTER-XML als Datei herunterladen (text/xml, kein JSON-Rumpf)","description":"Liefert die zuvor erzeugte XML-Datei als Anhang, benannt nach Jahr und Periode (etwa `UStVA-2026-03.xml` oder `UStVA-2026-Q1.xml`). Erzeugt wird hier NICHTS: wurde POST /{id}/generate-xml noch nicht aufgerufen oder liegt die Datei nicht mehr auf der Platte, antwortet die Route mit 404. Der Rumpf ist die XML-Datei selbst, kein JSON."}},"/api/v1/elster/gewerbesteuer":{"get":{"responses":{"200":{"description":"Liste der GewSt-Erklärungen mit Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerungen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Erhebungszeitraum (Kalenderjahr)"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"gewinn":{"type":"number","description":"Gewinn oder Verlust laut Steuerbilanz"},"hinzurechnungen":{"type":"object","properties":{"schuldzinsen":{"type":"number","minimum":0,"description":"Schuldzinsen, §8 Nr. 1a GewStG"},"mietenImmobilien":{"type":"number","minimum":0,"description":"Mieten und Pachten fuer Grundbesitz, §8 Nr. 1e GewStG"},"mietenMobilien":{"type":"number","minimum":0,"description":"Mieten und Pachten fuer bewegliche Wirtschaftsgueter, §8 Nr. 1d GewStG"},"lizenzKonzession":{"type":"number","minimum":0,"description":"Lizenz- und Konzessionsentgelte, §8 Nr. 1f GewStG"}},"required":["schuldzinsen","mietenImmobilien","mietenMobilien","lizenzKonzession"],"description":"Hinzurechnungen nach §8 GewStG, wie erfasst"},"kuerzungen":{"type":"object","properties":{"einheitswertGrundbesitz":{"type":"number","minimum":0,"description":"1,2 % des Einheitswerts des Grundbesitzes, §9 Nr. 1 GewStG"},"beteiligungsErtrag":{"type":"number","minimum":0,"description":"Gewinnanteile aus Beteiligungen, §9 Nr. 2a GewStG"}},"required":["einheitswertGrundbesitz","beteiligungsErtrag"],"description":"Kuerzungen nach §9 GewStG, wie erfasst"},"gewerbeertrag":{"type":["number","null"],"description":"Abgerundeter Gewerbeertrag, §11 Abs. 1 Nr. 2 GewStG; null solange nicht berechnet"},"hebesatz":{"type":"number","minimum":0,"maximum":2000,"description":"Hebesatz der Gemeinde in Prozent (z. B. 400)"},"steuer":{"type":["number","null"],"description":"Errechnete Gewerbesteuer; null solange nicht berechnet"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"rechtsform":{"type":["string","null"],"minLength":1,"description":"Rechtsform; entscheidet ueber den Freibetrag nach §11 Abs. 1 Nr. 1 GewStG"},"unternehmensname":{"type":["string","null"],"minLength":1,"description":"Name des Unternehmens fuer den Datenlieferanten-Block"},"steuernummer":{"type":["string","null"],"minLength":1,"description":"Steuernummer im ELSTER-Format"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","jahr","status","gewinn","hinzurechnungen","kuerzungen","gewerbeertrag","hebesatz","steuer","xmlPath","eingereichtAm","elsterTransferTicket","rechtsform","unternehmensname","steuernummer","createdAt","updatedAt"],"description":"Eine Gewerbesteuer-Erklaerung mit Bemessungsgrundlagen und Einreichungsstand"},"description":"Die Erklaerungen der aktuellen Seite"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Erklaerungen, die dem Filter entsprechen"}},"required":["erklaerungen","total"]},"example":{"erklaerungen":[{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"status":"entwurf","gewinn":0,"hinzurechnungen":{"schuldzinsen":0,"mietenImmobilien":0,"mietenMobilien":0,"lizenzKonzession":0},"kuerzungen":{"einheitswertGrundbesitz":0,"beteiligungsErtrag":0},"gewerbeertrag":0,"hebesatz":0,"steuer":0,"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","rechtsform":"string","unternehmensname":"string","steuernummer":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ElsterGewerbesteuer","tags":["elster"],"parameters":[{"in":"query","name":"jahr","schema":{"type":"integer","minimum":2000,"maximum":2099}},{"in":"query","name":"status","schema":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"]}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"summary":"Liste aller Gewerbesteuer-Erklärungen","description":"Liest `gewerbesteuer_erklaerung` des Mandanten, nach Jahr absteigend. Filtert wahlweise auf `jahr` und auf einen der drei Zustaende `entwurf`, `xml_generiert` und `eingereicht`. Geblaettert wird ueber `limit` (1-200, Standard 50) und `offset`; `total` zaehlt alle Treffer des Filters, nicht nur die Seite. Der Umschlag heisst `erklaerungen`, nicht `data`."},"post":{"responses":{"201":{"description":"Erklärung angelegt, samt frisch gerechnetem Rechenweg","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerung":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Erhebungszeitraum (Kalenderjahr)"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"gewinn":{"type":"number","description":"Gewinn oder Verlust laut Steuerbilanz"},"hinzurechnungen":{"type":"object","properties":{"schuldzinsen":{"type":"number","minimum":0,"description":"Schuldzinsen, §8 Nr. 1a GewStG"},"mietenImmobilien":{"type":"number","minimum":0,"description":"Mieten und Pachten fuer Grundbesitz, §8 Nr. 1e GewStG"},"mietenMobilien":{"type":"number","minimum":0,"description":"Mieten und Pachten fuer bewegliche Wirtschaftsgueter, §8 Nr. 1d GewStG"},"lizenzKonzession":{"type":"number","minimum":0,"description":"Lizenz- und Konzessionsentgelte, §8 Nr. 1f GewStG"}},"required":["schuldzinsen","mietenImmobilien","mietenMobilien","lizenzKonzession"],"description":"Hinzurechnungen nach §8 GewStG, wie erfasst"},"kuerzungen":{"type":"object","properties":{"einheitswertGrundbesitz":{"type":"number","minimum":0,"description":"1,2 % des Einheitswerts des Grundbesitzes, §9 Nr. 1 GewStG"},"beteiligungsErtrag":{"type":"number","minimum":0,"description":"Gewinnanteile aus Beteiligungen, §9 Nr. 2a GewStG"}},"required":["einheitswertGrundbesitz","beteiligungsErtrag"],"description":"Kuerzungen nach §9 GewStG, wie erfasst"},"gewerbeertrag":{"type":["number","null"],"description":"Abgerundeter Gewerbeertrag, §11 Abs. 1 Nr. 2 GewStG; null solange nicht berechnet"},"hebesatz":{"type":"number","minimum":0,"maximum":2000,"description":"Hebesatz der Gemeinde in Prozent (z. B. 400)"},"steuer":{"type":["number","null"],"description":"Errechnete Gewerbesteuer; null solange nicht berechnet"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"rechtsform":{"type":["string","null"],"minLength":1,"description":"Rechtsform; entscheidet ueber den Freibetrag nach §11 Abs. 1 Nr. 1 GewStG"},"unternehmensname":{"type":["string","null"],"minLength":1,"description":"Name des Unternehmens fuer den Datenlieferanten-Block"},"steuernummer":{"type":["string","null"],"minLength":1,"description":"Steuernummer im ELSTER-Format"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","jahr","status","gewinn","hinzurechnungen","kuerzungen","gewerbeertrag","hebesatz","steuer","xmlPath","eingereichtAm","elsterTransferTicket","rechtsform","unternehmensname","steuernummer","createdAt","updatedAt"],"description":"Der gespeicherte Stand"},"berechnung":{"type":"object","properties":{"hinzurechnungenRohSumme":{"type":"number","description":"Summe der Hinzurechnungen vor Freibetrag und 25-%-Ansatz"},"hinzurechnungenFreibetrag":{"type":"number","description":"Angesetzter Freibetrag auf die Rohsumme (200.000 EUR, §8 Nr. 1 GewStG)"},"hinzurechnungenAnteil":{"type":"number","description":"25 % der Rohsumme nach Freibetrag, mindestens 0"},"kuerzungenSumme":{"type":"number","description":"Summe der Kuerzungen nach §9 GewStG"},"gewerbeertagVorRundung":{"type":"number","description":"Gewinn plus Hinzurechnungsanteil minus Kuerzungen, ungerundet"},"freibetragUnternehmen":{"type":"number","description":"Freibetrag fuer Einzelunternehmen und Personengesellschaften (24.500 EUR); 0 bei GmbH/AG"},"gewerbeertrag":{"type":"number","minimum":0,"description":"Auf volle 100 EUR abgerundeter Gewerbeertrag, mindestens 0"},"steuermessbetrag":{"type":"number","description":"Gewerbeertrag mal 3,5 %, §11 Abs. 2 GewStG"},"gewerbesteuer":{"type":"number","description":"Steuermessbetrag mal Hebesatz geteilt durch 100"}},"required":["hinzurechnungenRohSumme","hinzurechnungenFreibetrag","hinzurechnungenAnteil","kuerzungenSumme","gewerbeertagVorRundung","freibetragUnternehmen","gewerbeertrag","steuermessbetrag","gewerbesteuer"],"description":"Der zugehoerige Rechenweg"}},"required":["erklaerung","berechnung"]},"example":{"erklaerung":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"status":"entwurf","gewinn":0,"hinzurechnungen":{"schuldzinsen":0,"mietenImmobilien":0,"mietenMobilien":0,"lizenzKonzession":0},"kuerzungen":{"einheitswertGrundbesitz":0,"beteiligungsErtrag":0},"gewerbeertrag":0,"hebesatz":0,"steuer":0,"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","rechtsform":"string","unternehmensname":"string","steuernummer":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"},"berechnung":{"hinzurechnungenRohSumme":0,"hinzurechnungenFreibetrag":0,"hinzurechnungenAnteil":0,"kuerzungenSumme":0,"gewerbeertagVorRundung":0,"freibetragUnternehmen":0,"gewerbeertrag":0,"steuermessbetrag":0,"gewerbesteuer":0}}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ElsterGewerbesteuer","tags":["elster"],"parameters":[],"description":"Neue Gewerbesteuer-Erklärung anlegen. Berechnet Gewerbeertrag, Steuermessbetrag und GewSt automatisch.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jahr":{"type":"integer","minimum":2000,"maximum":2099},"gewinn":{"type":"number"},"hinzurechnungen":{"type":"object","properties":{"schuldzinsen":{"type":"number","minimum":0,"default":0},"mietenImmobilien":{"type":"number","minimum":0,"default":0},"mietenMobilien":{"type":"number","minimum":0,"default":0},"lizenzKonzession":{"type":"number","minimum":0,"default":0}},"default":{}},"kuerzungen":{"type":"object","properties":{"einheitswertGrundbesitz":{"type":"number","minimum":0,"default":0},"beteiligungsErtrag":{"type":"number","minimum":0,"default":0}},"default":{}},"hebesatz":{"type":"number","minimum":0,"maximum":2000,"default":400},"rechtsform":{"type":"string"},"unternehmensname":{"type":"string"},"steuernummer":{"type":"string"}},"required":["jahr","gewinn"]},"example":{"jahr":2000,"gewinn":0,"hinzurechnungen":{"schuldzinsen":0,"mietenImmobilien":0,"mietenMobilien":0,"lizenzKonzession":0},"kuerzungen":{"einheitswertGrundbesitz":0,"beteiligungsErtrag":0},"hebesatz":0,"rechtsform":"string","unternehmensname":"string","steuernummer":"string"}}}},"summary":"Neue Gewerbesteuer-Erklärung anlegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/gewerbesteuer/{id}":{"get":{"responses":{"200":{"description":"Detail mit frisch gerechnetem Rechenweg","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerung":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Erhebungszeitraum (Kalenderjahr)"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"gewinn":{"type":"number","description":"Gewinn oder Verlust laut Steuerbilanz"},"hinzurechnungen":{"type":"object","properties":{"schuldzinsen":{"type":"number","minimum":0,"description":"Schuldzinsen, §8 Nr. 1a GewStG"},"mietenImmobilien":{"type":"number","minimum":0,"description":"Mieten und Pachten fuer Grundbesitz, §8 Nr. 1e GewStG"},"mietenMobilien":{"type":"number","minimum":0,"description":"Mieten und Pachten fuer bewegliche Wirtschaftsgueter, §8 Nr. 1d GewStG"},"lizenzKonzession":{"type":"number","minimum":0,"description":"Lizenz- und Konzessionsentgelte, §8 Nr. 1f GewStG"}},"required":["schuldzinsen","mietenImmobilien","mietenMobilien","lizenzKonzession"],"description":"Hinzurechnungen nach §8 GewStG, wie erfasst"},"kuerzungen":{"type":"object","properties":{"einheitswertGrundbesitz":{"type":"number","minimum":0,"description":"1,2 % des Einheitswerts des Grundbesitzes, §9 Nr. 1 GewStG"},"beteiligungsErtrag":{"type":"number","minimum":0,"description":"Gewinnanteile aus Beteiligungen, §9 Nr. 2a GewStG"}},"required":["einheitswertGrundbesitz","beteiligungsErtrag"],"description":"Kuerzungen nach §9 GewStG, wie erfasst"},"gewerbeertrag":{"type":["number","null"],"description":"Abgerundeter Gewerbeertrag, §11 Abs. 1 Nr. 2 GewStG; null solange nicht berechnet"},"hebesatz":{"type":"number","minimum":0,"maximum":2000,"description":"Hebesatz der Gemeinde in Prozent (z. B. 400)"},"steuer":{"type":["number","null"],"description":"Errechnete Gewerbesteuer; null solange nicht berechnet"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"rechtsform":{"type":["string","null"],"minLength":1,"description":"Rechtsform; entscheidet ueber den Freibetrag nach §11 Abs. 1 Nr. 1 GewStG"},"unternehmensname":{"type":["string","null"],"minLength":1,"description":"Name des Unternehmens fuer den Datenlieferanten-Block"},"steuernummer":{"type":["string","null"],"minLength":1,"description":"Steuernummer im ELSTER-Format"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","jahr","status","gewinn","hinzurechnungen","kuerzungen","gewerbeertrag","hebesatz","steuer","xmlPath","eingereichtAm","elsterTransferTicket","rechtsform","unternehmensname","steuernummer","createdAt","updatedAt"],"description":"Der gespeicherte Stand"},"berechnung":{"type":"object","properties":{"hinzurechnungenRohSumme":{"type":"number","description":"Summe der Hinzurechnungen vor Freibetrag und 25-%-Ansatz"},"hinzurechnungenFreibetrag":{"type":"number","description":"Angesetzter Freibetrag auf die Rohsumme (200.000 EUR, §8 Nr. 1 GewStG)"},"hinzurechnungenAnteil":{"type":"number","description":"25 % der Rohsumme nach Freibetrag, mindestens 0"},"kuerzungenSumme":{"type":"number","description":"Summe der Kuerzungen nach §9 GewStG"},"gewerbeertagVorRundung":{"type":"number","description":"Gewinn plus Hinzurechnungsanteil minus Kuerzungen, ungerundet"},"freibetragUnternehmen":{"type":"number","description":"Freibetrag fuer Einzelunternehmen und Personengesellschaften (24.500 EUR); 0 bei GmbH/AG"},"gewerbeertrag":{"type":"number","minimum":0,"description":"Auf volle 100 EUR abgerundeter Gewerbeertrag, mindestens 0"},"steuermessbetrag":{"type":"number","description":"Gewerbeertrag mal 3,5 %, §11 Abs. 2 GewStG"},"gewerbesteuer":{"type":"number","description":"Steuermessbetrag mal Hebesatz geteilt durch 100"}},"required":["hinzurechnungenRohSumme","hinzurechnungenFreibetrag","hinzurechnungenAnteil","kuerzungenSumme","gewerbeertagVorRundung","freibetragUnternehmen","gewerbeertrag","steuermessbetrag","gewerbesteuer"],"description":"Der zugehoerige Rechenweg"}},"required":["erklaerung","berechnung"]},"example":{"erklaerung":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"status":"entwurf","gewinn":0,"hinzurechnungen":{"schuldzinsen":0,"mietenImmobilien":0,"mietenMobilien":0,"lizenzKonzession":0},"kuerzungen":{"einheitswertGrundbesitz":0,"beteiligungsErtrag":0},"gewerbeertrag":0,"hebesatz":0,"steuer":0,"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","rechtsform":"string","unternehmensname":"string","steuernummer":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"},"berechnung":{"hinzurechnungenRohSumme":0,"hinzurechnungenFreibetrag":0,"hinzurechnungenAnteil":0,"kuerzungenSumme":0,"gewerbeertagVorRundung":0,"freibetragUnternehmen":0,"gewerbeertrag":0,"steuermessbetrag":0,"gewerbesteuer":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"getApiV1ElsterGewerbesteuerById","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Gewerbesteuer-Erklärung Detail. `berechnung` wird bei jedem Aufruf neu gerechnet und kann daher von den gespeicherten Feldern abweichen, wenn sich der Rechenweg geändert hat.","summary":"Gewerbesteuer-Erklärung Detail","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Aktualisiert, mit neu gerechnetem Rechenweg","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerung":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Erhebungszeitraum (Kalenderjahr)"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"gewinn":{"type":"number","description":"Gewinn oder Verlust laut Steuerbilanz"},"hinzurechnungen":{"type":"object","properties":{"schuldzinsen":{"type":"number","minimum":0,"description":"Schuldzinsen, §8 Nr. 1a GewStG"},"mietenImmobilien":{"type":"number","minimum":0,"description":"Mieten und Pachten fuer Grundbesitz, §8 Nr. 1e GewStG"},"mietenMobilien":{"type":"number","minimum":0,"description":"Mieten und Pachten fuer bewegliche Wirtschaftsgueter, §8 Nr. 1d GewStG"},"lizenzKonzession":{"type":"number","minimum":0,"description":"Lizenz- und Konzessionsentgelte, §8 Nr. 1f GewStG"}},"required":["schuldzinsen","mietenImmobilien","mietenMobilien","lizenzKonzession"],"description":"Hinzurechnungen nach §8 GewStG, wie erfasst"},"kuerzungen":{"type":"object","properties":{"einheitswertGrundbesitz":{"type":"number","minimum":0,"description":"1,2 % des Einheitswerts des Grundbesitzes, §9 Nr. 1 GewStG"},"beteiligungsErtrag":{"type":"number","minimum":0,"description":"Gewinnanteile aus Beteiligungen, §9 Nr. 2a GewStG"}},"required":["einheitswertGrundbesitz","beteiligungsErtrag"],"description":"Kuerzungen nach §9 GewStG, wie erfasst"},"gewerbeertrag":{"type":["number","null"],"description":"Abgerundeter Gewerbeertrag, §11 Abs. 1 Nr. 2 GewStG; null solange nicht berechnet"},"hebesatz":{"type":"number","minimum":0,"maximum":2000,"description":"Hebesatz der Gemeinde in Prozent (z. B. 400)"},"steuer":{"type":["number","null"],"description":"Errechnete Gewerbesteuer; null solange nicht berechnet"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"rechtsform":{"type":["string","null"],"minLength":1,"description":"Rechtsform; entscheidet ueber den Freibetrag nach §11 Abs. 1 Nr. 1 GewStG"},"unternehmensname":{"type":["string","null"],"minLength":1,"description":"Name des Unternehmens fuer den Datenlieferanten-Block"},"steuernummer":{"type":["string","null"],"minLength":1,"description":"Steuernummer im ELSTER-Format"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","jahr","status","gewinn","hinzurechnungen","kuerzungen","gewerbeertrag","hebesatz","steuer","xmlPath","eingereichtAm","elsterTransferTicket","rechtsform","unternehmensname","steuernummer","createdAt","updatedAt"],"description":"Der gespeicherte Stand"},"berechnung":{"type":"object","properties":{"hinzurechnungenRohSumme":{"type":"number","description":"Summe der Hinzurechnungen vor Freibetrag und 25-%-Ansatz"},"hinzurechnungenFreibetrag":{"type":"number","description":"Angesetzter Freibetrag auf die Rohsumme (200.000 EUR, §8 Nr. 1 GewStG)"},"hinzurechnungenAnteil":{"type":"number","description":"25 % der Rohsumme nach Freibetrag, mindestens 0"},"kuerzungenSumme":{"type":"number","description":"Summe der Kuerzungen nach §9 GewStG"},"gewerbeertagVorRundung":{"type":"number","description":"Gewinn plus Hinzurechnungsanteil minus Kuerzungen, ungerundet"},"freibetragUnternehmen":{"type":"number","description":"Freibetrag fuer Einzelunternehmen und Personengesellschaften (24.500 EUR); 0 bei GmbH/AG"},"gewerbeertrag":{"type":"number","minimum":0,"description":"Auf volle 100 EUR abgerundeter Gewerbeertrag, mindestens 0"},"steuermessbetrag":{"type":"number","description":"Gewerbeertrag mal 3,5 %, §11 Abs. 2 GewStG"},"gewerbesteuer":{"type":"number","description":"Steuermessbetrag mal Hebesatz geteilt durch 100"}},"required":["hinzurechnungenRohSumme","hinzurechnungenFreibetrag","hinzurechnungenAnteil","kuerzungenSumme","gewerbeertagVorRundung","freibetragUnternehmen","gewerbeertrag","steuermessbetrag","gewerbesteuer"],"description":"Der zugehoerige Rechenweg"}},"required":["erklaerung","berechnung"]},"example":{"erklaerung":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"status":"entwurf","gewinn":0,"hinzurechnungen":{"schuldzinsen":0,"mietenImmobilien":0,"mietenMobilien":0,"lizenzKonzession":0},"kuerzungen":{"einheitswertGrundbesitz":0,"beteiligungsErtrag":0},"gewerbeertrag":0,"hebesatz":0,"steuer":0,"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","rechtsform":"string","unternehmensname":"string","steuernummer":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"},"berechnung":{"hinzurechnungenRohSumme":0,"hinzurechnungenFreibetrag":0,"hinzurechnungenAnteil":0,"kuerzungenSumme":0,"gewerbeertagVorRundung":0,"freibetragUnternehmen":0,"gewerbeertrag":0,"steuermessbetrag":0,"gewerbesteuer":0}}}}},"400":{"description":"Nur Entwürfe können geändert werden (Klartext)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"patchApiV1ElsterGewerbesteuerById","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Gewerbesteuer-Erklärung aktualisieren und neu berechnen. Nur möglich wenn status=entwurf.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"gewinn":{"type":"number"},"hinzurechnungen":{"type":"object","properties":{"schuldzinsen":{"type":"number","minimum":0,"default":0},"mietenImmobilien":{"type":"number","minimum":0,"default":0},"mietenMobilien":{"type":"number","minimum":0,"default":0},"lizenzKonzession":{"type":"number","minimum":0,"default":0}},"default":{}},"kuerzungen":{"type":"object","properties":{"einheitswertGrundbesitz":{"type":"number","minimum":0,"default":0},"beteiligungsErtrag":{"type":"number","minimum":0,"default":0}},"default":{}},"hebesatz":{"type":"number","minimum":0,"maximum":2000,"default":400},"rechtsform":{"type":"string"},"unternehmensname":{"type":"string"},"steuernummer":{"type":"string"}}},"example":{"gewinn":0,"hinzurechnungen":{"schuldzinsen":0,"mietenImmobilien":0,"mietenMobilien":0,"lizenzKonzession":0},"kuerzungen":{"einheitswertGrundbesitz":0,"beteiligungsErtrag":0},"hebesatz":0,"rechtsform":"string","unternehmensname":"string","steuernummer":"string"}}}},"summary":"Gewerbesteuer-Erklärung aktualisieren und neu berechnen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/gewerbesteuer/{id}/generate-xml":{"post":{"responses":{"200":{"description":"XML erzeugt; validation nennt fehlende Pflichtelemente","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung, fuer die erzeugt wurde"},"xml_path":{"type":"string","minLength":1,"description":"Ablageort der geschriebenen XML-Datei"},"validation":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn alle Pflichtelemente im XML stehen"},"errors":{"type":"array","items":{"type":"string","minLength":1},"description":"Klartext je fehlendem Pflichtelement; leer wenn ok"}},"required":["ok","errors"],"description":"Ergebnis der Strukturpruefung — eine Element-Pruefung, KEINE ERiC-Validierung"}},"required":["id","xml_path","validation"]},"example":{"id":"00000000-0000-4000-8000-000000000000","xml_path":"string","validation":{"ok":true,"errors":["string"]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"postApiV1ElsterGewerbesteuerByIdGenerate-xml","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"ELSTER GewSt-XML erzeugen, auf Platte ablegen und auf Pflichtelemente prüfen. Antwortet mit JSON (Pfad + Prüfergebnis), nicht mit der Datei — die liegt unter GET /:id/xml.","summary":"ELSTER GewSt-XML erzeugen, auf Platte ablegen und auf Pflichtelemente prüfen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/gewerbesteuer/{id}/submit":{"post":{"responses":{"200":{"description":"Als eingereicht vermerkt — mit STUB-Quittung, ohne echte ELSTER-Übertragung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der eingereichten Erklaerung"},"status":{"type":"string","const":"eingereicht","description":"Neuer Stand der Erklaerung"},"elsterTransferTicket":{"type":"string","minLength":1,"description":"Quittungsnummer. Solange die ERiC-Anbindung fehlt, beginnt sie mit \"STUB-\" und stammt nicht vom Finanzamt."}},"required":["id","status","elsterTransferTicket"]},"example":{"id":"00000000-0000-4000-8000-000000000000","status":"eingereicht","elsterTransferTicket":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"ELSTER_SUBMIT_ENABLED nicht gesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"submit_disabled","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext mit dem Hinweis auf den manuellen Upload"}},"required":["error","message"]}}}},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"postApiV1ElsterGewerbesteuerByIdSubmit","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Reicht die Gewerbesteuer-Erklaerung bei ELSTER ein","description":"Gewerbesteuer-Erklärung bei ELSTER einreichen (GATED: erfordert ELSTER_SUBMIT_ENABLED=true). Andernfalls: XML-Download für manuellen Upload via Mein-ELSTER. ACHTUNG — die ERiC-Anbindung fehlt noch: die Erklärung wird auf \"eingereicht\" gesetzt und bekommt eine selbst erzeugte Quittungsnummer mit Präfix \"STUB-\". Es geht nichts ans Finanzamt."}},"/api/v1/elster/gewerbesteuer/{id}/xml":{"get":{"responses":{"200":{"description":"XML-Datei als Anhang (Content-Type application/xml)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Erklärung oder XML nicht gefunden (Klartext)"}},"operationId":"getApiV1ElsterGewerbesteuerByIdXml","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"ELSTER GewSt-XML als Datei herunterladen (application/xml, kein JSON-Rumpf)","description":"Liefert die zuvor erzeugte XML-Datei als Anhang, benannt nach dem Jahr (etwa `GewSt-2026.xml`). Erzeugt wird hier NICHTS: wurde POST /{id}/generate-xml noch nicht aufgerufen oder liegt die Datei nicht mehr auf der Platte, antwortet die Route mit 404. Der Rumpf ist die XML-Datei selbst, kein JSON."}},"/api/v1/elster/kst-eur":{"get":{"responses":{"200":{"description":"Liste der Erklärungen mit Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerungen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Veranlagungszeitraum (Kalenderjahr)"},"erklaerungsTyp":{"type":"string","enum":["KSt","EUR"],"description":"Aus der Rechtsform abgeleitet: KSt fuer Koerperschaften, sonst EUR"},"rechtsform":{"type":"string","enum":["GmbH","AG","UG","Einzelunternehmen","GbR","OHG","KG","PersGes"],"description":"Rechtsform des Unternehmens"},"unternehmensname":{"type":"string","minLength":1,"maxLength":200,"description":"Name des Unternehmens"},"steuernummer":{"type":"string","minLength":1,"maxLength":30,"description":"Steuernummer im ELSTER-Format"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"eingaben":{"anyOf":[{"type":"object","properties":{"zuVersteuerndesEinkommen":{"type":"number","description":"Zu versteuerndes Einkommen vor Korrekturen"},"nichtAbziehbareAufwendungen":{"type":"number","description":"Nicht abziehbare Aufwendungen, §10 KStG — erhoehen die Bemessungsgrundlage"},"verlustVortrag":{"type":"number","description":"Verlustvortrag aus Vorjahren — mindert die Bemessungsgrundlage"},"freibetraegeBesondere":{"type":"number","description":"Besondere Freibetraege, z. B. §24 KStG"}},"required":["zuVersteuerndesEinkommen"]},{"type":"object","properties":{"einnahmen":{"type":"number","minimum":0,"description":"Summe der Betriebseinnahmen"},"ausgaben":{"type":"object","properties":{"wareneinkauf":{"type":"number","minimum":0,"description":"Wareneinkauf"},"miete":{"type":"number","minimum":0,"description":"Miete und Pacht"},"personal":{"type":"number","minimum":0,"description":"Personalaufwand"},"abschreibungen":{"type":"number","minimum":0,"description":"Abschreibungen"},"kfz":{"type":"number","minimum":0,"description":"Fahrzeugkosten"},"telefon":{"type":"number","minimum":0,"description":"Telefon und Kommunikation"},"sonstige":{"type":"number","minimum":0,"description":"Sonstige Betriebsausgaben"}},"required":["wareneinkauf","miete","personal","abschreibungen","kfz","telefon","sonstige"],"description":"Betriebsausgaben nach Gruppen"},"anlage13a":{"type":"boolean","description":"Anlage 13a: Land- und Forstwirtschaft"}},"required":["einnahmen","ausgaben"]}],"description":"Die erfassten Werte — Form richtet sich nach erklaerungsTyp"},"ergebnis":{"anyOf":[{"type":"object","properties":{"bemessungsgrundlage":{"type":"number","minimum":0,"description":"Bemessungsgrundlage nach Korrekturen, auf 0 begrenzt"},"kst":{"type":"number","description":"Koerperschaftsteuer: 15 % der Bemessungsgrundlage"},"solz":{"type":"number","description":"Solidaritaetszuschlag: 5,5 % der Koerperschaftsteuer"},"gesamt":{"type":"number","description":"Summe aus Koerperschaftsteuer und Solidaritaetszuschlag"}},"required":["bemessungsgrundlage","kst","solz","gesamt"],"description":"Rechenergebnis der Koerperschaftsteuer"},{"type":"object","properties":{"summeAusgaben":{"type":"number","minimum":0,"description":"Summe aller Betriebsausgaben"},"gewinn":{"type":"number","description":"Gewinn: Einnahmen minus Ausgaben"}},"required":["summeAusgaben","gewinn"],"description":"Rechenergebnis der Einnahmenueberschussrechnung"}],"description":"Das gespeicherte Rechenergebnis — Form richtet sich nach erklaerungsTyp"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","jahr","erklaerungsTyp","rechtsform","unternehmensname","steuernummer","status","eingaben","ergebnis","xmlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Eine KSt- oder EUeR-Erklaerung mit Eingaben, Ergebnis und Einreichungsstand"},"description":"Die Erklaerungen der aktuellen Seite"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Erklaerungen, die dem Filter entsprechen"}},"required":["erklaerungen","total"]},"example":{"erklaerungen":[{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"erklaerungsTyp":"KSt","rechtsform":"GmbH","unternehmensname":"string","steuernummer":"string","status":"entwurf","eingaben":{"zuVersteuerndesEinkommen":0,"nichtAbziehbareAufwendungen":0,"verlustVortrag":0,"freibetraegeBesondere":0},"ergebnis":{"bemessungsgrundlage":0,"kst":0,"solz":0,"gesamt":0},"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1ElsterKst-eur","tags":["elster"],"parameters":[{"in":"query","name":"jahr","schema":{"type":"integer","minimum":2000,"maximum":2099}},{"in":"query","name":"typ","schema":{"type":"string","enum":["KSt","EUR"]}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"summary":"Liste aller KSt/EÜR-Erklärungen (Filter: jahr, typ)","description":"Liest `kst_eur_erklaerung` im Mandanten-Schema, sortiert nach Jahr absteigend und darin nach Erklärungstyp. `jahr` und `typ` filtern, `limit` (Vorgabe 50, höchstens 200) und `offset` blättern; `total` zählt alle Treffer des Filters, nicht nur die ausgelieferte Seite. Die Tabelle wird beim ersten Aufruf angelegt — ein Mandant ohne Erklärungen bekommt eine leere Liste, keinen Fehler. Der ganze Router verlangt mindestens die Rolle `manager`."},"post":{"responses":{"201":{"description":"Erklärung angelegt oder überschrieben, Ergebnis frisch gerechnet","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerung":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Veranlagungszeitraum (Kalenderjahr)"},"erklaerungsTyp":{"type":"string","enum":["KSt","EUR"],"description":"Aus der Rechtsform abgeleitet: KSt fuer Koerperschaften, sonst EUR"},"rechtsform":{"type":"string","enum":["GmbH","AG","UG","Einzelunternehmen","GbR","OHG","KG","PersGes"],"description":"Rechtsform des Unternehmens"},"unternehmensname":{"type":"string","minLength":1,"maxLength":200,"description":"Name des Unternehmens"},"steuernummer":{"type":"string","minLength":1,"maxLength":30,"description":"Steuernummer im ELSTER-Format"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"eingaben":{"anyOf":[{"type":"object","properties":{"zuVersteuerndesEinkommen":{"type":"number","description":"Zu versteuerndes Einkommen vor Korrekturen"},"nichtAbziehbareAufwendungen":{"type":"number","description":"Nicht abziehbare Aufwendungen, §10 KStG — erhoehen die Bemessungsgrundlage"},"verlustVortrag":{"type":"number","description":"Verlustvortrag aus Vorjahren — mindert die Bemessungsgrundlage"},"freibetraegeBesondere":{"type":"number","description":"Besondere Freibetraege, z. B. §24 KStG"}},"required":["zuVersteuerndesEinkommen"]},{"type":"object","properties":{"einnahmen":{"type":"number","minimum":0,"description":"Summe der Betriebseinnahmen"},"ausgaben":{"type":"object","properties":{"wareneinkauf":{"type":"number","minimum":0,"description":"Wareneinkauf"},"miete":{"type":"number","minimum":0,"description":"Miete und Pacht"},"personal":{"type":"number","minimum":0,"description":"Personalaufwand"},"abschreibungen":{"type":"number","minimum":0,"description":"Abschreibungen"},"kfz":{"type":"number","minimum":0,"description":"Fahrzeugkosten"},"telefon":{"type":"number","minimum":0,"description":"Telefon und Kommunikation"},"sonstige":{"type":"number","minimum":0,"description":"Sonstige Betriebsausgaben"}},"required":["wareneinkauf","miete","personal","abschreibungen","kfz","telefon","sonstige"],"description":"Betriebsausgaben nach Gruppen"},"anlage13a":{"type":"boolean","description":"Anlage 13a: Land- und Forstwirtschaft"}},"required":["einnahmen","ausgaben"]}],"description":"Die erfassten Werte — Form richtet sich nach erklaerungsTyp"},"ergebnis":{"anyOf":[{"type":"object","properties":{"bemessungsgrundlage":{"type":"number","minimum":0,"description":"Bemessungsgrundlage nach Korrekturen, auf 0 begrenzt"},"kst":{"type":"number","description":"Koerperschaftsteuer: 15 % der Bemessungsgrundlage"},"solz":{"type":"number","description":"Solidaritaetszuschlag: 5,5 % der Koerperschaftsteuer"},"gesamt":{"type":"number","description":"Summe aus Koerperschaftsteuer und Solidaritaetszuschlag"}},"required":["bemessungsgrundlage","kst","solz","gesamt"],"description":"Rechenergebnis der Koerperschaftsteuer"},{"type":"object","properties":{"summeAusgaben":{"type":"number","minimum":0,"description":"Summe aller Betriebsausgaben"},"gewinn":{"type":"number","description":"Gewinn: Einnahmen minus Ausgaben"}},"required":["summeAusgaben","gewinn"],"description":"Rechenergebnis der Einnahmenueberschussrechnung"}],"description":"Das gespeicherte Rechenergebnis — Form richtet sich nach erklaerungsTyp"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","jahr","erklaerungsTyp","rechtsform","unternehmensname","steuernummer","status","eingaben","ergebnis","xmlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Der gespeicherte Stand der Erklaerung"}},"required":["erklaerung"]},"example":{"erklaerung":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"erklaerungsTyp":"KSt","rechtsform":"GmbH","unternehmensname":"string","steuernummer":"string","status":"entwurf","eingaben":{"zuVersteuerndesEinkommen":0,"nichtAbziehbareAufwendungen":0,"verlustVortrag":0,"freibetraegeBesondere":0},"ergebnis":{"bemessungsgrundlage":0,"kst":0,"solz":0,"gesamt":0},"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ElsterKst-eur","tags":["elster"],"parameters":[],"description":"Neue KSt/EÜR-Erklärung anlegen. Typ wird aus Rechtsform abgeleitet. Existiert für Mandant, Jahr und Typ bereits eine Erklärung, wird sie überschrieben (ON CONFLICT DO UPDATE) — die Antwort ist auch dann 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jahr":{"type":"integer","minimum":2000,"maximum":2099},"rechtsform":{"type":"string","enum":["GmbH","AG","UG","Einzelunternehmen","GbR","OHG","KG","PersGes"]},"unternehmensname":{"type":"string","minLength":1,"maxLength":200},"steuernummer":{"type":"string","minLength":1,"maxLength":30},"eingaben":{"anyOf":[{"type":"object","properties":{"zuVersteuerndesEinkommen":{"type":"number","description":"Zu versteuerndes Einkommen vor Korrekturen"},"nichtAbziehbareAufwendungen":{"type":"number","description":"Nicht abziehbare Aufwendungen, §10 KStG — erhoehen die Bemessungsgrundlage"},"verlustVortrag":{"type":"number","description":"Verlustvortrag aus Vorjahren — mindert die Bemessungsgrundlage"},"freibetraegeBesondere":{"type":"number","description":"Besondere Freibetraege, z. B. §24 KStG"}},"required":["zuVersteuerndesEinkommen"]},{"type":"object","properties":{"einnahmen":{"type":"number","minimum":0,"description":"Summe der Betriebseinnahmen"},"ausgaben":{"type":"object","properties":{"wareneinkauf":{"type":"number","minimum":0,"description":"Wareneinkauf"},"miete":{"type":"number","minimum":0,"description":"Miete und Pacht"},"personal":{"type":"number","minimum":0,"description":"Personalaufwand"},"abschreibungen":{"type":"number","minimum":0,"description":"Abschreibungen"},"kfz":{"type":"number","minimum":0,"description":"Fahrzeugkosten"},"telefon":{"type":"number","minimum":0,"description":"Telefon und Kommunikation"},"sonstige":{"type":"number","minimum":0,"description":"Sonstige Betriebsausgaben"}},"required":["wareneinkauf","miete","personal","abschreibungen","kfz","telefon","sonstige"],"description":"Betriebsausgaben nach Gruppen"},"anlage13a":{"type":"boolean","description":"Anlage 13a: Land- und Forstwirtschaft"}},"required":["einnahmen","ausgaben"]}]}},"required":["jahr","rechtsform","unternehmensname","steuernummer","eingaben"]},"example":{"jahr":2000,"rechtsform":"GmbH","unternehmensname":"string","steuernummer":"string","eingaben":{"zuVersteuerndesEinkommen":0,"nichtAbziehbareAufwendungen":0,"verlustVortrag":0,"freibetraegeBesondere":0}}}}},"summary":"Neue KSt/EÜR-Erklärung anlegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/kst-eur/{id}":{"get":{"responses":{"200":{"description":"Erklärung Detail mit gespeicherten Eingaben und Ergebnis","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerung":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Veranlagungszeitraum (Kalenderjahr)"},"erklaerungsTyp":{"type":"string","enum":["KSt","EUR"],"description":"Aus der Rechtsform abgeleitet: KSt fuer Koerperschaften, sonst EUR"},"rechtsform":{"type":"string","enum":["GmbH","AG","UG","Einzelunternehmen","GbR","OHG","KG","PersGes"],"description":"Rechtsform des Unternehmens"},"unternehmensname":{"type":"string","minLength":1,"maxLength":200,"description":"Name des Unternehmens"},"steuernummer":{"type":"string","minLength":1,"maxLength":30,"description":"Steuernummer im ELSTER-Format"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"eingaben":{"anyOf":[{"type":"object","properties":{"zuVersteuerndesEinkommen":{"type":"number","description":"Zu versteuerndes Einkommen vor Korrekturen"},"nichtAbziehbareAufwendungen":{"type":"number","description":"Nicht abziehbare Aufwendungen, §10 KStG — erhoehen die Bemessungsgrundlage"},"verlustVortrag":{"type":"number","description":"Verlustvortrag aus Vorjahren — mindert die Bemessungsgrundlage"},"freibetraegeBesondere":{"type":"number","description":"Besondere Freibetraege, z. B. §24 KStG"}},"required":["zuVersteuerndesEinkommen"]},{"type":"object","properties":{"einnahmen":{"type":"number","minimum":0,"description":"Summe der Betriebseinnahmen"},"ausgaben":{"type":"object","properties":{"wareneinkauf":{"type":"number","minimum":0,"description":"Wareneinkauf"},"miete":{"type":"number","minimum":0,"description":"Miete und Pacht"},"personal":{"type":"number","minimum":0,"description":"Personalaufwand"},"abschreibungen":{"type":"number","minimum":0,"description":"Abschreibungen"},"kfz":{"type":"number","minimum":0,"description":"Fahrzeugkosten"},"telefon":{"type":"number","minimum":0,"description":"Telefon und Kommunikation"},"sonstige":{"type":"number","minimum":0,"description":"Sonstige Betriebsausgaben"}},"required":["wareneinkauf","miete","personal","abschreibungen","kfz","telefon","sonstige"],"description":"Betriebsausgaben nach Gruppen"},"anlage13a":{"type":"boolean","description":"Anlage 13a: Land- und Forstwirtschaft"}},"required":["einnahmen","ausgaben"]}],"description":"Die erfassten Werte — Form richtet sich nach erklaerungsTyp"},"ergebnis":{"anyOf":[{"type":"object","properties":{"bemessungsgrundlage":{"type":"number","minimum":0,"description":"Bemessungsgrundlage nach Korrekturen, auf 0 begrenzt"},"kst":{"type":"number","description":"Koerperschaftsteuer: 15 % der Bemessungsgrundlage"},"solz":{"type":"number","description":"Solidaritaetszuschlag: 5,5 % der Koerperschaftsteuer"},"gesamt":{"type":"number","description":"Summe aus Koerperschaftsteuer und Solidaritaetszuschlag"}},"required":["bemessungsgrundlage","kst","solz","gesamt"],"description":"Rechenergebnis der Koerperschaftsteuer"},{"type":"object","properties":{"summeAusgaben":{"type":"number","minimum":0,"description":"Summe aller Betriebsausgaben"},"gewinn":{"type":"number","description":"Gewinn: Einnahmen minus Ausgaben"}},"required":["summeAusgaben","gewinn"],"description":"Rechenergebnis der Einnahmenueberschussrechnung"}],"description":"Das gespeicherte Rechenergebnis — Form richtet sich nach erklaerungsTyp"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","jahr","erklaerungsTyp","rechtsform","unternehmensname","steuernummer","status","eingaben","ergebnis","xmlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Der gespeicherte Stand der Erklaerung"}},"required":["erklaerung"]},"example":{"erklaerung":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"erklaerungsTyp":"KSt","rechtsform":"GmbH","unternehmensname":"string","steuernummer":"string","status":"entwurf","eingaben":{"zuVersteuerndesEinkommen":0,"nichtAbziehbareAufwendungen":0,"verlustVortrag":0,"freibetraegeBesondere":0},"ergebnis":{"bemessungsgrundlage":0,"kst":0,"solz":0,"gesamt":0},"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"getApiV1ElsterKst-eurById","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"KSt/EÜR-Erklärung Detail","description":"Liefert eine einzelne Erklärung mit den gespeicherten `eingaben` und dem zuletzt gerechneten `ergebnis`; die Form beider Felder richtet sich nach `erklaerungsTyp` (KSt oder EUR). Die Abfrage filtert auf `id` UND `tenant_id` — eine fremde Kennung führt zu 404, nicht zu einem fremden Datensatz. `xmlPath`, `eingereichtAm` und `elsterTransferTicket` bleiben null, solange kein XML erzeugt und nichts eingereicht wurde."},"patch":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerung":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"jahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Veranlagungszeitraum (Kalenderjahr)"},"erklaerungsTyp":{"type":"string","enum":["KSt","EUR"],"description":"Aus der Rechtsform abgeleitet: KSt fuer Koerperschaften, sonst EUR"},"rechtsform":{"type":"string","enum":["GmbH","AG","UG","Einzelunternehmen","GbR","OHG","KG","PersGes"],"description":"Rechtsform des Unternehmens"},"unternehmensname":{"type":"string","minLength":1,"maxLength":200,"description":"Name des Unternehmens"},"steuernummer":{"type":"string","minLength":1,"maxLength":30,"description":"Steuernummer im ELSTER-Format"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XML erzeugt, an ELSTER uebergeben"},"eingaben":{"anyOf":[{"type":"object","properties":{"zuVersteuerndesEinkommen":{"type":"number","description":"Zu versteuerndes Einkommen vor Korrekturen"},"nichtAbziehbareAufwendungen":{"type":"number","description":"Nicht abziehbare Aufwendungen, §10 KStG — erhoehen die Bemessungsgrundlage"},"verlustVortrag":{"type":"number","description":"Verlustvortrag aus Vorjahren — mindert die Bemessungsgrundlage"},"freibetraegeBesondere":{"type":"number","description":"Besondere Freibetraege, z. B. §24 KStG"}},"required":["zuVersteuerndesEinkommen"]},{"type":"object","properties":{"einnahmen":{"type":"number","minimum":0,"description":"Summe der Betriebseinnahmen"},"ausgaben":{"type":"object","properties":{"wareneinkauf":{"type":"number","minimum":0,"description":"Wareneinkauf"},"miete":{"type":"number","minimum":0,"description":"Miete und Pacht"},"personal":{"type":"number","minimum":0,"description":"Personalaufwand"},"abschreibungen":{"type":"number","minimum":0,"description":"Abschreibungen"},"kfz":{"type":"number","minimum":0,"description":"Fahrzeugkosten"},"telefon":{"type":"number","minimum":0,"description":"Telefon und Kommunikation"},"sonstige":{"type":"number","minimum":0,"description":"Sonstige Betriebsausgaben"}},"required":["wareneinkauf","miete","personal","abschreibungen","kfz","telefon","sonstige"],"description":"Betriebsausgaben nach Gruppen"},"anlage13a":{"type":"boolean","description":"Anlage 13a: Land- und Forstwirtschaft"}},"required":["einnahmen","ausgaben"]}],"description":"Die erfassten Werte — Form richtet sich nach erklaerungsTyp"},"ergebnis":{"anyOf":[{"type":"object","properties":{"bemessungsgrundlage":{"type":"number","minimum":0,"description":"Bemessungsgrundlage nach Korrekturen, auf 0 begrenzt"},"kst":{"type":"number","description":"Koerperschaftsteuer: 15 % der Bemessungsgrundlage"},"solz":{"type":"number","description":"Solidaritaetszuschlag: 5,5 % der Koerperschaftsteuer"},"gesamt":{"type":"number","description":"Summe aus Koerperschaftsteuer und Solidaritaetszuschlag"}},"required":["bemessungsgrundlage","kst","solz","gesamt"],"description":"Rechenergebnis der Koerperschaftsteuer"},{"type":"object","properties":{"summeAusgaben":{"type":"number","minimum":0,"description":"Summe aller Betriebsausgaben"},"gewinn":{"type":"number","description":"Gewinn: Einnahmen minus Ausgaben"}},"required":["summeAusgaben","gewinn"],"description":"Rechenergebnis der Einnahmenueberschussrechnung"}],"description":"Das gespeicherte Rechenergebnis — Form richtet sich nach erklaerungsTyp"},"xmlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XML-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","jahr","erklaerungsTyp","rechtsform","unternehmensname","steuernummer","status","eingaben","ergebnis","xmlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Der gespeicherte Stand der Erklaerung"}},"required":["erklaerung"]},"example":{"erklaerung":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","jahr":2000,"erklaerungsTyp":"KSt","rechtsform":"GmbH","unternehmensname":"string","steuernummer":"string","status":"entwurf","eingaben":{"zuVersteuerndesEinkommen":0,"nichtAbziehbareAufwendungen":0,"verlustVortrag":0,"freibetraegeBesondere":0},"ergebnis":{"bemessungsgrundlage":0,"kst":0,"solz":0,"gesamt":0},"xmlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nicht mehr editierbar — Status ist nicht mehr entwurf (Klartext)"},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"patchApiV1ElsterKst-eurById","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"KSt/EÜR-Erklärung aktualisieren (nur im Status entwurf). Das Ergebnis wird nur neu gerechnet, wenn `eingaben` mitgeschickt wird.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"unternehmensname":{"type":"string","minLength":1,"maxLength":200},"steuernummer":{"type":"string","minLength":1,"maxLength":30},"eingaben":{"anyOf":[{"type":"object","properties":{"zuVersteuerndesEinkommen":{"type":"number","description":"Zu versteuerndes Einkommen vor Korrekturen"},"nichtAbziehbareAufwendungen":{"type":"number","description":"Nicht abziehbare Aufwendungen, §10 KStG — erhoehen die Bemessungsgrundlage"},"verlustVortrag":{"type":"number","description":"Verlustvortrag aus Vorjahren — mindert die Bemessungsgrundlage"},"freibetraegeBesondere":{"type":"number","description":"Besondere Freibetraege, z. B. §24 KStG"}},"required":["zuVersteuerndesEinkommen"]},{"type":"object","properties":{"einnahmen":{"type":"number","minimum":0,"description":"Summe der Betriebseinnahmen"},"ausgaben":{"type":"object","properties":{"wareneinkauf":{"type":"number","minimum":0,"description":"Wareneinkauf"},"miete":{"type":"number","minimum":0,"description":"Miete und Pacht"},"personal":{"type":"number","minimum":0,"description":"Personalaufwand"},"abschreibungen":{"type":"number","minimum":0,"description":"Abschreibungen"},"kfz":{"type":"number","minimum":0,"description":"Fahrzeugkosten"},"telefon":{"type":"number","minimum":0,"description":"Telefon und Kommunikation"},"sonstige":{"type":"number","minimum":0,"description":"Sonstige Betriebsausgaben"}},"required":["wareneinkauf","miete","personal","abschreibungen","kfz","telefon","sonstige"],"description":"Betriebsausgaben nach Gruppen"},"anlage13a":{"type":"boolean","description":"Anlage 13a: Land- und Forstwirtschaft"}},"required":["einnahmen","ausgaben"]}]}}},"example":{"unternehmensname":"string","steuernummer":"string","eingaben":{"zuVersteuerndesEinkommen":0,"nichtAbziehbareAufwendungen":0,"verlustVortrag":0,"freibetraegeBesondere":0}}}}},"summary":"KSt/EÜR-Erklärung aktualisieren (nur im Status entwurf)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/kst-eur/{id}/generate-xml":{"post":{"responses":{"200":{"description":"XML erzeugt; validation nennt fehlende Pflichtelemente","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung, fuer die erzeugt wurde"},"xml_path":{"type":"string","minLength":1,"description":"Ablageort der geschriebenen XML-Datei"},"validation":{"type":"object","properties":{"ok":{"type":"boolean","description":"true, wenn alle Pflichtelemente im XML stehen"},"errors":{"type":"array","items":{"type":"string","minLength":1},"description":"Klartext je fehlendem Pflichtelement; leer wenn ok"}},"required":["ok","errors"],"description":"Ergebnis der Strukturpruefung — eine Element-Pruefung, KEINE ERiC-Validierung"}},"required":["id","xml_path","validation"]},"example":{"id":"00000000-0000-4000-8000-000000000000","xml_path":"string","validation":{"ok":true,"errors":["string"]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Erklärung nicht gefunden (Klartext)"}},"operationId":"postApiV1ElsterKst-eurByIdGenerate-xml","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Erzeugt das ELSTER-XML zur KSt- oder EUeR-Erklaerung","description":"ELSTER-XML für KSt/EÜR-Erklärung erzeugen, auf Platte ablegen und auf Pflichtelemente prüfen. Antwortet mit JSON (Pfad + Prüfergebnis), nicht mit der Datei — die liegt unter GET /:id/xml."}},"/api/v1/elster/kst-eur/{id}/submit":{"post":{"responses":{"200":{"description":"Als eingereicht vermerkt — mit STUB-Quittung, ohne echte ELSTER-Übertragung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der eingereichten Erklaerung"},"status":{"type":"string","const":"eingereicht","description":"Neuer Stand der Erklaerung"},"elsterTransferTicket":{"type":"string","minLength":1,"description":"Quittungsnummer. Solange die ERiC-Anbindung fehlt, beginnt sie mit \"STUB-\" und stammt nicht vom Finanzamt."}},"required":["id","status","elsterTransferTicket"]},"example":{"id":"00000000-0000-4000-8000-000000000000","status":"eingereicht","elsterTransferTicket":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"ELSTER_SUBMIT_ENABLED nicht gesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"elster_submit_disabled","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext mit dem Hinweis auf den manuellen Upload"}},"required":["error","message"]}}}},"404":{"description":"Erklärung nicht gefunden (Klartext)"}},"operationId":"postApiV1ElsterKst-eurByIdSubmit","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Erklärung bei ELSTER einreichen (GATED: erfordert ELSTER_SUBMIT_ENABLED=true). Andernfalls: XML-Download für manuellen Upload via Mein-ELSTER. ACHTUNG — die ERiC-Anbindung fehlt noch: die Erklärung wird auf \"eingereicht\" gesetzt und bekommt eine selbst erzeugte Quittungsnummer mit Präfix \"STUB-\". Es geht nichts ans Finanzamt.","summary":"Erklärung bei ELSTER einreichen (GATED: erfordert ELSTER_SUBMIT_ENABLED=true)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/kst-eur/{id}/xml":{"get":{"responses":{"200":{"description":"XML-Datei als Anhang (Content-Type text/xml, Content-Disposition attachment)","content":{"text/xml":{"schema":{"type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Erklärung oder XML nicht gefunden (Klartext)"}},"operationId":"getApiV1ElsterKst-eurByIdXml","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"ELSTER-XML als Datei herunterladen (text/xml, kein JSON-Rumpf)","description":"Liest die beim Erzeugen abgelegte Datei von `xml_path` und gibt sie unverändert zurück — als Anhang mit dem Namen `<Typ>-<Jahr>.xml`. Der Aufruf ändert nichts am Datensatz und erzeugt insbesondere kein XML nach. 404 kommt in drei Fällen: die Erklärung gehört nicht zu diesem Mandanten oder existiert nicht, `xml_path` ist noch leer (dann zuerst POST /:id/generate-xml), oder die Datei ist am hinterlegten Pfad nicht mehr lesbar."}},"/api/v1/elster/ebilanz":{"get":{"responses":{"200":{"description":"Liste der Erklärungen mit Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerungen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"geschaeftsjahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Wirtschaftsjahr, auf das sich die Bilanz bezieht"},"bilanzart":{"type":"string","enum":["jahresabschluss","eroeffnungsbilanz","zwischenbilanz","liquidationsbilanz"],"description":"Art der Bilanz"},"rechtsform":{"type":["string","null"],"minLength":1,"description":"Rechtsform des Unternehmens"},"unternehmensname":{"type":["string","null"],"minLength":1,"description":"Name des Unternehmens"},"steuernummer":{"type":["string","null"],"minLength":1,"description":"Steuernummer, 13-stellig im ELSTER-Format"},"beraterNr":{"type":["string","null"],"minLength":1,"description":"Beraternummer, 7-stellig"},"mandantNr":{"type":["string","null"],"minLength":1,"description":"Mandantennummer beim Berater, 5-stellig"},"wjBeginn":{"type":["string","null"],"format":"date-time","description":"Beginn des Wirtschaftsjahres; DATE-Spalte, kommt als ISO-Zeitstempel"},"wjEnde":{"type":["string","null"],"format":"date-time","description":"Ende des Wirtschaftsjahres; DATE-Spalte, kommt als ISO-Zeitstempel"},"aktiva":{"type":["object","null"],"properties":{"shareCapNotPaidIn":{"type":"number","description":"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert"},"fixAssets":{"type":"number","description":"Anlagevermoegen: Sachanlagen, immaterielle und Finanzanlagen"},"inventory":{"type":"number","description":"Vorraete"},"receivables":{"type":"number","description":"Forderungen aus Lieferungen und Leistungen"},"cashAndEquivalents":{"type":"number","description":"Liquide Mittel: Kasse und Bank"},"prepaidExpenses":{"type":"number","description":"Aktive Rechnungsabgrenzungsposten"}},"required":["shareCapNotPaidIn","fixAssets","inventory","receivables","cashAndEquivalents","prepaidExpenses"],"description":"Aktiv-Positionen; null solange nichts aggregiert wurde"},"passiva":{"type":["object","null"],"properties":{"subscribedCapital":{"type":"number","description":"Gezeichnetes Kapital"},"reserves":{"type":"number","description":"Kapital- und Gewinnruecklagen samt Bilanzgewinn-Vortrag"},"netIncomeCarryover":{"type":"number","description":"Jahresueberschuss oder -fehlbetrag"},"provisions":{"type":"number","description":"Rueckstellungen"},"liabilities":{"type":"number","description":"Verbindlichkeiten, lang- und kurzfristig"},"deferredIncome":{"type":"number","description":"Passive Rechnungsabgrenzungsposten"}},"required":["subscribedCapital","reserves","netIncomeCarryover","provisions","liabilities","deferredIncome"],"description":"Passiv-Positionen; null solange nichts aggregiert wurde"},"guv":{"type":["object","null"],"properties":{"revenue":{"type":"number","description":"Umsatzerloese"},"otherOperatingIncome":{"type":"number","description":"Sonstige betriebliche Ertraege"},"materialExpense":{"type":"number","description":"Materialaufwand"},"personnelExpense":{"type":"number","description":"Personalaufwand"},"depreciation":{"type":"number","description":"Abschreibungen"},"otherOperatingExpense":{"type":"number","description":"Sonstige betriebliche Aufwendungen"},"interestIncome":{"type":"number","description":"Zinsertraege"},"interestExpense":{"type":"number","description":"Zinsaufwendungen"},"incomeTaxes":{"type":"number","description":"Steuern vom Einkommen und Ertrag"},"otherTaxes":{"type":"number","description":"Sonstige Steuern"}},"required":["revenue","otherOperatingIncome","materialExpense","personnelExpense","depreciation","otherOperatingExpense","interestIncome","interestExpense","incomeTaxes","otherTaxes"],"description":"GuV-Positionen; null solange nichts aggregiert wurde"},"bilanzsumme":{"type":["number","null"],"description":"Bilanzsumme der Aktivseite; null solange nicht berechnet"},"jahresueberschuss":{"type":["number","null"],"description":"Jahresueberschuss aus der GuV; null solange nicht berechnet"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XBRL erzeugt, an ELSTER uebergeben"},"xbrlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XBRL-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","geschaeftsjahr","bilanzart","rechtsform","unternehmensname","steuernummer","beraterNr","mandantNr","wjBeginn","wjEnde","aktiva","passiva","guv","bilanzsumme","jahresueberschuss","status","xbrlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Eine E-Bilanz-Erklaerung mit Bilanz, GuV und Einreichungsstand"},"description":"Die Erklaerungen der aktuellen Seite"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Erklaerungen, die dem Filter entsprechen"}},"required":["erklaerungen","total"]},"example":{"erklaerungen":[{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","geschaeftsjahr":2000,"bilanzart":"jahresabschluss","rechtsform":"string","unternehmensname":"string","steuernummer":"string","beraterNr":"string","mandantNr":"string","wjBeginn":"2026-01-01T12:00:00.000Z","wjEnde":"2026-01-01T12:00:00.000Z","aktiva":{"shareCapNotPaidIn":0,"fixAssets":0,"inventory":0,"receivables":0,"cashAndEquivalents":0,"prepaidExpenses":0},"passiva":{"subscribedCapital":0,"reserves":0,"netIncomeCarryover":0,"provisions":0,"liabilities":0,"deferredIncome":0},"guv":{"revenue":0,"otherOperatingIncome":0,"materialExpense":0,"personnelExpense":0,"depreciation":0,"otherOperatingExpense":0,"interestIncome":0,"interestExpense":0,"incomeTaxes":0,"otherTaxes":0},"bilanzsumme":0,"jahresueberschuss":0,"status":"entwurf","xbrlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"total":0}}}},"401":{"description":"Kein Mandantenkontext"}},"operationId":"getApiV1ElsterEbilanz","tags":["elster"],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"]}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"summary":"Liste der E-Bilanz-Erklaerungen, neuestes Geschaeftsjahr zuerst","description":"Blaettert durch `ebilanz_erklaerung` des Mandanten, neuestes Geschaeftsjahr zuerst. `status` filtert exakt auf `entwurf`, `xml_generiert` oder `eingereicht`; `limit` (1-200, Vorgabe 50) und `offset` blaettern, `total` zaehlt alle Treffer des Filters. Fehlt die Tabelle, wird sie beim Aufruf leer angelegt. Die Positionen werden hier NICHT neu aus den Buchungen gerechnet — es kommt der gespeicherte Stand."},"post":{"responses":{"201":{"description":"Erklärung angelegt. Findet sich im Wirtschaftsjahr keine Buchung, stehen alle Positionen auf 0 — die Antwort ist auch dann 201.","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerung":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"geschaeftsjahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Wirtschaftsjahr, auf das sich die Bilanz bezieht"},"bilanzart":{"type":"string","enum":["jahresabschluss","eroeffnungsbilanz","zwischenbilanz","liquidationsbilanz"],"description":"Art der Bilanz"},"rechtsform":{"type":["string","null"],"minLength":1,"description":"Rechtsform des Unternehmens"},"unternehmensname":{"type":["string","null"],"minLength":1,"description":"Name des Unternehmens"},"steuernummer":{"type":["string","null"],"minLength":1,"description":"Steuernummer, 13-stellig im ELSTER-Format"},"beraterNr":{"type":["string","null"],"minLength":1,"description":"Beraternummer, 7-stellig"},"mandantNr":{"type":["string","null"],"minLength":1,"description":"Mandantennummer beim Berater, 5-stellig"},"wjBeginn":{"type":["string","null"],"format":"date-time","description":"Beginn des Wirtschaftsjahres; DATE-Spalte, kommt als ISO-Zeitstempel"},"wjEnde":{"type":["string","null"],"format":"date-time","description":"Ende des Wirtschaftsjahres; DATE-Spalte, kommt als ISO-Zeitstempel"},"aktiva":{"type":["object","null"],"properties":{"shareCapNotPaidIn":{"type":"number","description":"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert"},"fixAssets":{"type":"number","description":"Anlagevermoegen: Sachanlagen, immaterielle und Finanzanlagen"},"inventory":{"type":"number","description":"Vorraete"},"receivables":{"type":"number","description":"Forderungen aus Lieferungen und Leistungen"},"cashAndEquivalents":{"type":"number","description":"Liquide Mittel: Kasse und Bank"},"prepaidExpenses":{"type":"number","description":"Aktive Rechnungsabgrenzungsposten"}},"required":["shareCapNotPaidIn","fixAssets","inventory","receivables","cashAndEquivalents","prepaidExpenses"],"description":"Aktiv-Positionen; null solange nichts aggregiert wurde"},"passiva":{"type":["object","null"],"properties":{"subscribedCapital":{"type":"number","description":"Gezeichnetes Kapital"},"reserves":{"type":"number","description":"Kapital- und Gewinnruecklagen samt Bilanzgewinn-Vortrag"},"netIncomeCarryover":{"type":"number","description":"Jahresueberschuss oder -fehlbetrag"},"provisions":{"type":"number","description":"Rueckstellungen"},"liabilities":{"type":"number","description":"Verbindlichkeiten, lang- und kurzfristig"},"deferredIncome":{"type":"number","description":"Passive Rechnungsabgrenzungsposten"}},"required":["subscribedCapital","reserves","netIncomeCarryover","provisions","liabilities","deferredIncome"],"description":"Passiv-Positionen; null solange nichts aggregiert wurde"},"guv":{"type":["object","null"],"properties":{"revenue":{"type":"number","description":"Umsatzerloese"},"otherOperatingIncome":{"type":"number","description":"Sonstige betriebliche Ertraege"},"materialExpense":{"type":"number","description":"Materialaufwand"},"personnelExpense":{"type":"number","description":"Personalaufwand"},"depreciation":{"type":"number","description":"Abschreibungen"},"otherOperatingExpense":{"type":"number","description":"Sonstige betriebliche Aufwendungen"},"interestIncome":{"type":"number","description":"Zinsertraege"},"interestExpense":{"type":"number","description":"Zinsaufwendungen"},"incomeTaxes":{"type":"number","description":"Steuern vom Einkommen und Ertrag"},"otherTaxes":{"type":"number","description":"Sonstige Steuern"}},"required":["revenue","otherOperatingIncome","materialExpense","personnelExpense","depreciation","otherOperatingExpense","interestIncome","interestExpense","incomeTaxes","otherTaxes"],"description":"GuV-Positionen; null solange nichts aggregiert wurde"},"bilanzsumme":{"type":["number","null"],"description":"Bilanzsumme der Aktivseite; null solange nicht berechnet"},"jahresueberschuss":{"type":["number","null"],"description":"Jahresueberschuss aus der GuV; null solange nicht berechnet"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XBRL erzeugt, an ELSTER uebergeben"},"xbrlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XBRL-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","geschaeftsjahr","bilanzart","rechtsform","unternehmensname","steuernummer","beraterNr","mandantNr","wjBeginn","wjEnde","aktiva","passiva","guv","bilanzsumme","jahresueberschuss","status","xbrlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Der gespeicherte Stand der Erklaerung"}},"required":["erklaerung"]},"example":{"erklaerung":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","geschaeftsjahr":2000,"bilanzart":"jahresabschluss","rechtsform":"string","unternehmensname":"string","steuernummer":"string","beraterNr":"string","mandantNr":"string","wjBeginn":"2026-01-01T12:00:00.000Z","wjEnde":"2026-01-01T12:00:00.000Z","aktiva":{"shareCapNotPaidIn":0,"fixAssets":0,"inventory":0,"receivables":0,"cashAndEquivalents":0,"prepaidExpenses":0},"passiva":{"subscribedCapital":0,"reserves":0,"netIncomeCarryover":0,"provisions":0,"liabilities":0,"deferredIncome":0},"guv":{"revenue":0,"otherOperatingIncome":0,"materialExpense":0,"personnelExpense":0,"depreciation":0,"otherOperatingExpense":0,"interestIncome":0,"interestExpense":0,"incomeTaxes":0,"otherTaxes":0},"bilanzsumme":0,"jahresueberschuss":0,"status":"entwurf","xbrlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"400":{"description":"Eingabe ungültig"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Bereits vorhanden für dieses Geschäftsjahr (Klartext)"}},"operationId":"postApiV1ElsterEbilanz","tags":["elster"],"parameters":[],"description":"Neue E-Bilanz Erklärung anlegen. Aggregiert Aktiva/Passiva/GuV automatisch aus journal_entries.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"geschaeftsjahr":{"type":"integer","minimum":2000,"maximum":2099},"bilanzart":{"type":"string","enum":["jahresabschluss","eroeffnungsbilanz","zwischenbilanz","liquidationsbilanz"],"default":"jahresabschluss"},"rechtsform":{"type":"string","minLength":1},"unternehmensname":{"type":"string","minLength":1},"steuernummer":{"type":"string","minLength":1},"beraterNr":{"type":"string","default":"0000000"},"mandantNr":{"type":"string","default":"00000"},"wjBeginn":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"wjEnde":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["geschaeftsjahr","rechtsform","unternehmensname","steuernummer","wjBeginn","wjEnde"]},"example":{"geschaeftsjahr":2000,"bilanzart":"jahresabschluss","rechtsform":"string","unternehmensname":"string","steuernummer":"string","beraterNr":"string","mandantNr":"string","wjBeginn":"2026-01-01","wjEnde":"2026-01-01"}}}},"summary":"Neue E-Bilanz Erklärung anlegen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/ebilanz/{id}":{"get":{"responses":{"200":{"description":"Detail mit Bilanz, GuV und Einreichungsstand","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerung":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"geschaeftsjahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Wirtschaftsjahr, auf das sich die Bilanz bezieht"},"bilanzart":{"type":"string","enum":["jahresabschluss","eroeffnungsbilanz","zwischenbilanz","liquidationsbilanz"],"description":"Art der Bilanz"},"rechtsform":{"type":["string","null"],"minLength":1,"description":"Rechtsform des Unternehmens"},"unternehmensname":{"type":["string","null"],"minLength":1,"description":"Name des Unternehmens"},"steuernummer":{"type":["string","null"],"minLength":1,"description":"Steuernummer, 13-stellig im ELSTER-Format"},"beraterNr":{"type":["string","null"],"minLength":1,"description":"Beraternummer, 7-stellig"},"mandantNr":{"type":["string","null"],"minLength":1,"description":"Mandantennummer beim Berater, 5-stellig"},"wjBeginn":{"type":["string","null"],"format":"date-time","description":"Beginn des Wirtschaftsjahres; DATE-Spalte, kommt als ISO-Zeitstempel"},"wjEnde":{"type":["string","null"],"format":"date-time","description":"Ende des Wirtschaftsjahres; DATE-Spalte, kommt als ISO-Zeitstempel"},"aktiva":{"type":["object","null"],"properties":{"shareCapNotPaidIn":{"type":"number","description":"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert"},"fixAssets":{"type":"number","description":"Anlagevermoegen: Sachanlagen, immaterielle und Finanzanlagen"},"inventory":{"type":"number","description":"Vorraete"},"receivables":{"type":"number","description":"Forderungen aus Lieferungen und Leistungen"},"cashAndEquivalents":{"type":"number","description":"Liquide Mittel: Kasse und Bank"},"prepaidExpenses":{"type":"number","description":"Aktive Rechnungsabgrenzungsposten"}},"required":["shareCapNotPaidIn","fixAssets","inventory","receivables","cashAndEquivalents","prepaidExpenses"],"description":"Aktiv-Positionen; null solange nichts aggregiert wurde"},"passiva":{"type":["object","null"],"properties":{"subscribedCapital":{"type":"number","description":"Gezeichnetes Kapital"},"reserves":{"type":"number","description":"Kapital- und Gewinnruecklagen samt Bilanzgewinn-Vortrag"},"netIncomeCarryover":{"type":"number","description":"Jahresueberschuss oder -fehlbetrag"},"provisions":{"type":"number","description":"Rueckstellungen"},"liabilities":{"type":"number","description":"Verbindlichkeiten, lang- und kurzfristig"},"deferredIncome":{"type":"number","description":"Passive Rechnungsabgrenzungsposten"}},"required":["subscribedCapital","reserves","netIncomeCarryover","provisions","liabilities","deferredIncome"],"description":"Passiv-Positionen; null solange nichts aggregiert wurde"},"guv":{"type":["object","null"],"properties":{"revenue":{"type":"number","description":"Umsatzerloese"},"otherOperatingIncome":{"type":"number","description":"Sonstige betriebliche Ertraege"},"materialExpense":{"type":"number","description":"Materialaufwand"},"personnelExpense":{"type":"number","description":"Personalaufwand"},"depreciation":{"type":"number","description":"Abschreibungen"},"otherOperatingExpense":{"type":"number","description":"Sonstige betriebliche Aufwendungen"},"interestIncome":{"type":"number","description":"Zinsertraege"},"interestExpense":{"type":"number","description":"Zinsaufwendungen"},"incomeTaxes":{"type":"number","description":"Steuern vom Einkommen und Ertrag"},"otherTaxes":{"type":"number","description":"Sonstige Steuern"}},"required":["revenue","otherOperatingIncome","materialExpense","personnelExpense","depreciation","otherOperatingExpense","interestIncome","interestExpense","incomeTaxes","otherTaxes"],"description":"GuV-Positionen; null solange nichts aggregiert wurde"},"bilanzsumme":{"type":["number","null"],"description":"Bilanzsumme der Aktivseite; null solange nicht berechnet"},"jahresueberschuss":{"type":["number","null"],"description":"Jahresueberschuss aus der GuV; null solange nicht berechnet"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XBRL erzeugt, an ELSTER uebergeben"},"xbrlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XBRL-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","geschaeftsjahr","bilanzart","rechtsform","unternehmensname","steuernummer","beraterNr","mandantNr","wjBeginn","wjEnde","aktiva","passiva","guv","bilanzsumme","jahresueberschuss","status","xbrlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Der gespeicherte Stand der Erklaerung"}},"required":["erklaerung"]},"example":{"erklaerung":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","geschaeftsjahr":2000,"bilanzart":"jahresabschluss","rechtsform":"string","unternehmensname":"string","steuernummer":"string","beraterNr":"string","mandantNr":"string","wjBeginn":"2026-01-01T12:00:00.000Z","wjEnde":"2026-01-01T12:00:00.000Z","aktiva":{"shareCapNotPaidIn":0,"fixAssets":0,"inventory":0,"receivables":0,"cashAndEquivalents":0,"prepaidExpenses":0},"passiva":{"subscribedCapital":0,"reserves":0,"netIncomeCarryover":0,"provisions":0,"liabilities":0,"deferredIncome":0},"guv":{"revenue":0,"otherOperatingIncome":0,"materialExpense":0,"personnelExpense":0,"depreciation":0,"otherOperatingExpense":0,"interestIncome":0,"interestExpense":0,"incomeTaxes":0,"otherTaxes":0},"bilanzsumme":0,"jahresueberschuss":0,"status":"entwurf","xbrlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"401":{"description":"Kein Mandantenkontext"},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"getApiV1ElsterEbilanzById","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"E-Bilanz-Erklaerung mit allen Positionen lesen","description":"Liefert EINE Erklaerung mit allen Bilanz- und GuV-Positionen sowie dem Einreichungsstand. Gelesen wird der GESPEICHERTE Stand; neu aus `journal_entries` gerechnet wird nur beim Anlegen und beim Aendern (PATCH). Gesucht wird ausschliesslich im eigenen Mandanten — eine fremde Kennung ergibt 404, nicht 403."},"patch":{"responses":{"200":{"description":"Aktualisiert, Positionen neu aus journal_entries aggregiert","content":{"application/json":{"schema":{"type":"object","properties":{"erklaerung":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem die Erklaerung gehoert"},"geschaeftsjahr":{"type":["integer","null"],"minimum":2000,"maximum":2099,"description":"Wirtschaftsjahr, auf das sich die Bilanz bezieht"},"bilanzart":{"type":"string","enum":["jahresabschluss","eroeffnungsbilanz","zwischenbilanz","liquidationsbilanz"],"description":"Art der Bilanz"},"rechtsform":{"type":["string","null"],"minLength":1,"description":"Rechtsform des Unternehmens"},"unternehmensname":{"type":["string","null"],"minLength":1,"description":"Name des Unternehmens"},"steuernummer":{"type":["string","null"],"minLength":1,"description":"Steuernummer, 13-stellig im ELSTER-Format"},"beraterNr":{"type":["string","null"],"minLength":1,"description":"Beraternummer, 7-stellig"},"mandantNr":{"type":["string","null"],"minLength":1,"description":"Mandantennummer beim Berater, 5-stellig"},"wjBeginn":{"type":["string","null"],"format":"date-time","description":"Beginn des Wirtschaftsjahres; DATE-Spalte, kommt als ISO-Zeitstempel"},"wjEnde":{"type":["string","null"],"format":"date-time","description":"Ende des Wirtschaftsjahres; DATE-Spalte, kommt als ISO-Zeitstempel"},"aktiva":{"type":["object","null"],"properties":{"shareCapNotPaidIn":{"type":"number","description":"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert"},"fixAssets":{"type":"number","description":"Anlagevermoegen: Sachanlagen, immaterielle und Finanzanlagen"},"inventory":{"type":"number","description":"Vorraete"},"receivables":{"type":"number","description":"Forderungen aus Lieferungen und Leistungen"},"cashAndEquivalents":{"type":"number","description":"Liquide Mittel: Kasse und Bank"},"prepaidExpenses":{"type":"number","description":"Aktive Rechnungsabgrenzungsposten"}},"required":["shareCapNotPaidIn","fixAssets","inventory","receivables","cashAndEquivalents","prepaidExpenses"],"description":"Aktiv-Positionen; null solange nichts aggregiert wurde"},"passiva":{"type":["object","null"],"properties":{"subscribedCapital":{"type":"number","description":"Gezeichnetes Kapital"},"reserves":{"type":"number","description":"Kapital- und Gewinnruecklagen samt Bilanzgewinn-Vortrag"},"netIncomeCarryover":{"type":"number","description":"Jahresueberschuss oder -fehlbetrag"},"provisions":{"type":"number","description":"Rueckstellungen"},"liabilities":{"type":"number","description":"Verbindlichkeiten, lang- und kurzfristig"},"deferredIncome":{"type":"number","description":"Passive Rechnungsabgrenzungsposten"}},"required":["subscribedCapital","reserves","netIncomeCarryover","provisions","liabilities","deferredIncome"],"description":"Passiv-Positionen; null solange nichts aggregiert wurde"},"guv":{"type":["object","null"],"properties":{"revenue":{"type":"number","description":"Umsatzerloese"},"otherOperatingIncome":{"type":"number","description":"Sonstige betriebliche Ertraege"},"materialExpense":{"type":"number","description":"Materialaufwand"},"personnelExpense":{"type":"number","description":"Personalaufwand"},"depreciation":{"type":"number","description":"Abschreibungen"},"otherOperatingExpense":{"type":"number","description":"Sonstige betriebliche Aufwendungen"},"interestIncome":{"type":"number","description":"Zinsertraege"},"interestExpense":{"type":"number","description":"Zinsaufwendungen"},"incomeTaxes":{"type":"number","description":"Steuern vom Einkommen und Ertrag"},"otherTaxes":{"type":"number","description":"Sonstige Steuern"}},"required":["revenue","otherOperatingIncome","materialExpense","personnelExpense","depreciation","otherOperatingExpense","interestIncome","interestExpense","incomeTaxes","otherTaxes"],"description":"GuV-Positionen; null solange nichts aggregiert wurde"},"bilanzsumme":{"type":["number","null"],"description":"Bilanzsumme der Aktivseite; null solange nicht berechnet"},"jahresueberschuss":{"type":["number","null"],"description":"Jahresueberschuss aus der GuV; null solange nicht berechnet"},"status":{"type":"string","enum":["entwurf","xml_generiert","eingereicht"],"description":"Fortschritt: angelegt, XBRL erzeugt, an ELSTER uebergeben"},"xbrlPath":{"type":["string","null"],"minLength":1,"description":"Ablageort der erzeugten XBRL-Datei; null solange nichts erzeugt wurde"},"eingereichtAm":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der Uebergabe an ELSTER; null solange nicht eingereicht"},"elsterTransferTicket":{"type":["string","null"],"minLength":1,"description":"Quittungsnummer der Uebertragung; null solange nicht eingereicht"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt der Erklaerung"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung an der Erklaerung"}},"required":["id","tenantId","geschaeftsjahr","bilanzart","rechtsform","unternehmensname","steuernummer","beraterNr","mandantNr","wjBeginn","wjEnde","aktiva","passiva","guv","bilanzsumme","jahresueberschuss","status","xbrlPath","eingereichtAm","elsterTransferTicket","createdAt","updatedAt"],"description":"Der gespeicherte Stand der Erklaerung"}},"required":["erklaerung"]},"example":{"erklaerung":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","geschaeftsjahr":2000,"bilanzart":"jahresabschluss","rechtsform":"string","unternehmensname":"string","steuernummer":"string","beraterNr":"string","mandantNr":"string","wjBeginn":"2026-01-01T12:00:00.000Z","wjEnde":"2026-01-01T12:00:00.000Z","aktiva":{"shareCapNotPaidIn":0,"fixAssets":0,"inventory":0,"receivables":0,"cashAndEquivalents":0,"prepaidExpenses":0},"passiva":{"subscribedCapital":0,"reserves":0,"netIncomeCarryover":0,"provisions":0,"liabilities":0,"deferredIncome":0},"guv":{"revenue":0,"otherOperatingIncome":0,"materialExpense":0,"personnelExpense":0,"depreciation":0,"otherOperatingExpense":0,"interestIncome":0,"interestExpense":0,"incomeTaxes":0,"otherTaxes":0},"bilanzsumme":0,"jahresueberschuss":0,"status":"entwurf","xbrlPath":"string","eingereichtAm":"2026-01-01T12:00:00.000Z","elsterTransferTicket":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"400":{"description":"Nur entwurf darf geändert werden (Klartext)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"patchApiV1ElsterEbilanzById","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Stammdaten der E-Bilanz Erklärung ändern (nur status=entwurf). Aggregiert Werte neu.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bilanzart":{"type":"string","enum":["jahresabschluss","eroeffnungsbilanz","zwischenbilanz","liquidationsbilanz"]},"rechtsform":{"type":"string","minLength":1},"unternehmensname":{"type":"string","minLength":1},"steuernummer":{"type":"string","minLength":1},"beraterNr":{"type":"string"},"mandantNr":{"type":"string"},"wjBeginn":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"wjEnde":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}}},"example":{"bilanzart":"jahresabschluss","rechtsform":"string","unternehmensname":"string","steuernummer":"string","beraterNr":"string","mandantNr":"string","wjBeginn":"2026-01-01","wjEnde":"2026-01-01"}}}},"summary":"Stammdaten der E-Bilanz Erklärung ändern (nur status=entwurf)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/ebilanz/{id}/generate-xbrl":{"post":{"responses":{"200":{"description":"XBRL erzeugt und abgelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Erklaerung, fuer die erzeugt wurde"},"xbrl_path":{"type":"string","minLength":1,"description":"Ablageort der geschriebenen XBRL-Datei"},"xbrl_size":{"type":"integer","minimum":0,"description":"Laenge des erzeugten XBRL in Zeichen"}},"required":["id","xbrl_path","xbrl_size"]},"example":{"id":"00000000-0000-4000-8000-000000000000","xbrl_path":"string","xbrl_size":0}}}},"400":{"description":"Eingangsprüfung fehlgeschlagen — es wurde nichts erzeugt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Fehlerkennung fuer die Auswertung"},"errors":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"description":"Klartext je Beanstandung, z. B. \"header.steuernummer: must be 13 digits\""}},"required":["error","errors"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"postApiV1ElsterEbilanzByIdGenerate-xbrl","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"XBRL für die E-Bilanz Erklärung erzeugen und validieren (Taxonomie 6.9). Antwortet mit JSON (Pfad + Länge), nicht mit der Datei — die liegt unter GET /:id/xbrl. Geprüft wird VOR dem Erzeugen: schlägt die Prüfung fehl, entsteht keine Datei.","summary":"XBRL für die E-Bilanz Erklärung erzeugen und validieren (Taxonomie 6.9)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/ebilanz/{id}/submit":{"post":{"responses":{"200":{"description":"Als eingereicht vermerkt — mit STUB-Quittung, ohne echte ELSTER-Übertragung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der eingereichten Erklaerung"},"status":{"type":"string","const":"eingereicht","description":"Neuer Stand der Erklaerung"},"elsterTransferTicket":{"type":"string","minLength":1,"description":"Quittungsnummer. Solange die ERiC-Anbindung fehlt, beginnt sie mit \"STUB-EBILANZ-\" und stammt nicht vom Finanzamt."}},"required":["id","status","elsterTransferTicket"]},"example":{"id":"00000000-0000-4000-8000-000000000000","status":"eingereicht","elsterTransferTicket":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"ELSTER_SUBMIT_ENABLED nicht gesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"elster_submit_disabled","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext mit dem Hinweis auf den manuellen Upload"}},"required":["error","message"]}}}},"404":{"description":"Nicht gefunden (Klartext)"}},"operationId":"postApiV1ElsterEbilanzByIdSubmit","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"E-Bilanz bei ELSTER einreichen (GATED: erfordert ELSTER_SUBMIT_ENABLED=true). Andernfalls: XBRL-Download für manuellen Upload via Mein-ELSTER. ACHTUNG — die ERiC-Anbindung fehlt noch: die Erklärung wird auf \"eingereicht\" gesetzt und bekommt eine selbst erzeugte Quittungsnummer mit Präfix \"STUB-EBILANZ-\". Es geht nichts ans Finanzamt.","summary":"E-Bilanz bei ELSTER einreichen (GATED: erfordert ELSTER_SUBMIT_ENABLED=true)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/elster/ebilanz/{id}/xbrl":{"get":{"responses":{"200":{"description":"XBRL-Datei als Anhang, Dateiname `EBilanz-<Geschaeftsjahr>.xbrl`","content":{"application/xml":{"schema":{"type":"string"}}}},"401":{"description":"Kein Mandantenkontext"},"404":{"description":"Nicht gefunden oder XBRL noch nicht generiert (Klartext)"}},"operationId":"getApiV1ElsterEbilanzByIdXbrl","tags":["elster"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liefert die bereits erzeugte XBRL-Datei zum Herunterladen, kein JSON-Rumpf. Die Datei wird HIER NICHT erzeugt: liegt noch kein Pfad in `xbrl_path`, kommt 404 mit dem Hinweis, zuerst `POST /:id/generate-xbrl` aufzurufen. Denselben Status gibt es, wenn die Erklaerung nicht existiert oder die Datei auf der Platte fehlt — drei verschiedene Ursachen, unterscheidbar nur am Klartext. Gelesen wird ausschliesslich im eigenen Mandanten.","summary":"Liefert die bereits erzeugte XBRL-Datei zum Herunterladen, kein JSON-Rumpf","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/kostenrechnung/kostenstellen":{"get":{"responses":{"200":{"description":"Liste der Kostenstellen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"parentId":{"type":["string","null"]},"budget":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","parentId","budget"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","tenantId":"string","code":"string","name":"string","parentId":"string","budget":0}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KostenrechnungKostenstellen","tags":["kostenrechnung"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List Kostenstellen","description":"Listet die Kostenstellen des Mandanten (Cost Centers), nach Code sortiert. Gelöschte Kostenstellen erscheinen nicht."},"post":{"responses":{"201":{"description":"Kostenstelle angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"parentId":{"type":["string","null"]},"budget":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","parentId","budget"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","name":"string","parentId":"string","budget":0}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1KostenrechnungKostenstellen","tags":["kostenrechnung"],"parameters":[],"summary":"Create Kostenstelle","description":"Legt eine neue Kostenstelle an, optional unter einer übergeordneten Kostenstelle. `budget` ist das JAHRESbudget — der Soll-Ist-Vergleich rechnet daraus ein Zwölftel je Monat. Erfordert mindestens die Rolle manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"parentId":{"type":["string","null"],"format":"uuid"},"budget":{"type":"number","minimum":0,"default":0}},"required":["code","name"]},"example":{"code":"string","name":"string","parentId":"00000000-0000-4000-8000-000000000000","budget":0}}}}}},"/api/v1/kostenrechnung/kostenstellen/{id}":{"get":{"responses":{"200":{"description":"Kostenstelle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"parentId":{"type":["string","null"]},"budget":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","parentId","budget"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","name":"string","parentId":"string","budget":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostenstelle_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KostenrechnungKostenstellenById","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get Kostenstelle","description":"Liefert eine Kostenstelle anhand ihrer Id."},"put":{"responses":{"200":{"description":"Kostenstelle aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"parentId":{"type":["string","null"]},"budget":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","parentId","budget"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","name":"string","parentId":"string","budget":0}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostenstelle_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1KostenrechnungKostenstellenById","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update Kostenstelle","description":"Ersetzt eine Kostenstelle vollständig — kein Teil-Update: nicht übergebene Felder werden auf ihren Vorgabewert zurückgesetzt (`budget` auf 0, `parentId` auf null). Erfordert mindestens die Rolle manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"parentId":{"type":["string","null"],"format":"uuid"},"budget":{"type":"number","minimum":0,"default":0}},"required":["code","name"]},"example":{"code":"string","name":"string","parentId":"00000000-0000-4000-8000-000000000000","budget":0}}}}},"delete":{"responses":{"200":{"description":"Kostenstelle gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostenstelle_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1KostenrechnungKostenstellenById","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete Kostenstelle","description":"Löscht eine Kostenstelle. Soft-Delete (`deleted_at`): bereits gebuchte Kosten bleiben erhalten und behalten ihren Bezug. Erfordert mindestens die Rolle manager."}},"/api/v1/kostenrechnung/kostentraeger":{"get":{"responses":{"200":{"description":"Liste der Kostenträger","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","type"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","tenantId":"string","code":"string","name":"string","type":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KostenrechnungKostentraeger","tags":["kostenrechnung"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List Kostenträger","description":"Listet die Kostenträger des Mandanten (Cost Objects) — das, wofür Kosten anfallen: Auftrag, Projekt, Produkt. Nach Code sortiert, gelöschte erscheinen nicht."},"post":{"responses":{"201":{"description":"Kostenträger angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","type"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","name":"string","type":"string"}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1KostenrechnungKostentraeger","tags":["kostenrechnung"],"parameters":[],"summary":"Create Kostenträger","description":"Legt einen neuen Kostenträger an. Der `code` ist je Mandant eindeutig. Erfordert mindestens die Rolle manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"type":{"type":"string","maxLength":50,"default":"intern"}},"required":["code","name"]},"example":{"code":"string","name":"string","type":"string"}}}}}},"/api/v1/kostenrechnung/kostentraeger/{id}":{"get":{"responses":{"200":{"description":"Kostenträger","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","type"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","name":"string","type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostentraeger_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KostenrechnungKostentraegerById","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get Kostenträger","description":"Liefert einen Kostenträger anhand seiner Id."},"put":{"responses":{"200":{"description":"Kostenträger aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","type"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","name":"string","type":"string"}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostentraeger_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1KostenrechnungKostentraegerById","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update Kostenträger","description":"Ersetzt einen Kostenträger vollständig — kein Teil-Update: ohne `type` fällt das Feld auf den Vorgabewert `intern` zurück. Erfordert mindestens die Rolle manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"type":{"type":"string","maxLength":50,"default":"intern"}},"required":["code","name"]},"example":{"code":"string","name":"string","type":"string"}}}}},"delete":{"responses":{"200":{"description":"Kostenträger gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostentraeger_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1KostenrechnungKostentraegerById","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete Kostenträger","description":"Löscht einen Kostenträger. Soft-Delete (`deleted_at`): bereits gebuchte Kosten bleiben erhalten und behalten ihren Bezug. Erfordert mindestens die Rolle manager."}},"/api/v1/kostenrechnung/kostenarten":{"get":{"responses":{"200":{"description":"Liste der Kostenarten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","type"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","tenantId":"string","code":"string","name":"string","type":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KostenrechnungKostenarten","tags":["kostenrechnung"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List Kostenarten","description":"Listet die Kostenarten des Mandanten (Cost Types) — die Gliederung, WELCHE Kosten anfallen, getrennt nach fix und variabel. Nach Code sortiert, gelöschte erscheinen nicht."},"post":{"responses":{"201":{"description":"Kostenart angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","type"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","name":"string","type":"string"}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1KostenrechnungKostenarten","tags":["kostenrechnung"],"parameters":[],"summary":"Create Kostenart","description":"Legt eine neue Kostenart an (`type`: fix oder var). Der `code` ist je Mandant eindeutig. Erfordert mindestens die Rolle manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"type":{"type":"string","enum":["fix","var"],"default":"var"}},"required":["code","name"]},"example":{"code":"string","name":"string","type":"fix"}}}}}},"/api/v1/kostenrechnung/kostenarten/{id}":{"get":{"responses":{"200":{"description":"Kostenart","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","type"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","name":"string","type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostenart_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KostenrechnungKostenartenById","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get Kostenart","description":"Liefert eine Kostenart anhand ihrer Id."},"put":{"responses":{"200":{"description":"Kostenart aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","type"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","name":"string","type":"string"}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostenart_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1KostenrechnungKostenartenById","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update Kostenart","description":"Ersetzt eine Kostenart vollständig — kein Teil-Update: ohne `type` fällt das Feld auf den Vorgabewert `var` zurück. Erfordert mindestens die Rolle manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"type":{"type":"string","enum":["fix","var"],"default":"var"}},"required":["code","name"]},"example":{"code":"string","name":"string","type":"fix"}}}}},"delete":{"responses":{"200":{"description":"Kostenart gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostenart_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1KostenrechnungKostenartenById","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete Kostenart","description":"Löscht eine Kostenart. Soft-Delete (`deleted_at`): bereits gebuchte Kosten bleiben erhalten — im BAB stehen ihre Kostenart-Felder danach auf null. Erfordert mindestens die Rolle manager."}},"/api/v1/kostenrechnung/buchen":{"post":{"responses":{"201":{"description":"Buchung erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"kostenstelleId":{"type":"string"},"kostentraegerId":{"type":["string","null"]},"kostenartenId":{"type":"string"},"betrag":{"type":"number"},"buchungsdatum":{},"beschreibung":{"type":["string","null"]},"referenzId":{"type":["string","null"]},"referenzTyp":{"type":["string","null"]},"createdAt":{}},"required":["id","tenantId","kostenstelleId","kostentraegerId","kostenartenId","betrag","beschreibung","referenzId","referenzTyp"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","kostenstelleId":"string","kostentraegerId":"string","kostenartenId":"string","betrag":0,"beschreibung":"string","referenzId":"string","referenzTyp":"string"}}}},"400":{"description":"Validierungsfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"423":{"description":"Buchungsperiode geschlossen — ein Wiederholen hilft nicht, die Periode muss geöffnet werden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"period_closed"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1KostenrechnungBuchen","tags":["kostenrechnung"],"parameters":[],"summary":"Post a Kostenbuchung","description":"Bucht Kosten auf eine Kostenstelle, eine Kostenart und optional einen Kostenträger. Der Satz wird zusätzlich als interne Umlage ins Universal-Journal gespiegelt (Verrechnungskonten 9100/9200). Ist die Buchungsperiode geschlossen, antwortet die Route mit 423 — die Kostenbuchung selbst ist zu diesem Zeitpunkt bereits geschrieben, und ein Wiederholen hilft nicht. Scheitert die Spiegelung aus einem anderen Grund, antwortet die Route mit 201, ohne dass ein Journaleintrag entstanden ist. Erfordert mindestens die Rolle manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kostenstelleId":{"type":"string","format":"uuid"},"kostentraegerId":{"type":["string","null"],"format":"uuid"},"kostenartenId":{"type":"string","format":"uuid"},"betrag":{"type":"number"},"buchungsdatum":{"type":"string","format":"date"},"beschreibung":{"type":"string"},"referenzId":{"type":["string","null"]},"referenzTyp":{"type":["string","null"]}},"required":["kostenstelleId","kostenartenId","betrag","buchungsdatum"]},"example":{"kostenstelleId":"00000000-0000-4000-8000-000000000000","kostentraegerId":"00000000-0000-4000-8000-000000000000","kostenartenId":"00000000-0000-4000-8000-000000000000","betrag":0,"buchungsdatum":"2026-01-01","beschreibung":"string","referenzId":"string","referenzTyp":"string"}}}}}},"/api/v1/kostenrechnung/kostenstelle/{id}/auswertung":{"get":{"responses":{"200":{"description":"BAB-Auswertung","content":{"application/json":{"schema":{"type":"object","properties":{"kostenstelle":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"parentId":{"type":["string","null"]},"budget":{"type":"number"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","code","name","parentId","budget"],"additionalProperties":false},"auswertung":{"type":"array","items":{"type":"object","properties":{"kostenartenId":{"type":"string"},"code":{"type":["string","null"]},"name":{"type":["string","null"]},"type":{"type":["string","null"]},"total":{"type":"number"}},"required":["kostenartenId","code","name","type","total"],"additionalProperties":false}},"summary":{"type":"object","properties":{"totalIst":{"type":"number"},"budget":{"type":"number"},"abweichung":{"type":"number"}},"required":["totalIst","budget","abweichung"],"additionalProperties":false},"filter":{"type":"object","properties":{"periodeFrom":{"type":["string","null"]},"periodeTo":{"type":["string","null"]}},"required":["periodeFrom","periodeTo"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"}},"required":["tenantId"],"additionalProperties":false}},"required":["kostenstelle","auswertung","summary","filter","meta"],"additionalProperties":false},"example":{"kostenstelle":{"id":"string","tenantId":"string","code":"string","name":"string","parentId":"string","budget":0},"auswertung":[{"kostenartenId":"string","code":"string","name":"string","type":"string","total":0}],"summary":{"totalIst":0,"budget":0,"abweichung":0},"filter":{"periodeFrom":"string","periodeTo":"string"},"meta":{"tenantId":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"kostenstelle_not_found"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KostenrechnungKostenstelleByIdAuswertung","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get BAB for one Kostenstelle","description":"BAB (Betriebsabrechnungsbogen) für eine Kostenstelle: Ist-Kosten je Kostenart, dazu die Summe gegen das hinterlegte Budget. Über `periodeFrom`/`periodeTo` auf einen Datumsbereich einschränkbar; das Budget im `summary` ist immer das volle Jahresbudget und wird NICHT auf den Zeitraum umgerechnet."}},"/api/v1/kostenrechnung/soll-ist-vergleich/{periode}":{"get":{"responses":{"200":{"description":"Soll-Ist-Vergleich","content":{"application/json":{"schema":{"type":"object","properties":{"periode":{"type":"string"},"vergleich":{"type":"array","items":{"type":"object","properties":{"kostenstelleId":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"soll":{"type":"number"},"ist":{"type":"number"},"abweichung":{"type":"number"},"abweichungProzent":{"type":["number","null"]}},"required":["kostenstelleId","code","name","soll","ist","abweichung","abweichungProzent"],"additionalProperties":false}},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"kostenstellen":{"type":"number"}},"required":["tenantId","kostenstellen"],"additionalProperties":false}},"required":["periode","vergleich","meta"],"additionalProperties":false},"example":{"periode":"string","vergleich":[{"kostenstelleId":"string","code":"string","name":"string","soll":0,"ist":0,"abweichung":0,"abweichungProzent":0}],"meta":{"tenantId":"string","kostenstellen":0}}}}},"400":{"description":"Periode nicht im Format YYYY-MM","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_periode"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nach `retryAfter` Sekunden erneut versuchen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1KostenrechnungSoll-ist-vergleichByPeriode","tags":["kostenrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"periode","required":true}],"summary":"Compare Soll vs Ist for one period","description":"Soll-Ist-Vergleich aller Kostenstellen für eine Periode (YYYY-MM). Das Soll ist ein Zwölftel des Jahresbudgets; `abweichungProzent` bleibt null, wenn das Soll 0 ist. Ein anderes Periodenformat wird mit 400 abgelehnt."}},"/api/v1/posting-groups/customer":{"get":{"responses":{"200":{"description":"Liste der Customer Posting Groups (Umschlag: data + pagination, kein meta)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"receivablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","receivablesAccount","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","tenantId":"string","code":"string","description":"string","receivablesAccount":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Posting-groupsCustomer","tags":["posting-groups"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List customer posting groups","description":"Listet die Debitoren-Buchungsgruppen (Customer Posting Groups) des Mandanten — je Gruppe das Forderungs-Sachkonto, auf das Kundenbelege gebucht werden."},"post":{"responses":{"201":{"description":"Angelegte Customer Posting Group (der ganze Datensatz, kein Umschlag)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"receivablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","receivablesAccount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","description":"string","receivablesAccount":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Posting-groupsCustomer","tags":["posting-groups"],"parameters":[],"summary":"Create customer posting group","description":"Legt eine Debitoren-Buchungsgruppe an. Der `code` ist je Mandant eindeutig; ein bereits vergebener Code verletzt die Eindeutigkeit und wird derzeit als 503 gemeldet, nicht als 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"description":{"type":"string","maxLength":255,"default":""},"receivablesAccount":{"type":"string","maxLength":20}},"required":["code","receivablesAccount"]},"example":{"code":"string","description":"string","receivablesAccount":"string"}}}}}},"/api/v1/posting-groups/customer/{id}":{"get":{"responses":{"200":{"description":"Customer Posting Group","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"receivablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","receivablesAccount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","description":"string","receivablesAccount":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"getApiV1Posting-groupsCustomerById","tags":["posting-groups"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get customer posting group","description":"Liefert eine Debitoren-Buchungsgruppe anhand ihrer Id."},"put":{"responses":{"200":{"description":"Aktualisierte Customer Posting Group","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"receivablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","receivablesAccount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","description":"string","receivablesAccount":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"putApiV1Posting-groupsCustomerById","tags":["posting-groups"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update customer posting group","description":"Ersetzt eine Debitoren-Buchungsgruppe vollständig — kein Teil-Update: nicht übergebene Felder werden auf ihren Vorgabewert zurückgesetzt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"description":{"type":"string","maxLength":255,"default":""},"receivablesAccount":{"type":"string","maxLength":20}},"required":["code","receivablesAccount"]},"example":{"code":"string","description":"string","receivablesAccount":"string"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Meldung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"deleteApiV1Posting-groupsCustomerById","tags":["posting-groups"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete customer posting group","description":"Löscht eine Debitoren-Buchungsgruppe. Soft-Delete (`deleted_at`): der Datensatz bleibt in der Datenbank und bereits gebuchte Belege behalten ihren Bezug."}},"/api/v1/posting-groups/vendor":{"get":{"responses":{"200":{"description":"Liste der Vendor Posting Groups (Umschlag: data + pagination, kein meta)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"payablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","payablesAccount","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","tenantId":"string","code":"string","description":"string","payablesAccount":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Posting-groupsVendor","tags":["posting-groups"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List vendor posting groups","description":"Listet die Kreditoren-Buchungsgruppen (Vendor Posting Groups) des Mandanten — je Gruppe das Verbindlichkeiten-Sachkonto, auf das Lieferantenbelege gebucht werden."},"post":{"responses":{"201":{"description":"Angelegte Vendor Posting Group (der ganze Datensatz, kein Umschlag)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"payablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","payablesAccount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","description":"string","payablesAccount":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Posting-groupsVendor","tags":["posting-groups"],"parameters":[],"summary":"Create vendor posting group","description":"Legt eine Kreditoren-Buchungsgruppe an. Der `code` ist je Mandant eindeutig; ein bereits vergebener Code verletzt die Eindeutigkeit und wird derzeit als 503 gemeldet, nicht als 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"description":{"type":"string","maxLength":255,"default":""},"payablesAccount":{"type":"string","maxLength":20}},"required":["code","payablesAccount"]},"example":{"code":"string","description":"string","payablesAccount":"string"}}}}}},"/api/v1/posting-groups/vendor/{id}":{"get":{"responses":{"200":{"description":"Vendor Posting Group","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"payablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","payablesAccount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","description":"string","payablesAccount":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"getApiV1Posting-groupsVendorById","tags":["posting-groups"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get vendor posting group","description":"Liefert eine Kreditoren-Buchungsgruppe anhand ihrer Id."},"put":{"responses":{"200":{"description":"Aktualisierte Vendor Posting Group","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"payablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","payablesAccount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","code":"string","description":"string","payablesAccount":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"putApiV1Posting-groupsVendorById","tags":["posting-groups"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update vendor posting group","description":"Ersetzt eine Kreditoren-Buchungsgruppe vollständig — kein Teil-Update: nicht übergebene Felder werden auf ihren Vorgabewert zurückgesetzt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"description":{"type":"string","maxLength":255,"default":""},"payablesAccount":{"type":"string","maxLength":20}},"required":["code","payablesAccount"]},"example":{"code":"string","description":"string","payablesAccount":"string"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Meldung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"deleteApiV1Posting-groupsVendorById","tags":["posting-groups"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete vendor posting group","description":"Löscht eine Kreditoren-Buchungsgruppe. Soft-Delete (`deleted_at`): der Datensatz bleibt in der Datenbank und bereits gebuchte Belege behalten ihren Bezug."}},"/api/v1/posting-groups/general":{"get":{"responses":{"200":{"description":"Liste der General Posting Groups (Umschlag: data + pagination, kein meta)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"genBusGroup":{"type":"string"},"genProdGroup":{"type":"string"},"salesAccount":{"type":"string"},"purchaseAccount":{"type":"string"},"cogsAccount":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","genBusGroup","genProdGroup","salesAccount","purchaseAccount","cogsAccount","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"example":{"data":[{"id":"string","tenantId":"string","genBusGroup":"string","genProdGroup":"string","salesAccount":"string","purchaseAccount":"string","cogsAccount":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Posting-groupsGeneral","tags":["posting-groups"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List general posting groups","description":"Listet die allgemeinen Buchungsgruppen (General Posting Groups) des Mandanten. Jede Kombination aus Geschäfts- und Produktbuchungsgruppe bestimmt Erlös-, Wareneingangs- und optional das Wareneinsatz-Sachkonto."},"post":{"responses":{"201":{"description":"Angelegte General Posting Group (der ganze Datensatz, kein Umschlag)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"genBusGroup":{"type":"string"},"genProdGroup":{"type":"string"},"salesAccount":{"type":"string"},"purchaseAccount":{"type":"string"},"cogsAccount":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","genBusGroup","genProdGroup","salesAccount","purchaseAccount","cogsAccount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","genBusGroup":"string","genProdGroup":"string","salesAccount":"string","purchaseAccount":"string","cogsAccount":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Posting-groupsGeneral","tags":["posting-groups"],"parameters":[],"summary":"Create general posting group","description":"Legt eine allgemeine Buchungsgruppe an. Das Paar aus `genBusGroup` und `genProdGroup` ist je Mandant eindeutig; eine bereits vergebene Kombination verletzt die Eindeutigkeit und wird derzeit als 503 gemeldet, nicht als 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"genBusGroup":{"type":"string","minLength":1,"maxLength":20},"genProdGroup":{"type":"string","minLength":1,"maxLength":20},"salesAccount":{"type":"string","maxLength":20},"purchaseAccount":{"type":"string","maxLength":20},"cogsAccount":{"type":["string","null"],"maxLength":20}},"required":["genBusGroup","genProdGroup","salesAccount","purchaseAccount"]},"example":{"genBusGroup":"string","genProdGroup":"string","salesAccount":"string","purchaseAccount":"string","cogsAccount":"string"}}}}}},"/api/v1/posting-groups/general/{id}":{"get":{"responses":{"200":{"description":"General Posting Group","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"genBusGroup":{"type":"string"},"genProdGroup":{"type":"string"},"salesAccount":{"type":"string"},"purchaseAccount":{"type":"string"},"cogsAccount":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","genBusGroup","genProdGroup","salesAccount","purchaseAccount","cogsAccount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","genBusGroup":"string","genProdGroup":"string","salesAccount":"string","purchaseAccount":"string","cogsAccount":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"getApiV1Posting-groupsGeneralById","tags":["posting-groups"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get general posting group","description":"Liefert eine allgemeine Buchungsgruppe anhand ihrer Id."},"put":{"responses":{"200":{"description":"Aktualisierte General Posting Group","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"genBusGroup":{"type":"string"},"genProdGroup":{"type":"string"},"salesAccount":{"type":"string"},"purchaseAccount":{"type":"string"},"cogsAccount":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","genBusGroup","genProdGroup","salesAccount","purchaseAccount","cogsAccount","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","genBusGroup":"string","genProdGroup":"string","salesAccount":"string","purchaseAccount":"string","cogsAccount":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"putApiV1Posting-groupsGeneralById","tags":["posting-groups"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update general posting group","description":"Ersetzt eine allgemeine Buchungsgruppe vollständig — kein Teil-Update: `cogsAccount` wird ohne Angabe auf null gesetzt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"genBusGroup":{"type":"string","minLength":1,"maxLength":20},"genProdGroup":{"type":"string","minLength":1,"maxLength":20},"salesAccount":{"type":"string","maxLength":20},"purchaseAccount":{"type":"string","maxLength":20},"cogsAccount":{"type":["string","null"],"maxLength":20}},"required":["genBusGroup","genProdGroup","salesAccount","purchaseAccount"]},"example":{"genBusGroup":"string","genProdGroup":"string","salesAccount":"string","purchaseAccount":"string","cogsAccount":"string"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Meldung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"deleteApiV1Posting-groupsGeneralById","tags":["posting-groups"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete general posting group","description":"Löscht eine allgemeine Buchungsgruppe. Soft-Delete (`deleted_at`): der Datensatz bleibt in der Datenbank und bereits gebuchte Belege behalten ihren Bezug."}},"/api/v1/posting-groups/{type}":{"get":{"responses":{"200":{"description":"Liste zum angefragten Typ. ACHTUNG: die Positionen haben je nach `type` eine ANDERE Form — customer fuehrt `receivablesAccount`, vendor `payablesAccount`, general das Paar `salesAccount`/`purchaseAccount` plus `cogsAccount`. Der Umschlag traegt hier zusaetzlich `type`.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"customer"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"receivablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","receivablesAccount","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["type","data","pagination"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"vendor"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"code":{"type":"string"},"description":{"type":"string"},"payablesAccount":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","code","description","payablesAccount","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["type","data","pagination"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"general"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"genBusGroup":{"type":"string"},"genProdGroup":{"type":"string"},"salesAccount":{"type":"string"},"purchaseAccount":{"type":"string"},"cogsAccount":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","genBusGroup","genProdGroup","salesAccount","purchaseAccount","cogsAccount","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"],"additionalProperties":false}},"required":["type","data","pagination"],"additionalProperties":false}]},"example":{"type":"customer","data":[{"id":"string","tenantId":"string","code":"string","description":"string","receivablesAccount":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"400":{"description":"Ungültiger Typ"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Posting-groupsByType","tags":["posting-groups"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"schema":{"type":"string"},"in":"path","name":"type","required":true}],"summary":"List posting groups by type","description":"Listet Buchungsgruppen nach Typ (customer|vendor|general) über einen Pfad. Ein anderer Wert wird mit 400 abgelehnt. Die Positionen haben je nach Typ eine andere Form — siehe die 200-Antwort."}},"/api/v1/posting-groups/resolve":{"post":{"responses":{"200":{"description":"Aufgelöste GL-Konten. Jedes Konto ist `null`, solange keine Posting Group greift; welche Felder aufgelöst wurden, steht zusätzlich in `resolved` bzw. `unresolved`. `meta` führt hier nur `tenantId`.","content":{"application/json":{"schema":{"type":"object","properties":{"transactionType":{"type":"string","enum":["sale","purchase"]},"accounts":{"type":"object","properties":{"receivablesAccount":{"type":["string","null"]},"payablesAccount":{"type":["string","null"]},"salesAccount":{"type":["string","null"]},"purchaseAccount":{"type":["string","null"]},"cogsAccount":{"type":["string","null"]},"resolved":{"type":"array","items":{"type":"string"}},"unresolved":{"type":"array","items":{"type":"string"}}},"required":["receivablesAccount","payablesAccount","salesAccount","purchaseAccount","cogsAccount","resolved","unresolved"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"}},"required":["tenantId"],"additionalProperties":false}},"required":["transactionType","accounts","meta"],"additionalProperties":false},"example":{"transactionType":"sale","accounts":{"receivablesAccount":"string","payablesAccount":"string","salesAccount":"string","purchaseAccount":"string","cogsAccount":"string","resolved":["string"],"unresolved":["string"]},"meta":{"tenantId":"string"}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Posting-groupsResolve","tags":["posting-groups"],"parameters":[],"summary":"Resolve GL accounts for a posting","description":"Ermittelt die Sachkonten für eine Buchung aus den Buchungsgruppen: Debitor über die Buchungsgruppe des Kunden, Kreditor über die des Lieferanten, Erlös/Wareneingang/Wareneinsatz über das Paar aus Geschäfts- und Produktbuchungsgruppe. Die Route bucht nichts und schlägt nicht fehl, wenn nichts aufgelöst werden kann: sie antwortet auch dann mit 200, alle fünf Konten sind dann `null`. Trägt der Kunde bzw. Lieferant gar keine Buchungsgruppe — oder ist die zugehörige Stammdatentabelle im Mandanten nicht vorhanden —, erscheint das Konto weder in `resolved` noch in `unresolved`. Nur ein hinterlegter, aber nicht auffindbarer Gruppen-Code landet in `unresolved`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"vendorId":{"type":"string","format":"uuid"},"genBusGroup":{"type":"string","maxLength":20},"genProdGroup":{"type":"string","maxLength":20},"transactionType":{"type":"string","enum":["sale","purchase"],"default":"sale"}}},"example":{"customerId":"00000000-0000-4000-8000-000000000000","vendorId":"00000000-0000-4000-8000-000000000000","genBusGroup":"string","genProdGroup":"string","transactionType":"sale"}}}}}},"/api/v1/dimensions":{"get":{"responses":{"200":{"description":"Liste der Dimensionen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"code":{},"name":{},"type":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]},"meta":{"type":"object","properties":{"tenantId":{},"source":{"type":"string"}},"required":["source"]}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"source":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1Dimensions","tags":["dimensions"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"List dimensions","description":"Listet die Buchungsdimensionen des Mandanten (Auswertungsmerkmale nach dem Muster von MS Business Central), nach Code sortiert. Beim ersten Aufruf legt die Route die beiden Standard-Dimensionen DEPARTMENT und PROJECT an — ein GET mit Schreibwirkung; ein zweiter Aufruf ändert nichts mehr."},"post":{"responses":{"201":{"description":"Dimension angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"code":{},"name":{},"type":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1Dimensions","tags":["dimensions"],"parameters":[],"summary":"Create dimension","description":"Legt eine neue Dimension an. Der `code` wird in Großbuchstaben abgelegt und ist je Mandant eindeutig; ein bereits vergebener Code verletzt die Eindeutigkeit und wird derzeit als 503 gemeldet, nicht als 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"type":{"type":"string","enum":["global","shortcut"],"default":"shortcut"}},"required":["code","name"]},"example":{"code":"string","name":"string","type":"global"}}}}}},"/api/v1/dimensions/by-code/{code}/values":{"get":{"responses":{"200":{"description":"Dimension Values","content":{"application/json":{"schema":{"type":"object","properties":{"dimensionCode":{"type":"string"},"dimensionId":{"type":"string"},"data":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"dimensionId":{},"code":{},"name":{},"blocked":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["blocked"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"offset":{"type":"number"},"total":{"type":"number"}},"required":["limit","offset","total"]}},"required":["dimensionCode","dimensionId","data","pagination"],"additionalProperties":false},"example":{"dimensionCode":"string","dimensionId":"string","data":[{"blocked":true}],"pagination":{"limit":0,"offset":0,"total":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Dimension nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1DimensionsBy-codeByCodeValues","tags":["dimensions"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"schema":{"type":"string"},"in":"path","name":"code","required":true}],"summary":"List dimension values","description":"Listet die Werte einer Dimension, angesprochen über deren Code (Groß-/Kleinschreibung egal). Gesperrte Werte (`blocked`) sind enthalten und als solche gekennzeichnet."},"post":{"responses":{"201":{"description":"Dimension Value angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"dimensionId":{},"code":{},"name":{},"blocked":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["blocked"],"additionalProperties":false},"example":{"blocked":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Dimension nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1DimensionsBy-codeByCodeValues","tags":["dimensions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"code","required":true}],"summary":"Create dimension value","description":"Legt einen Wert für eine Dimension an. Der `code` ist je Dimension eindeutig; ein bereits vergebener Code verletzt die Eindeutigkeit und wird derzeit als 503 gemeldet, nicht als 409. Ein Wert mit `blocked: true` ist sofort für neue Buchungen gesperrt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"blocked":{"type":"boolean","default":false}},"required":["code","name"]},"example":{"code":"string","name":"string","blocked":true}}}}}},"/api/v1/dimensions/by-code/{code}/values/{id}":{"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"dimensionId":{},"code":{},"name":{},"blocked":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["blocked"],"additionalProperties":false},"example":{"blocked":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1DimensionsBy-codeByCodeValuesById","tags":["dimensions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"code","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update dimension value","description":"Ersetzt einen Dimensionswert vollständig — kein Teil-Update: ohne `blocked` fällt das Feld auf false zurück. Der Wert wird allein über seine Id gesucht; der Dimensions-Code im Pfad wird NICHT geprüft und schränkt nichts ein.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"blocked":{"type":"boolean","default":false}},"required":["code","name"]},"example":{"code":"string","name":"string","blocked":true}}}}},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1DimensionsBy-codeByCodeValuesById","tags":["dimensions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"code","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete dimension value","description":"Löscht einen Dimensionswert. Soft-Delete (`deleted_at`): bereits erfasste Dimensionseinträge bleiben erhalten. Der Wert wird allein über seine Id gesucht; der Dimensions-Code im Pfad wird NICHT geprüft und schränkt nichts ein."}},"/api/v1/dimensions/by-code/{code}":{"get":{"responses":{"200":{"description":"Dimension","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"code":{},"name":{},"type":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1DimensionsBy-codeByCode","tags":["dimensions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"code","required":true}],"summary":"Get dimension by code","description":"Liefert eine Dimension anhand ihres Codes (Groß-/Kleinschreibung egal). Die Werte der Dimension stehen unter `/dimensions/{code}/values`."}},"/api/v1/dimensions/{id}":{"put":{"responses":{"200":{"description":"Dimension aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"code":{},"name":{},"type":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"putApiV1DimensionsById","tags":["dimensions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update dimension","description":"Ersetzt eine Dimension vollständig, angesprochen über ihre Id — kein Teil-Update: ohne `type` fällt das Feld auf den Vorgabewert `shortcut` zurück. Ein geänderter `code` gilt nur für neue Buchungen; bereits erfasste Dimensionseinträge tragen den alten Code weiter.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":255},"type":{"type":"string","enum":["global","shortcut"],"default":"shortcut"}},"required":["code","name"]},"example":{"code":"string","name":"string","type":"global"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1DimensionsById","tags":["dimensions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete dimension","description":"Löscht eine Dimension, angesprochen über ihre Id. Soft-Delete (`deleted_at`): ihre Werte und bereits erfasste Dimensionseinträge werden NICHT mitgelöscht und bleiben über die Werte-Routen erreichbar."}},"/api/v1/dimensions/dimension-entries":{"post":{"responses":{"201":{"description":"Dimension Entries erstellt","content":{"application/json":{"schema":{"type":"object","properties":{"postingRef":{"type":"string"},"entries":{"type":"array","items":{"type":"object","properties":{"id":{},"postingRef":{},"postingRefType":{},"dimensionCode":{},"dimensionValueCode":{},"createdAt":{}}}},"meta":{"type":"object","properties":{"tenantId":{}}}},"required":["postingRef","entries","meta"],"additionalProperties":false},"example":{"postingRef":"string","entries":[{}],"meta":{}}}}},"400":{"description":"Dimension oder Wert nicht gefunden bzw. gesperrt — message nennt den Code","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1DimensionsDimension-entries","tags":["dimensions"],"parameters":[],"summary":"Record dimension entries for a posting","description":"Erfasst eine oder mehrere Dimensionskombinationen zu einer Buchungsreferenz. Unbekannte oder gesperrte Werte werden mit 400 abgelehnt — `message` nennt den Code. Achtung: die Einträge werden EINZELN geschrieben, ohne umschließende Transaktion. Schlägt der dritte Eintrag fehl, bleiben die ersten beiden geschrieben, und die Antwort ist trotzdem 400. Doppelte Kombinationen werden nicht verhindert: ein zweiter Aufruf mit derselben Referenz legt zusätzliche Zeilen an.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"postingRef":{"type":"string","minLength":1,"maxLength":100},"postingRefType":{"type":"string","maxLength":50,"default":"journal"},"entries":{"type":"array","items":{"type":"object","properties":{"dimensionCode":{"type":"string","minLength":1,"maxLength":20},"dimensionValueCode":{"type":"string","minLength":1,"maxLength":20}},"required":["dimensionCode","dimensionValueCode"]},"minItems":1}},"required":["postingRef","entries"]},"example":{"postingRef":"string","postingRefType":"string","entries":[{"dimensionCode":"string","dimensionValueCode":"string"}]}}}}}},"/api/v1/dimensions/dimension-entries/{postingRef}":{"get":{"responses":{"200":{"description":"Dimension Entries","content":{"application/json":{"schema":{"type":"object","properties":{"postingRef":{"type":"string"},"entries":{"type":"array","items":{"type":"object","properties":{"id":{},"dimensionCode":{},"dimensionValueCode":{},"postingRefType":{},"createdAt":{}}}},"meta":{"type":"object","properties":{"tenantId":{},"count":{"type":"number"}},"required":["count"]}},"required":["postingRef","entries","meta"],"additionalProperties":false},"example":{"postingRef":"string","entries":[{}],"meta":{"count":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1DimensionsDimension-entriesByPostingRef","tags":["dimensions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"postingRef","required":true}],"summary":"Get dimension entries for a posting","description":"Liefert alle Dimensionseinträge zu einer Buchungsreferenz, nach Dimensions-Code sortiert. Eine unbekannte Referenz ist kein Fehler: die Antwort ist 200 mit leerer Liste und `meta.count` = 0, kein 404."}},"/api/v1/dimensions/entity-dimensions/{entityType}/{entityId}":{"get":{"responses":{"200":{"description":"Entity Dimensions","content":{"application/json":{"schema":{"type":"object","properties":{"entityType":{"type":"string"},"entityId":{"type":"string"},"dimensions":{"type":"array","items":{"type":"object","properties":{"id":{},"tenantId":{},"entityType":{},"entityId":{},"dimensionId":{},"valueId":{},"dimensionCode":{},"dimensionValueCode":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}},"meta":{"type":"object","properties":{"tenantId":{},"count":{"type":"number"}},"required":["count"]}},"required":["entityType","entityId","dimensions","meta"],"additionalProperties":false},"example":{"entityType":"string","entityId":"string","dimensions":[{}],"meta":{"count":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiV1DimensionsEntity-dimensionsByEntityTypeByEntityId","tags":["dimensions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entityType","required":true},{"schema":{"type":"string"},"in":"path","name":"entityId","required":true}],"summary":"List dimensions assigned to an entity","description":"Liefert die Dimensionszuordnungen eines Datensatzes (z.B. customer, invoice) samt Code und Bezeichnung von Dimension und Wert. Eine unbekannte Entity ist kein Fehler: die Antwort ist 200 mit leerer Liste, kein 404."}},"/api/v1/dimensions/entity-dimensions":{"post":{"responses":{"201":{"description":"Entity Dimension gesetzt (Upsert — 201 auch beim Überschreiben)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"tenantId":{},"entityType":{},"entityId":{},"dimensionId":{},"valueId":{},"dimensionCode":{},"dimensionValueCode":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"400":{"description":"Dimension oder Wert nicht gefunden bzw. gesperrt — message nennt den Code","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiV1DimensionsEntity-dimensions","tags":["dimensions"],"parameters":[],"summary":"Assign a dimension to an entity","description":"Ordnet einem Datensatz einen Dimensionswert zu. Upsert: je Entity und Dimension gibt es genau eine Zuordnung — ein zweiter Aufruf überschreibt den bisherigen Wert und antwortet ebenfalls mit 201. Ein unbekannter oder gesperrter Wert wird mit 400 abgelehnt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entityType":{"type":"string","minLength":1,"maxLength":50},"entityId":{"type":"string","format":"uuid"},"dimensionCode":{"type":"string","minLength":1,"maxLength":20},"dimensionValueCode":{"type":"string","minLength":1,"maxLength":20}},"required":["entityType","entityId","dimensionCode","dimensionValueCode"]},"example":{"entityType":"string","entityId":"00000000-0000-4000-8000-000000000000","dimensionCode":"string","dimensionValueCode":"string"}}}}}},"/api/v1/dimensions/entity-dimensions/{entityType}/{entityId}/{dimensionCode}":{"delete":{"responses":{"200":{"description":"Entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Dimension oder Zuordnung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht erreichbar — später erneut versuchen.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"deleteApiV1DimensionsEntity-dimensionsByEntityTypeByEntityIdByDimensionCode","tags":["dimensions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"entityType","required":true},{"schema":{"type":"string"},"in":"path","name":"entityId","required":true},{"schema":{"type":"string"},"in":"path","name":"dimensionCode","required":true}],"summary":"Remove a dimension from an entity","description":"Entfernt die Zuordnung einer Dimension zu einem Datensatz. Endgültiges Löschen (kein Soft-Delete). Unbekannte Dimension oder fehlende Zuordnung → 404."}},"/api/v1/saved-searches":{"get":{"responses":{"200":{"description":"Die Suchen dieser Seite samt Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der gespeicherten Suche (UUID)"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, zu dem die Suche gehoert"},"userId":{"type":"string","minLength":1,"description":"Benutzer, dem die Suche gehoert — nur er darf sie aendern und loeschen"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Anzeigename der Suche in der Auswahlliste"},"entityType":{"type":"string","minLength":1,"maxLength":50,"description":"Liste, fuer die die Suche gilt, z. B. `invoices` — frei waehlbar, nicht gegen eine Liste geprueft"},"filters":{"type":"object","additionalProperties":{},"description":"Die gespeicherten Filter als freies Objekt; leeres Objekt, wenn keine gesetzt sind"},"columns":{"type":"array","items":{"type":"string"},"description":"Die sichtbaren Spalten in ihrer Reihenfolge; leer, wenn die Standardspalten gelten"},"sort":{"type":"string","maxLength":100,"description":"Die gespeicherte Sortierung; leerer Text, wenn die Standardsortierung gilt"},"isShared":{"type":"boolean","description":"`true` = alle im Mandanten sehen die Suche, aendern darf sie weiterhin nur der Eigentuemer"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","userId","name","entityType","filters","columns","sort","isShared","createdAt","updatedAt"],"additionalProperties":false},"description":"Die Suchen dieser Seite, aufsteigend nach Name — eigene UND geteilte"},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false,"description":"Seitenangaben; `total` zaehlt alle Treffer, nicht nur diese Seite"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Der Mandant, gegen den gesucht wurde"},"userId":{"type":"string","minLength":1,"description":"Die Benutzerkennung, gegen die „eigene\" bestimmt wurde; ohne Benutzerkontext die Mandantenkennung"}},"required":["tenantId","userId"],"additionalProperties":false,"description":"Herkunftsangaben zur Abfrage"}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","userId":"string","name":"string","entityType":"string","filters":{},"columns":["string"],"sort":"string","isShared":true,"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"tenantId":"string","userId":"string"}}}}},"400":{"description":"Query-Parameter abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen (JSON nach dem Schema). Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext `database unavailable`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Saved-searches","tags":["saved-searches"],"parameters":[{"in":"query","name":"entity_type","schema":{"type":"string","maxLength":50}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Gespeicherte Suchen auflisten","description":"Listet die gespeicherten Suchen, die der Anfragende sehen darf: seine eigenen plus alle mit `isShared` im selben Mandanten. Mit `entity_type` laesst sich auf eine Liste einschraenken."},"post":{"responses":{"201":{"description":"Die angelegte Suche, wie der Server sie gespeichert hat","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der gespeicherten Suche (UUID)"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, zu dem die Suche gehoert"},"userId":{"type":"string","minLength":1,"description":"Benutzer, dem die Suche gehoert — nur er darf sie aendern und loeschen"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Anzeigename der Suche in der Auswahlliste"},"entityType":{"type":"string","minLength":1,"maxLength":50,"description":"Liste, fuer die die Suche gilt, z. B. `invoices` — frei waehlbar, nicht gegen eine Liste geprueft"},"filters":{"type":"object","additionalProperties":{},"description":"Die gespeicherten Filter als freies Objekt; leeres Objekt, wenn keine gesetzt sind"},"columns":{"type":"array","items":{"type":"string"},"description":"Die sichtbaren Spalten in ihrer Reihenfolge; leer, wenn die Standardspalten gelten"},"sort":{"type":"string","maxLength":100,"description":"Die gespeicherte Sortierung; leerer Text, wenn die Standardsortierung gilt"},"isShared":{"type":"boolean","description":"`true` = alle im Mandanten sehen die Suche, aendern darf sie weiterhin nur der Eigentuemer"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","userId","name","entityType","filters","columns","sort","isShared","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","userId":"string","name":"string","entityType":"string","filters":{},"columns":["string"],"sort":"string","isShared":true,"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"INSERT fehlgeschlagen (JSON nach dem Schema) — es wurde nichts gespeichert. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext `database unavailable`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Saved-searches","tags":["saved-searches"],"parameters":[],"summary":"Gespeicherte Suche anlegen","description":"Legt eine gespeicherte Suche an und gibt sie OHNE Umschlag zurueck. Eigentuemer ist der Anfragende; ohne Benutzerkontext wird die Mandantenkennung als Eigentuemer eingetragen. Gleiche Namen sind erlaubt — es gibt keine Eindeutigkeitspruefung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"entityType":{"type":"string","minLength":1,"maxLength":50},"filters":{"type":"object","additionalProperties":{},"default":{}},"columns":{"type":"array","items":{"type":"string"},"default":[]},"sort":{"type":"string","maxLength":100,"default":""},"isShared":{"type":"boolean","default":false}},"required":["name","entityType"]},"example":{"name":"string","entityType":"string","filters":{},"columns":["string"],"sort":"string","isShared":true}}}}}},"/api/v1/saved-searches/{id}":{"get":{"responses":{"200":{"description":"Die gespeicherte Suche","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der gespeicherten Suche (UUID)"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, zu dem die Suche gehoert"},"userId":{"type":"string","minLength":1,"description":"Benutzer, dem die Suche gehoert — nur er darf sie aendern und loeschen"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Anzeigename der Suche in der Auswahlliste"},"entityType":{"type":"string","minLength":1,"maxLength":50,"description":"Liste, fuer die die Suche gilt, z. B. `invoices` — frei waehlbar, nicht gegen eine Liste geprueft"},"filters":{"type":"object","additionalProperties":{},"description":"Die gespeicherten Filter als freies Objekt; leeres Objekt, wenn keine gesetzt sind"},"columns":{"type":"array","items":{"type":"string"},"description":"Die sichtbaren Spalten in ihrer Reihenfolge; leer, wenn die Standardspalten gelten"},"sort":{"type":"string","maxLength":100,"description":"Die gespeicherte Sortierung; leerer Text, wenn die Standardsortierung gilt"},"isShared":{"type":"boolean","description":"`true` = alle im Mandanten sehen die Suche, aendern darf sie weiterhin nur der Eigentuemer"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","userId","name","entityType","filters","columns","sort","isShared","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","userId":"string","name":"string","entityType":"string","filters":{},"columns":["string"],"sort":"string","isShared":true,"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht vorhanden, geloescht, oder fremd und nicht geteilt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"saved_search_not_found","description":"Feste Fehlerkennung. Deckt auch „gehoert einem anderen Benutzer\" ab — es gibt kein 403"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Abfrage fehlgeschlagen (JSON nach dem Schema). Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext `database unavailable`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Saved-searchesById","tags":["saved-searches"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Gespeicherte Suche lesen","description":"Liefert eine einzelne gespeicherte Suche OHNE Umschlag — die Felder stehen direkt im Wurzelobjekt. Lesbar sind die eigenen und die geteilten; alles andere antwortet mit 404, nicht mit 403."},"put":{"responses":{"200":{"description":"Die Suche nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der gespeicherten Suche (UUID)"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, zu dem die Suche gehoert"},"userId":{"type":"string","minLength":1,"description":"Benutzer, dem die Suche gehoert — nur er darf sie aendern und loeschen"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Anzeigename der Suche in der Auswahlliste"},"entityType":{"type":"string","minLength":1,"maxLength":50,"description":"Liste, fuer die die Suche gilt, z. B. `invoices` — frei waehlbar, nicht gegen eine Liste geprueft"},"filters":{"type":"object","additionalProperties":{},"description":"Die gespeicherten Filter als freies Objekt; leeres Objekt, wenn keine gesetzt sind"},"columns":{"type":"array","items":{"type":"string"},"description":"Die sichtbaren Spalten in ihrer Reihenfolge; leer, wenn die Standardspalten gelten"},"sort":{"type":"string","maxLength":100,"description":"Die gespeicherte Sortierung; leerer Text, wenn die Standardsortierung gilt"},"isShared":{"type":"boolean","description":"`true` = alle im Mandanten sehen die Suche, aendern darf sie weiterhin nur der Eigentuemer"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","tenantId","userId","name","entityType","filters","columns","sort","isShared","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","userId":"string","name":"string","entityType":"string","filters":{},"columns":["string"],"sort":"string","isShared":true,"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Rumpf abgelehnt (rohes Zod-Ergebnis)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":false,"description":"Immer `false` — die Eingabe wurde abgelehnt"},"error":{"type":"object","additionalProperties":{},"description":"Rohes Zod-Fehlerobjekt. Innere Form ist NICHT zugesagt und kann sich mit der Zod-Version aendern"}},"required":["success","error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht vorhanden, geloescht, oder sie gehoert einem anderen Benutzer","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"saved_search_not_found","description":"Feste Fehlerkennung. Deckt auch „gehoert einem anderen Benutzer\" ab — es gibt kein 403"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"UPDATE fehlgeschlagen (JSON nach dem Schema) — es wurde nichts geaendert. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext `database unavailable`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1Saved-searchesById","tags":["saved-searches"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Gespeicherte Suche aktualisieren","description":"Aendert die EIGENE gespeicherte Suche und gibt sie OHNE Umschlag zurueck. Eine fremde — auch eine geteilte — antwortet mit 404, nicht mit 403. Ein Rumpf ohne aenderbares Feld ist kein Fehler: die Suche kommt dann unveraendert mit 200 zurueck, und auch `updatedAt` bleibt stehen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"entityType":{"type":"string","minLength":1,"maxLength":50},"filters":{"type":"object","additionalProperties":{},"default":{}},"columns":{"type":"array","items":{"type":"string"},"default":[]},"sort":{"type":"string","maxLength":100,"default":""},"isShared":{"type":"boolean","default":false}}},"example":{"name":"string","entityType":"string","filters":{},"columns":["string"],"sort":"string","isShared":true}}}}},"delete":{"responses":{"200":{"description":"Die Suche ist entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","minLength":1,"description":"Deutscher Satz der Form `Saved search <id> gelöscht`"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht vorhanden, bereits geloescht, oder sie gehoert einem anderen Benutzer","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"saved_search_not_found","description":"Feste Fehlerkennung. Deckt auch „gehoert einem anderen Benutzer\" ab — es gibt kein 403"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Abfrage fehlgeschlagen (JSON nach dem Schema) — es wurde nichts entfernt. Fehlt der Datenbank-Client ganz, kommt unter demselben Code stattdessen Klartext `database unavailable`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"retryAfter":{"type":"number","const":5,"description":"Empfohlene Wartezeit in Sekunden bis zum naechsten Versuch"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"deleteApiV1Saved-searchesById","tags":["saved-searches"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Gespeicherte Suche löschen","description":"Setzt `deleted_at` auf der EIGENEN gespeicherten Suche — die Zeile bleibt in der Datenbank stehen und ist nur nicht mehr sichtbar. Eine fremde Suche antwortet mit 404, nicht mit 403; der zweite Klick ebenfalls, weil bereits Geloeschtes nicht noch einmal getroffen wird."}},"/api/v1/saved-views":{"get":{"responses":{"200":{"description":"Liste der saved views, im `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"scope":{"type":"string"},"module":{"type":"string"},"name":{"type":"string"},"filter":{},"sort":{},"columns":{},"isDefault":{"type":"boolean"},"isShared":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","userId","scope","module","name","isDefault","isShared"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","tenantId":"string","userId":"string","scope":"string","module":"string","name":"string","isDefault":true,"isShared":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Saved-views","tags":["saved-views"],"parameters":[{"in":"query","name":"module","schema":{"type":"string","minLength":1,"maxLength":64},"required":true}],"description":"Listet gespeicherte Ansichten fuer ein Modul (eigene + shared + tenant-default). `module` ist PFLICHT — ohne den Parameter kommt 400, es gibt keine modulweite Gesamtliste. Sichtbar ist eine Ansicht, wenn sie den Bereich `tenant-default` hat, als `shared` markiert und wirklich geteilt ist, ODER dem anfragenden Nutzer gehoert. Sortiert: die Standardansicht zuerst, danach nach Name. Ohne Blaetterung und ohne Obergrenze.","summary":"Listet gespeicherte Ansichten fuer ein Modul (eigene + shared + tenant-default)","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Saved View angelegt — der Datensatz flach, ohne `data`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"scope":{"type":"string"},"module":{"type":"string"},"name":{"type":"string"},"filter":{},"sort":{},"columns":{},"isDefault":{"type":"boolean"},"isShared":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","userId","scope","module","name","isDefault","isShared"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","scope":"string","module":"string","name":"string","isDefault":true,"isShared":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Saved-views","tags":["saved-views"],"parameters":[],"description":"Legt eine neue gespeicherte Ansicht an. Pflicht sind `module` und `name`; ohne Angabe entsteht sie mit Bereich `private`, leeren Filtern, leerer Sortierung, leerer Spaltenliste und beiden Kennzeichen auf false. Mit `isDefault: true` verlieren die uebrigen Ansichten desselben Nutzers in DEMSELBEN Modul ihr Kennzeichen — die beiden Schreibvorgaenge laufen nacheinander, nicht in einer Transaktion. Ein doppelter Name wird nicht abgelehnt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"module":{"type":"string","minLength":1,"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":120},"scope":{"type":"string","enum":["private","shared","tenant-default"],"default":"private"},"filter":{"type":"object","additionalProperties":{},"default":{}},"sort":{"type":"object","additionalProperties":{},"default":{}},"columns":{"type":"array","items":{"type":"string"},"default":[]},"isDefault":{"type":"boolean","default":false},"isShared":{"type":"boolean","default":false}},"required":["module","name"]},"example":{"module":"string","name":"string","scope":"private","filter":{},"sort":{},"columns":["string"],"isDefault":true,"isShared":true}}}},"summary":"Legt eine neue gespeicherte Ansicht an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/saved-views/{id}":{"put":{"responses":{"200":{"description":"Aktualisiert — der vollstaendige Datensatz, nicht nur die geaenderten Felder.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"scope":{"type":"string"},"module":{"type":"string"},"name":{"type":"string"},"filter":{},"sort":{},"columns":{},"isDefault":{"type":"boolean"},"isShared":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","userId","scope","module","name","isDefault","isShared"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","scope":"string","module":"string","name":"string","isDefault":true,"isShared":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"putApiV1Saved-viewsById","tags":["saved-views"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aktualisiert eine gespeicherte Ansicht. Geschrieben werden nur die mitgeschickten Felder; `updated_at` zieht immer mit. Aendern darf, wem die Ansicht gehoert — und ausserdem JEDER im Mandanten, sobald ihr Bereich nicht mehr `private` ist. `module` laesst sich hier NICHT aendern: das Feld wird angenommen, aber nicht geschrieben. Mit `isDefault: true` verlieren die uebrigen Ansichten desselben Nutzers in demselben Modul ihr Kennzeichen. Eine fremde private oder unbekannte Ansicht ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"module":{"type":"string","minLength":1,"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":120},"scope":{"type":"string","enum":["private","shared","tenant-default"],"default":"private"},"filter":{"type":"object","additionalProperties":{},"default":{}},"sort":{"type":"object","additionalProperties":{},"default":{}},"columns":{"type":"array","items":{"type":"string"},"default":[]},"isDefault":{"type":"boolean","default":false},"isShared":{"type":"boolean","default":false}}},"example":{"module":"string","name":"string","scope":"private","filter":{},"sort":{},"columns":["string"],"isDefault":true,"isShared":true}}}},"summary":"Aktualisiert eine gespeicherte Ansicht","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"204":{"description":"Geloescht — 204 No Content, die Antwort hat KEINEN Rumpf. Deshalb steht hier auch kein Schema."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"deleteApiV1Saved-viewsById","tags":["saved-views"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Loescht eine gespeicherte Ansicht. Die Zeile wird endgueltig aus `public.saved_views` entfernt — kein `deleted_at`, kein Zurueckholen. Loeschen darf, wem die Ansicht gehoert; zusaetzlich darf JEDER im Mandanten eine Ansicht im Bereich `tenant-default` entfernen. Eine geteilte Ansicht eines anderen Nutzers ist damit NICHT loeschbar, obwohl sie sich aendern laesst. Unbekannt oder nicht erlaubt ergibt gleichermassen 404.","summary":"Loescht eine gespeicherte Ansicht","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/saved-views/{id}/share":{"post":{"responses":{"200":{"description":"Geteilt — die vollstaendige Ansicht, mit `shared_with` in `filter`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"userId":{"type":["string","null"]},"scope":{"type":"string"},"module":{"type":"string"},"name":{"type":"string"},"filter":{},"sort":{},"columns":{},"isDefault":{"type":"boolean"},"isShared":{"type":"boolean"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","userId","scope","module","name","isDefault","isShared"],"additionalProperties":false},"example":{"id":"string","tenantId":"string","userId":"string","scope":"string","module":"string","name":"string","isDefault":true,"isShared":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"postApiV1Saved-viewsByIdShare","tags":["saved-views"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Markiert eine Ansicht als shared (optional mit user-Liste fuer zukuenftige ACL). Gesetzt werden Bereich `shared` und `is_shared = TRUE`; die mitgegebenen `userIds` landen als `shared_with` IM FILTER-JSON und schraenken derzeit NICHTS ein — die Ansicht ist danach fuer jeden im Mandanten sichtbar, auch bei leerer Liste. Teilen darf nur, wem die Ansicht gehoert; alles andere ergibt 404. Ein Weg zurueck fuehrt ueber `PUT /api/v1/saved-views/{id}` mit `scope` und `isShared`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":0}},"required":["userIds"]},"example":{"userIds":["00000000-0000-4000-8000-000000000000"]}}}},"summary":"Markiert eine Ansicht als shared (optional mit user-Liste fuer zukuenftige ACL)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dashboard-config":{"get":{"responses":{"200":{"description":"Die gespeicherte Einrichtung, sonst der Rollen-Standard.","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","description":"Faellt zurueck auf die Mandanten-Id, wenn kein Nutzer im Kontext steht"},"role":{"type":"string","description":"admin | manager | accountant | sales | member | viewer"},"widgets":{"description":"Bei `isCustom: true` der rohe Inhalt der gespeicherten JSONB-Spalte, bei `isCustom: false` die Standard-Kacheln der Rolle (Form: siehe /defaults/{role})"},"isCustom":{"type":"boolean","description":"false = es ist nichts gespeichert, das ist der Rollen-Standard"},"updatedAt":{"type":["string","null"],"description":"null im Standardfall — da gibt es keine Zeile"}},"required":["userId","role","isCustom","updatedAt"]},"example":{"userId":"string","role":"string","isCustom":true,"updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Dashboard-config","tags":["dashboard-config"],"parameters":[],"summary":"Liefert die Dashboard-Einrichtung des angemeldeten Nutzers","description":"Liefert die Kacheln des angemeldeten Nutzers. Hat er selbst etwas gespeichert, kommt das (`isCustom: true`); sonst der Standard seiner Rolle — dann OHNE Zeitstempel und mit `isCustom: false`. Ein Standard ist also keine leere Antwort, sondern eine gueltige. Die Rolle bestimmt der Server aus dem Anmeldekontext; sie laesst sich hier nicht mitgeben. Wer die Kacheln einer ANDEREN Rolle sehen will, nimmt `/defaults/{role}`."},"put":{"responses":{"200":{"description":"Die gespeicherte Einrichtung, so wie sie ab jetzt gilt.","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","description":"Faellt zurueck auf die Mandanten-Id, wenn kein Nutzer im Kontext steht"},"role":{"type":"string","description":"admin | manager | accountant | sales | member | viewer"},"widgets":{"description":"Bei `isCustom: true` der rohe Inhalt der gespeicherten JSONB-Spalte, bei `isCustom: false` die Standard-Kacheln der Rolle (Form: siehe /defaults/{role})"},"isCustom":{"type":"boolean","description":"false = es ist nichts gespeichert, das ist der Rollen-Standard"},"updatedAt":{"type":["string","null"],"description":"null im Standardfall — da gibt es keine Zeile"}},"required":["userId","role","isCustom","updatedAt"]},"example":{"userId":"string","role":"string","isCustom":true,"updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1Dashboard-config","tags":["dashboard-config"],"parameters":[],"description":"Speichert die Kacheln des angemeldeten Nutzers. Es gibt je Nutzer und Mandant genau eine Zeile: ein zweiter Aufruf ueberschreibt sie vollstaendig — `widgets` ist die GANZE Liste, nicht eine Ergaenzung, und eine weggelassene Kachel ist danach weg. Ein mitgeschicktes `role` wird uebernommen und ersetzt die aus dem Anmeldekontext abgeleitete; die Kacheln selbst werden dadurch NICHT ausgetauscht. Der Aufruf antwortet 200, auch wenn er die Zeile neu angelegt hat.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["admin","manager","accountant","sales","member","viewer"]},"widgets":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":50},"type":{"type":"string","minLength":1,"maxLength":50},"title":{"type":"string","minLength":1,"maxLength":100},"size":{"type":"string","enum":["sm","md","lg"],"default":"md"},"visible":{"type":"boolean","default":true},"order":{"type":"integer","minimum":0,"default":0},"config":{"type":"object","additionalProperties":{}}},"required":["id","type","title"]}}},"required":["widgets"]},"example":{"role":"admin","widgets":[{"id":"string","type":"string","title":"string","size":"sm","visible":true,"order":0,"config":{}}]}}}},"summary":"Speichert die Kacheln des angemeldeten Nutzers","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Zurückgesetzt; die Antwort traegt bereits die Standard-Kacheln.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"userId":{"type":"string"},"role":{"type":"string"},"widgets":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","description":"kpi | chart | table | feed | quick-actions | status"},"title":{"type":"string"},"size":{"type":"string","description":"sm | md | lg"},"visible":{"type":"boolean"},"order":{"type":"integer","description":"Kleiner heisst weiter oben"},"config":{"type":"object","additionalProperties":{}}},"required":["id","type","title","size","visible","order"]},"description":"Die Standard-Kacheln der Rolle, ab jetzt wieder gueltig"},"isCustom":{"type":"boolean","const":false}},"required":["message","userId","role","widgets","isCustom"]},"example":{"message":"string","userId":"string","role":"string","widgets":[{"id":"string","type":"string","title":"string","size":"string","visible":true,"order":0,"config":{}}],"isCustom":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"deleteApiV1Dashboard-config","tags":["dashboard-config"],"parameters":[],"summary":"Eigene Dashboard-Einrichtung verwerfen, Rollen-Standard gilt wieder","description":"Verwirft die eigene Einrichtung des angemeldeten Nutzers: die gespeicherte Zeile wird ENDGUELTIG geloescht, nicht nur ausgeblendet — eine Ruecknahme gibt es nicht. Danach gilt wieder der Standard seiner Rolle, und den traegt die Antwort bereits, damit die Oberflaeche nicht nachladen muss. Der Aufruf ist wiederholbar und antwortet 200, auch wenn es gar nichts zu loeschen gab."}},"/api/v1/dashboard-config/defaults/{role}":{"get":{"responses":{"200":{"description":"Die Standard-Kacheln dieser Rolle.","content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string"},"widgets":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","description":"kpi | chart | table | feed | quick-actions | status"},"title":{"type":"string"},"size":{"type":"string","description":"sm | md | lg"},"visible":{"type":"boolean"},"order":{"type":"integer","description":"Kleiner heisst weiter oben"},"config":{"type":"object","additionalProperties":{}}},"required":["id","type","title","size","visible","order"]}},"isDefault":{"type":"boolean","const":true}},"required":["role","widgets","isDefault"]},"example":{"role":"string","widgets":[{"id":"string","type":"string","title":"string","size":"string","visible":true,"order":0,"config":{}}],"isDefault":true}}}},"400":{"description":"Ungültige Rolle — die Antwort nennt die gueltigen unter `validRoles`"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Dashboard-configDefaultsByRole","tags":["dashboard-config"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"role","required":true}],"description":"Liefert die fest hinterlegten Standard-Kacheln einer Rolle — ohne Datenbankzugriff und ohne Bezug zum angemeldeten Nutzer. Erlaubt sind `admin`, `manager`, `accountant`, `sales`, `member` und `viewer`; alles andere ergibt 400 und die Antwort nennt die gueltigen Werte. Gedacht als Vorschau, bevor jemand seine eigene Einrichtung ueber DELETE verwirft.","summary":"Liefert die fest hinterlegten Standard-Kacheln einer Rolle","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/analytics/kpis":{"get":{"responses":{"200":{"description":"Die vier Kennzahlen. Zahlen sind gemessen, `null` heisst „nicht ermittelbar\".","content":{"application/json":{"schema":{"type":"object","properties":{"revenue":{"type":["number","null"],"description":"Bezahlter Umsatz im laufenden Monat, in Euro. `null` = nicht ermittelbar (Abfrage gescheitert), NICHT 0."},"revenueChange":{"type":["number","null"],"description":"Veraenderung zum Vormonat in Prozent, gerundet. `null`, wenn der Vormonat 0 war (eine Steigerung von null aus ist nicht ausdrueckbar) ODER wenn einer der beiden Umsaetze nicht ermittelbar war."},"openInvoices":{"type":["integer","null"],"description":"Anzahl offener Rechnungen. `null` = nicht ermittelbar."},"activeCustomers":{"type":["integer","null"],"description":"Nicht geloeschte Kunden. `null` = nicht ermittelbar."},"unavailable":{"type":"boolean","const":true,"description":"Nur gesetzt, wenn GAR NICHTS ermittelt werden konnte (kein Mandantenkontext, Datenbank weg, alle vier Abfragen gescheitert). Eine einzelne gescheiterte Abfrage setzt es NICHT — die zeigt sich als `null` in ihrem eigenen Feld."}},"required":["revenue","revenueChange","openInvoices","activeCustomers"]},"example":{"revenue":0,"revenueChange":0,"openInvoices":0,"activeCustomers":0,"unavailable":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AnalyticsKpis","tags":["analytics"],"parameters":[],"summary":"Kennzahlen-Paket fuer die Dashboard-Uebersicht","description":"Liefert die vier Kacheln der Startseite in einem Aufruf. Gegenstueck zur\nEINZAHL-Route `/analytics/kpi?metric=…`, die genau einen frei gewaehlten\nWert zurueckgibt — beide bestehen nebeneinander und haben verschiedene\nAntwortformen.\n\nEINE ZAHL IST GEMESSEN, EIN `null` IST NICHT ERMITTELBAR. Die vier\nAbfragen laufen nebenlaeufig und scheitern unabhaengig voneinander. Eine\ngescheiterte gibt `null` in ihrem eigenen Feld; die uebrigen behalten\nihre gemessene Zahl. Eine `0` heisst also wirklich null.\n\n`unavailable: true` kommt nur, wenn GAR NICHTS ermittelt werden konnte —\nkein Mandantenkontext, Datenbank weg, oder alle Abfragen gescheitert.\n\nDer Statuscode bleibt in allen Faellen 200: ein 5xx wuerde als roter\nFehlerkasten erscheinen, obwohl eine Kachel ohne Wert kein Seitenfehler\nist.\n\n`revenueChange` ist `null` aus ZWEI Gruenden: der Vormonat war 0 (eine\nSteigerung von null aus ist nicht ausdrueckbar) oder einer der beiden\nUmsaetze war nicht ermittelbar. Die Antwort unterscheidet das nicht.\n\n`activeCustomers` faellt still auf die Alt-Tabelle `contacts` zurueck,\nwenn `customers` nicht lesbar ist. Die Antwort sagt nicht, welche der\nbeiden gezaehlt wurde. Scheitern beide, ist der Wert `null`.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."}},"/api/v1/analytics/kpi":{"get":{"responses":{"200":{"description":"Der Wert, oder das ausdrueckliche „nicht ermittelbar\".","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"value":{"type":"number","description":"Der Messwert. 0 heisst wirklich null, nicht „unbekannt\"."},"label":{"type":"string","description":"Deutsche Beschriftung, aus dem Quelltext."},"unit":{"type":"string","description":"EUR oder % — sonst nicht gesetzt."}},"required":["value"]},{"type":"object","properties":{"value":{"type":"null","description":"Kein Wert ermittelbar."},"unavailable":{"type":"boolean","const":true}},"required":["value","unavailable"]}]},"example":{"value":0,"label":"string","unit":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AnalyticsKpi","tags":["analytics"],"parameters":[],"summary":"Einen einzelnen Kennwert holen","description":"Liefert EINEN Kennwert, ausgewaehlt ueber `?metric=`. Gegenstueck zur\nMEHRZAHL-Route `/analytics/kpis`, die ein festes Vierer-Paket fuer die\nDashboard-Uebersicht zurueckgibt — die beiden sind NICHT dieselbe Route\nin zwei Schreibweisen und haben verschiedene Antwortformen.\n\nBekannte Werte fuer `metric`: `revenue`, `open_invoices_amount`,\n`open_invoices`, `active_customers`, `open_tickets_count`,\n`low_stock_count`, `shipments_today_count`, `orders_week_count`,\n`deals_won_month`, `win_loss_rate`.\n\nZWEI FORMEN UNTER 200, und der Unterschied ist der Punkt:\n· `{ value: <Zahl> }` — gemessen. Eine 0 heisst wirklich null.\n· `{ value: null, unavailable: true }` — nicht ermittelbar (Datenbank\n  weg, Abfrage gescheitert, kein Mandantenkontext). Die Kachel zeigt\n  dann „—\" statt einer Zahl, die es nie gab.\n\nDer Statuscode bleibt in beiden Faellen 200, und das ist Absicht: ein\n5xx wuerde in der Oberfläche als roter Fehlerkasten erscheinen, obwohl\neine einzelne Kachel ohne Wert kein Seitenfehler ist.\n\nAUSNAHME, die man kennen muss: ein UNBEKANNTER oder fehlender\n`metric`-Wert gibt `{ value: 0 }` — nicht `unavailable`. Wer sich\nvertippt, bekommt also eine glaubwuerdige Null. Das ist eine bewusste\nEntscheidung des Quelltexts (kein 400, kein 500), aber ein Aufrufer\nsollte seine Kennung gegen die Liste oben pruefen.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."}},"/api/v1/dashboard/layouts":{"get":{"responses":{"200":{"description":"Alle Dashboards des Nutzers, Standard zuerst. `layout` und `layout_jsonb` tragen denselben Wert — `layout` ist der Alt-Schluessel, neuer Code liest `layout_jsonb`.","content":{"application/json":{"schema":{"type":"object","properties":{"layouts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"user_id":{"type":"string"},"name":{"type":"string"},"is_default":{"type":"boolean"},"layout":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"layout_jsonb":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","name","is_default","layout","layout_jsonb","created_at","updated_at"],"additionalProperties":false}}},"required":["layouts"],"additionalProperties":false},"example":{"layouts":[{"id":"string","user_id":"string","name":"string","is_default":true,"layout":{"widgets":[]},"layout_jsonb":{"widgets":[]},"created_at":"string","updated_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1DashboardLayouts","tags":["dashboard"],"parameters":[],"description":"Liefert alle Dashboard-Layouts des eingeloggten Users. Gelesen wird `tenant_user_dashboards` im Mandanten-Schema, zusaetzlich gefiltert auf die `user_id` des angemeldeten Nutzers — fremde Dashboards sind auch innerhalb desselben Mandanten nicht sichtbar. Sortiert wird das Standard-Dashboard zuerst, danach nach letzter Aenderung. Weder Blaetterung noch Obergrenze.","summary":"Liefert alle Dashboard-Layouts des eingeloggten Users","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Das angelegte Dashboard. `layout` und `layout_jsonb` tragen denselben Wert und immer die Form `{ widgets: [...] }`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"user_id":{"type":"string"},"name":{"type":"string"},"is_default":{"type":"boolean"},"layout":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"layout_jsonb":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","name","is_default","layout","layout_jsonb","created_at","updated_at"],"additionalProperties":false},"example":{"id":"string","user_id":"string","name":"string","is_default":true,"layout":{"widgets":[]},"layout_jsonb":{"widgets":[]},"created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"Name bereits vergeben"}},"operationId":"postApiV1DashboardLayouts","tags":["dashboard"],"parameters":[],"description":"Legt ein neues Dashboard-Layout an. Optional via ?from_preset=executive|operations|sales aus einem Preset.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"layout_jsonb":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"i":{"type":"string","minLength":1,"maxLength":80},"x":{"type":"integer","minimum":0,"maximum":11},"y":{"type":"integer","minimum":0},"w":{"type":"integer","minimum":1,"maximum":12},"h":{"type":"integer","minimum":1,"maximum":50},"minW":{"type":"integer","minimum":1,"maximum":12},"minH":{"type":"integer","minimum":1,"maximum":50},"type":{"type":"string","minLength":1,"maxLength":60},"config":{"type":"object","additionalProperties":{},"default":{}}},"required":["i","x","y","w","h","type"]}},{"type":"object","properties":{"widgets":{"type":"array","items":{"type":"object","properties":{"i":{"type":"string","minLength":1,"maxLength":80},"x":{"type":"integer","minimum":0,"maximum":11},"y":{"type":"integer","minimum":0},"w":{"type":"integer","minimum":1,"maximum":12},"h":{"type":"integer","minimum":1,"maximum":50},"minW":{"type":"integer","minimum":1,"maximum":12},"minH":{"type":"integer","minimum":1,"maximum":50},"type":{"type":"string","minLength":1,"maxLength":60},"config":{"type":"object","additionalProperties":{},"default":{}}},"required":["i","x","y","w","h","type"]}}},"required":["widgets"]}],"default":{"widgets":[]}},"is_default":{"type":"boolean"}},"required":["name"]},"example":{"name":"string","layout_jsonb":[{"i":"string","x":0,"y":0,"w":1,"h":1,"minW":1,"minH":1,"type":"string","config":{}}],"is_default":true}}}},"summary":"Legt ein neues Dashboard-Layout an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dashboard/layouts/{id}":{"get":{"responses":{"200":{"description":"Layout","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"user_id":{"type":"string"},"name":{"type":"string"},"is_default":{"type":"boolean"},"layout":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"layout_jsonb":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","name","is_default","layout","layout_jsonb","created_at","updated_at"],"additionalProperties":false},"example":{"id":"string","user_id":"string","name":"string","is_default":true,"layout":{"widgets":[]},"layout_jsonb":{"widgets":[]},"created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"getApiV1DashboardLayoutsById","tags":["dashboard"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liefert ein einzelnes Dashboard-Layout. Gelesen wird ausschliesslich ein Dashboard des angemeldeten Nutzers — ein fremdes und ein unbekanntes ergeben gleichermassen 404, die Antwort verraet den Unterschied nicht. `layout` und `layout_jsonb` tragen denselben Wert und immer die Form `{ widgets: [...] }`: Alt-Zeilen, die als blankes Array in der Datenbank liegen, werden beim Lesen umgeformt.","summary":"Liefert ein einzelnes Dashboard-Layout","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Aktualisiert — das vollstaendige Dashboard, nicht nur die geaenderten Felder.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"user_id":{"type":"string"},"name":{"type":"string"},"is_default":{"type":"boolean"},"layout":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"layout_jsonb":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","name","is_default","layout","layout_jsonb","created_at","updated_at"],"additionalProperties":false},"example":{"id":"string","user_id":"string","name":"string","is_default":true,"layout":{"widgets":[]},"layout_jsonb":{"widgets":[]},"created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"putApiV1DashboardLayoutsById","tags":["dashboard"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aktualisiert Name, Layout oder Default-Flag eines Dashboards. Geschrieben werden nur die mitgeschickten Felder; enthaelt der Rumpf keines davon, antwortet der Aufruf 400 `no_fields_to_update` und fasst nichts an. Mit `is_default: true` verliert das bisherige Standard-Dashboard desselben Nutzers sein Kennzeichen — die beiden Schreibvorgaenge laufen nacheinander, nicht in einer Transaktion. Ein bereits vergebener Name ergibt 409 `name_already_in_use`; ein fremdes oder unbekanntes Dashboard 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"layout_jsonb":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"i":{"type":"string","minLength":1,"maxLength":80},"x":{"type":"integer","minimum":0,"maximum":11},"y":{"type":"integer","minimum":0},"w":{"type":"integer","minimum":1,"maximum":12},"h":{"type":"integer","minimum":1,"maximum":50},"minW":{"type":"integer","minimum":1,"maximum":12},"minH":{"type":"integer","minimum":1,"maximum":50},"type":{"type":"string","minLength":1,"maxLength":60},"config":{"type":"object","additionalProperties":{},"default":{}}},"required":["i","x","y","w","h","type"]}},{"type":"object","properties":{"widgets":{"type":"array","items":{"type":"object","properties":{"i":{"type":"string","minLength":1,"maxLength":80},"x":{"type":"integer","minimum":0,"maximum":11},"y":{"type":"integer","minimum":0},"w":{"type":"integer","minimum":1,"maximum":12},"h":{"type":"integer","minimum":1,"maximum":50},"minW":{"type":"integer","minimum":1,"maximum":12},"minH":{"type":"integer","minimum":1,"maximum":50},"type":{"type":"string","minLength":1,"maxLength":60},"config":{"type":"object","additionalProperties":{},"default":{}}},"required":["i","x","y","w","h","type"]}}},"required":["widgets"]}]},"is_default":{"type":"boolean"}}},"example":{"name":"string","layout_jsonb":[{"i":"string","x":0,"y":0,"w":1,"h":1,"minW":1,"minH":1,"type":"string","config":{}}],"is_default":true}}}},"summary":"Aktualisiert Name, Layout oder Default-Flag eines Dashboards","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Geloescht — nur eine Quittung mit der getroffenen Kennung, kein Datensatz.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["deleted","id"],"additionalProperties":false},"example":{"deleted":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"deleteApiV1DashboardLayoutsById","tags":["dashboard"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Loescht ein Dashboard-Layout (hard-delete). Die Zeile wird endgueltig aus `tenant_user_dashboards` entfernt — es gibt kein `deleted_at` und kein Zurueckholen. Getroffen wird nur ein Dashboard des angemeldeten Nutzers; ein fremdes oder unbekanntes ergibt 404. War es das Standard-Dashboard, hat der Nutzer danach keines mehr: ein anderes rueckt NICHT nach.","summary":"Loescht ein Dashboard-Layout (hard-delete)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dashboard/layouts/{id}/set-default":{"post":{"responses":{"200":{"description":"Default gesetzt — zurueck kommt das vollstaendige Dashboard mit `is_default: true`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"user_id":{"type":"string"},"name":{"type":"string"},"is_default":{"type":"boolean"},"layout":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"layout_jsonb":{"type":"object","properties":{"widgets":{"type":"array","items":{}}},"required":["widgets"],"additionalProperties":false},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","name","is_default","layout","layout_jsonb","created_at","updated_at"],"additionalProperties":false},"example":{"id":"string","user_id":"string","name":"string","is_default":true,"layout":{"widgets":[]},"layout_jsonb":{"widgets":[]},"created_at":"string","updated_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"postApiV1DashboardLayoutsByIdSet-default","tags":["dashboard"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt ein Layout als Default. Bestehender Default desselben Users wird automatisch entfernt.","summary":"Setzt ein Layout als Default","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dashboard/widgets/types":{"get":{"responses":{"200":{"description":"Alle verfuegbaren Widget-Typen samt Konfigurationsform, dazu die Vorlagen.","content":{"application/json":{"schema":{"type":"object","properties":{"types":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"name":{"type":"string"},"icon":{"type":"string"},"description":{"type":"string"},"configSchema":{}},"required":["type","name","icon","description"],"additionalProperties":false}},"presets":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}},"required":["key","name","description"],"additionalProperties":false}}},"required":["types","presets"],"additionalProperties":false},"example":{"types":[{"type":"string","name":"string","icon":"string","description":"string"}],"presets":[{"key":"string","name":"string","description":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1DashboardWidgetsTypes","tags":["dashboard"],"parameters":[],"description":"Listet alle verfuegbaren Widget-Typen mit Meta + configSchema. Die Liste ist eine feste Registry im Servercode, keine Mandantendaten — sie ist fuer jeden Aufrufer gleich und aendert sich nur mit einer neuen Version. Je Typ kommen Kennung, Anzeigename, Symbol, Beschreibung und die Feldliste, aus der die Oberflaeche das Konfigurationsformular baut. Daneben steht unter `presets` die Auswahl an Dashboard-Vorlagen, die `POST /api/v1/dashboard/layouts?from_preset=…` annimmt.","summary":"Listet alle verfuegbaren Widget-Typen mit Meta + configSchema","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dashboard/widgets/{type}/data":{"get":{"responses":{"200":{"description":"Daten der Kachel. ACHTUNG: `unavailable: true` heisst, dass die Zielroute nicht erreichbar war — die Kachel ist dann leer WEGEN eines Fehlers, nicht mangels Daten. Bei Erfolg wird der Rumpf der Zielroute unveraendert durchgereicht.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"type":{"type":"string"},"config":{"type":"object","additionalProperties":{}}},"required":["type","config"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"unavailable":{"type":"boolean","const":true},"upstreamStatus":{"type":"number"},"endpoint":{"type":"string"},"data":{"type":"null"}},"required":["type","unavailable","upstreamStatus","endpoint","data"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"unavailable":{"type":"boolean","const":true},"error":{"type":"string"},"endpoint":{"type":"string"},"data":{"type":"null"}},"required":["type","unavailable","error","endpoint","data"],"additionalProperties":false},{"type":"object","additionalProperties":{}}]},"example":{"type":"string","config":{}}}}},"400":{"description":"Ungueltiger Widget-Typ oder unsichere URL"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1DashboardWidgetsByTypeData","tags":["dashboard"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"type","required":true}],"summary":"Liefert die Daten fuer ein Widget des angegebenen Typs","description":"Generischer Proxy: liefert die Daten fuer ein Widget des angegebenen Typs basierend auf der mitgegebenen config."}},"/api/v1/sidebar-prefs":{"get":{"responses":{"200":{"description":"Prefs","content":{"application/json":{"schema":{"type":"object","properties":{"hiddenKeys":{"type":"array","items":{"type":"string"}},"groupOrder":{"type":"array","items":{"type":"string"}},"itemOrder":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},"updatedAt":{"type":["string","null"]}},"required":["hiddenKeys","groupOrder","itemOrder","updatedAt"]},"example":{"hiddenKeys":["string"],"groupOrder":["string"],"itemOrder":{"beispiel":["string"]},"updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Sidebar-prefs","tags":["sidebar-prefs"],"parameters":[],"description":"Liefert die Sidebar-Einstellungen des angemeldeten Nutzers. Gelesen wird genau eine Zeile aus `tenant_user_sidebar_prefs` im Mandantenschema, gefiltert auf die `user_id` aus dem Kontext; die Tabelle wird dabei bei Bedarf selbstheilend angelegt. Fehlt die Zeile, antwortet die Route mit 200 und leeren Defaults statt mit 404. Auch ein Datenbankfehler endet in denselben leeren Defaults, damit die Sidebar nie blockiert: ein 200 beweist also nicht, dass gespeicherte Werte gelesen wurden.","summary":"Liefert die Sidebar-Einstellungen des angemeldeten Nutzers","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Prefs gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"hiddenKeys":{"type":"array","items":{"type":"string"}},"groupOrder":{"type":"array","items":{"type":"string"}},"itemOrder":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},"updatedAt":{"type":["string","null"]}},"required":["hiddenKeys","groupOrder","itemOrder","updatedAt"]},"example":{"hiddenKeys":["string"],"groupOrder":["string"],"itemOrder":{"beispiel":["string"]},"updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putApiV1Sidebar-prefs","tags":["sidebar-prefs"],"parameters":[],"description":"Persistiert Sidebar-Praeferenzen (hidden_keys, group_order, item_order). Upsert pro User.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"hiddenKeys":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z0-9/_\\-:.]+$","minLength":1,"maxLength":120},"maxItems":500},"groupOrder":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z0-9/_\\-:.]+$","minLength":1,"maxLength":120},"maxItems":100},"itemOrder":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z0-9/_\\-:.]+$","minLength":1,"maxLength":120},"maxItems":500}}}},"example":{"hiddenKeys":["00000000-0000-4000-8000-000000000000"],"groupOrder":["00000000-0000-4000-8000-000000000000"],"itemOrder":{"beispiel":["00000000-0000-4000-8000-000000000000"]}}}}},"summary":"Persistiert Sidebar-Praeferenzen (hidden_keys, group_order, item_order)","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Zurueckgesetzt","content":{"application/json":{"schema":{"type":"object","properties":{"reset":{"type":"boolean"}},"required":["reset"]},"example":{"reset":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1Sidebar-prefs","tags":["sidebar-prefs"],"parameters":[],"description":"Setzt die Sidebar-Einstellungen des Nutzers zurueck. Die Zeile verschwindet per SQL-DELETE aus `tenant_user_sidebar_prefs`, es gibt kein Soft-Delete und kein Rueckgaengig. Betroffen ist ausschliesslich die eigene `user_id`. Die Antwort lautet `{ reset: true }` auch dann, wenn gar keine Zeile vorhanden war.","summary":"Setzt die Sidebar-Einstellungen des Nutzers zurueck","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/task-display-prefs":{"get":{"responses":{"200":{"description":"Der gespeicherte Anzeigemodus — auch der Rückfall auf `initials` kommt hier an","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["initials","firstName","lastName"],"description":"Wie der Zustaendigen-Badge beschriftet wird: Initialen, Vorname oder Nachname"}},"required":["mode"]},"example":{"mode":"initials"}}}},"401":{"description":"Kein Benutzer- oder Mandantenkontext"}},"operationId":"getApiV1Task-display-prefs","tags":["task-display-prefs"],"parameters":[],"summary":"Liefert den Zuständigen-Anzeigemodus des eingeloggten Users (default initials)","description":"Liest `task_display_preferences` aus `tenant_user_sidebar_prefs` im Mandanten-Schema, gefiltert auf die eigene Benutzerkennung — jeder sieht ausschließlich seine eigene Einstellung. Ein fehlender oder unbekannter gespeicherter Wert wird auf `initials` normalisiert. Die Tabelle wird bei Bedarf selbst angelegt, und auch ein Fehler beim Lesen führt zu 200 mit `initials` statt zu einem Fehlercode: das Aufgaben-Board soll an einer Anzeigeeinstellung nicht hängenbleiben."},"put":{"responses":{"200":{"description":"Der gespeicherte Modus — zurueckgelesen aus der Datenbank, nicht der gesendete Wert","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["initials","firstName","lastName"],"description":"Wie der Zustaendigen-Badge beschriftet wird: Initialen, Vorname oder Nachname"}},"required":["mode"]},"example":{"mode":"initials"}}}},"400":{"description":"Unbekannter Modus im Rumpf"},"401":{"description":"Kein Benutzer- oder Mandantenkontext"},"503":{"description":"Nicht gespeichert — Datenbank nicht erreichbar"}},"operationId":"putApiV1Task-display-prefs","tags":["task-display-prefs"],"parameters":[],"description":"Persistiert den Zuständigen-Anzeigemodus (initials/firstName/lastName). Upsert pro User.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["initials","firstName","lastName"]}},"required":["mode"]},"example":{"mode":"initials"}}}},"summary":"Persistiert den Zuständigen-Anzeigemodus (initials/firstName/lastName)","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/account-schedules":{"get":{"responses":{"200":{"description":"Liste der Kontenschemata mit Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Kontenschemas"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem das Schema gehoert"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Name des Schemas, je Mandant eindeutig"},"description":{"type":"string","maxLength":500,"description":"Beschreibung; leerer String wenn keine erfasst"},"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":50,"description":"Kennung der Zeile, in Formeln referenzierbar"},"label":{"type":"string","maxLength":200,"description":"Beschriftung der Zeile im Bericht"},"accountFilter":{"type":"string","maxLength":500,"description":"Kontenfilter: einzelnes Konto (\"4400\"), Bereich (\"4000..4999\") oder mehrere durch | getrennt (\"4400|4300\"). Leer bei Ueberschriften."},"calculationType":{"type":"string","enum":["net_change","balance_at_date","formula","heading","total"],"description":"net_change = Bewegung im Zeitraum, balance_at_date = Saldo zum Stichtag, formula = Rechnung ueber Zeilen-Kennungen, heading = Ueberschrift ohne Wert, total = Summe ueber den Kontenfilter."},"bold":{"type":"boolean","description":"Zeile fett darstellen; fehlt bei den Vorlagenzeilen"},"indent":{"type":"integer","minimum":0,"maximum":5,"description":"Einrueckungstiefe 0-5"},"formula":{"type":"string","maxLength":500,"description":"Nur bei calculationType=formula: Rechnung ueber Zeilen-Kennungen, z. B. \"r1+r2-r3\""},"showOppositeSign":{"type":"boolean","description":"Vorzeichen des Werts umkehren"}},"required":["id","label","accountFilter","calculationType"],"description":"Eine Definitionszeile des Kontenschemas"},"description":"Die Definitionszeilen in Berichtsreihenfolge"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung"}},"required":["id","tenantId","name","description","rows","createdAt","updatedAt"],"description":"Ein Kontenschema — die Definition, nicht der gerechnete Bericht"},"description":"Die Schemata der aktuellen Seite, nach Namen sortiert"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller nicht geloeschten Schemata"}},"required":["limit","offset","total"],"description":"Seitenangaben"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, fuer den gezaehlt wurde"}},"required":["tenantId"],"description":"Angaben zur Abfrage"}},"required":["data","pagination","meta"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","name":"string","description":"string","rows":[{"id":"string","label":"string","accountFilter":"string","calculationType":"net_change","bold":true,"indent":0,"formula":"string","showOppositeSign":true}],"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"pagination":{"limit":1,"offset":0,"total":0},"meta":{"tenantId":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Account-schedules","tags":["account-schedules"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"description":"Listet alle Kontenschemata. Beim ersten Aufruf legt der Endpunkt zwei Vorlagen an (\"P&L Statement\", \"Balance Sheet\"), falls der Mandant noch keine hat.","summary":"Listet alle Kontenschemata","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Schema angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Kontenschemas"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem das Schema gehoert"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Name des Schemas, je Mandant eindeutig"},"description":{"type":"string","maxLength":500,"description":"Beschreibung; leerer String wenn keine erfasst"},"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":50,"description":"Kennung der Zeile, in Formeln referenzierbar"},"label":{"type":"string","maxLength":200,"description":"Beschriftung der Zeile im Bericht"},"accountFilter":{"type":"string","maxLength":500,"description":"Kontenfilter: einzelnes Konto (\"4400\"), Bereich (\"4000..4999\") oder mehrere durch | getrennt (\"4400|4300\"). Leer bei Ueberschriften."},"calculationType":{"type":"string","enum":["net_change","balance_at_date","formula","heading","total"],"description":"net_change = Bewegung im Zeitraum, balance_at_date = Saldo zum Stichtag, formula = Rechnung ueber Zeilen-Kennungen, heading = Ueberschrift ohne Wert, total = Summe ueber den Kontenfilter."},"bold":{"type":"boolean","description":"Zeile fett darstellen; fehlt bei den Vorlagenzeilen"},"indent":{"type":"integer","minimum":0,"maximum":5,"description":"Einrueckungstiefe 0-5"},"formula":{"type":"string","maxLength":500,"description":"Nur bei calculationType=formula: Rechnung ueber Zeilen-Kennungen, z. B. \"r1+r2-r3\""},"showOppositeSign":{"type":"boolean","description":"Vorzeichen des Werts umkehren"}},"required":["id","label","accountFilter","calculationType"],"description":"Eine Definitionszeile des Kontenschemas"},"description":"Die Definitionszeilen in Berichtsreihenfolge"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung"}},"required":["id","tenantId","name","description","rows","createdAt","updatedAt"],"description":"Ein Kontenschema — die Definition, nicht der gerechnete Bericht"},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","name":"string","description":"string","rows":[{"id":"string","label":"string","accountFilter":"string","calculationType":"net_change","bold":true,"indent":0,"formula":"string","showOppositeSign":true}],"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Nicht angelegt — auch dann, wenn der Name schon vergeben ist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Account-schedules","tags":["account-schedules"],"parameters":[],"description":"Legt ein neues Kontenschema an. Die Antwort ist das Schema selbst, ohne Umschlag.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":500,"default":""},"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":50,"description":"Kennung der Zeile, in Formeln referenzierbar"},"label":{"type":"string","maxLength":200,"description":"Beschriftung der Zeile im Bericht"},"accountFilter":{"type":"string","maxLength":500,"default":"","description":"Kontenfilter: einzelnes Konto (\"4400\"), Bereich (\"4000..4999\") oder mehrere durch | getrennt (\"4400|4300\"). Leer bei Ueberschriften."},"calculationType":{"type":"string","enum":["net_change","balance_at_date","formula","heading","total"],"description":"net_change = Bewegung im Zeitraum, balance_at_date = Saldo zum Stichtag, formula = Rechnung ueber Zeilen-Kennungen, heading = Ueberschrift ohne Wert, total = Summe ueber den Kontenfilter."},"bold":{"type":"boolean","default":false,"description":"Zeile fett darstellen"},"indent":{"type":"integer","minimum":0,"maximum":5,"default":0,"description":"Einrueckungstiefe 0-5"},"formula":{"type":"string","maxLength":500,"description":"Nur bei calculationType=formula: Rechnung ueber Zeilen-Kennungen, z. B. \"r1+r2-r3\""},"showOppositeSign":{"type":"boolean","default":false,"description":"Vorzeichen des Werts umkehren"}},"required":["id","label","calculationType"]}}},"required":["name","rows"]},"example":{"name":"string","description":"string","rows":[{"id":"string","label":"string","accountFilter":"string","calculationType":"net_change","bold":true,"indent":0,"formula":"string","showOppositeSign":true}]}}}},"summary":"Legt ein neues Kontenschema an","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/account-schedules/{id}":{"get":{"responses":{"200":{"description":"Kontenschema","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Kontenschemas"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem das Schema gehoert"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Name des Schemas, je Mandant eindeutig"},"description":{"type":"string","maxLength":500,"description":"Beschreibung; leerer String wenn keine erfasst"},"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":50,"description":"Kennung der Zeile, in Formeln referenzierbar"},"label":{"type":"string","maxLength":200,"description":"Beschriftung der Zeile im Bericht"},"accountFilter":{"type":"string","maxLength":500,"description":"Kontenfilter: einzelnes Konto (\"4400\"), Bereich (\"4000..4999\") oder mehrere durch | getrennt (\"4400|4300\"). Leer bei Ueberschriften."},"calculationType":{"type":"string","enum":["net_change","balance_at_date","formula","heading","total"],"description":"net_change = Bewegung im Zeitraum, balance_at_date = Saldo zum Stichtag, formula = Rechnung ueber Zeilen-Kennungen, heading = Ueberschrift ohne Wert, total = Summe ueber den Kontenfilter."},"bold":{"type":"boolean","description":"Zeile fett darstellen; fehlt bei den Vorlagenzeilen"},"indent":{"type":"integer","minimum":0,"maximum":5,"description":"Einrueckungstiefe 0-5"},"formula":{"type":"string","maxLength":500,"description":"Nur bei calculationType=formula: Rechnung ueber Zeilen-Kennungen, z. B. \"r1+r2-r3\""},"showOppositeSign":{"type":"boolean","description":"Vorzeichen des Werts umkehren"}},"required":["id","label","accountFilter","calculationType"],"description":"Eine Definitionszeile des Kontenschemas"},"description":"Die Definitionszeilen in Berichtsreihenfolge"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung"}},"required":["id","tenantId","name","description","rows","createdAt","updatedAt"],"description":"Ein Kontenschema — die Definition, nicht der gerechnete Bericht"},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","name":"string","description":"string","rows":[{"id":"string","label":"string","accountFilter":"string","calculationType":"net_change","bold":true,"indent":0,"formula":"string","showOppositeSign":true}],"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"account_schedule_not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Account-schedulesById","tags":["account-schedules"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liefert ein Kontenschema. Die Antwort ist das Schema selbst, ohne Umschlag.","summary":"Liefert ein Kontenschema","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Kontenschemas"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, dem das Schema gehoert"},"name":{"type":"string","minLength":1,"maxLength":100,"description":"Name des Schemas, je Mandant eindeutig"},"description":{"type":"string","maxLength":500,"description":"Beschreibung; leerer String wenn keine erfasst"},"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":50,"description":"Kennung der Zeile, in Formeln referenzierbar"},"label":{"type":"string","maxLength":200,"description":"Beschriftung der Zeile im Bericht"},"accountFilter":{"type":"string","maxLength":500,"description":"Kontenfilter: einzelnes Konto (\"4400\"), Bereich (\"4000..4999\") oder mehrere durch | getrennt (\"4400|4300\"). Leer bei Ueberschriften."},"calculationType":{"type":"string","enum":["net_change","balance_at_date","formula","heading","total"],"description":"net_change = Bewegung im Zeitraum, balance_at_date = Saldo zum Stichtag, formula = Rechnung ueber Zeilen-Kennungen, heading = Ueberschrift ohne Wert, total = Summe ueber den Kontenfilter."},"bold":{"type":"boolean","description":"Zeile fett darstellen; fehlt bei den Vorlagenzeilen"},"indent":{"type":"integer","minimum":0,"maximum":5,"description":"Einrueckungstiefe 0-5"},"formula":{"type":"string","maxLength":500,"description":"Nur bei calculationType=formula: Rechnung ueber Zeilen-Kennungen, z. B. \"r1+r2-r3\""},"showOppositeSign":{"type":"boolean","description":"Vorzeichen des Werts umkehren"}},"required":["id","label","accountFilter","calculationType"],"description":"Eine Definitionszeile des Kontenschemas"},"description":"Die Definitionszeilen in Berichtsreihenfolge"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","format":"date-time","description":"Letzte Aenderung"}},"required":["id","tenantId","name","description","rows","createdAt","updatedAt"],"description":"Ein Kontenschema — die Definition, nicht der gerechnete Bericht"},"example":{"id":"00000000-0000-4000-8000-000000000000","tenantId":"string","name":"string","description":"string","rows":[{"id":"string","label":"string","accountFilter":"string","calculationType":"net_change","bold":true,"indent":0,"formula":"string","showOppositeSign":true}],"createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"account_schedule_not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Nicht aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"putApiV1Account-schedulesById","tags":["account-schedules"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aktualisiert ein Kontenschema. Die Antwort ist das Schema selbst, ohne Umschlag. Ein leerer Rumpf ändert nichts und liefert den unveränderten Stand zurück; gibt es das Schema dabei nicht, antwortet der Endpunkt mit 503 statt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":500,"default":""},"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":50,"description":"Kennung der Zeile, in Formeln referenzierbar"},"label":{"type":"string","maxLength":200,"description":"Beschriftung der Zeile im Bericht"},"accountFilter":{"type":"string","maxLength":500,"default":"","description":"Kontenfilter: einzelnes Konto (\"4400\"), Bereich (\"4000..4999\") oder mehrere durch | getrennt (\"4400|4300\"). Leer bei Ueberschriften."},"calculationType":{"type":"string","enum":["net_change","balance_at_date","formula","heading","total"],"description":"net_change = Bewegung im Zeitraum, balance_at_date = Saldo zum Stichtag, formula = Rechnung ueber Zeilen-Kennungen, heading = Ueberschrift ohne Wert, total = Summe ueber den Kontenfilter."},"bold":{"type":"boolean","default":false,"description":"Zeile fett darstellen"},"indent":{"type":"integer","minimum":0,"maximum":5,"default":0,"description":"Einrueckungstiefe 0-5"},"formula":{"type":"string","maxLength":500,"description":"Nur bei calculationType=formula: Rechnung ueber Zeilen-Kennungen, z. B. \"r1+r2-r3\""},"showOppositeSign":{"type":"boolean","default":false,"description":"Vorzeichen des Werts umkehren"}},"required":["id","label","calculationType"]}}}},"example":{"name":"string","description":"string","rows":[{"id":"string","label":"string","accountFilter":"string","calculationType":"net_change","bold":true,"indent":0,"formula":"string","showOppositeSign":true}]}}}},"summary":"Aktualisiert ein Kontenschema","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","minLength":1,"description":"Ergebnis im Klartext"}},"required":["message"]},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden — oder bereits gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"account_schedule_not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Nicht gelöscht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"deleteApiV1Account-schedulesById","tags":["account-schedules"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Löscht ein Kontenschema (Soft-Delete, der Datensatz bleibt bestehen)","description":"Setzt `deleted_at` — das Schema verschwindet aus Liste, Detail und Bericht, die Zeile bleibt aber in der Tabelle. Ein Weg zurueck fuehrt nicht ueber diese Schnittstelle. Die Buchungen selbst sind NICHT betroffen: ein Kontenschema ist nur eine Auswertungsvorschrift, kein Datenbestand. Der Aufruf trifft ausschlieszlich Schemata des eigenen Mandanten; eine unbekannte oder bereits geloeschte Kennung ergibt 404 statt einer stillen Erfolgsmeldung. Die Antwort enthaelt nur eine Meldung, keinen Datensatz."}},"/api/v1/account-schedules/{id}/run":{"get":{"responses":{"200":{"description":"Bericht-Ergebnis mit gerechneten Zeilen","content":{"application/json":{"schema":{"type":"object","properties":{"scheduleId":{"type":"string","format":"uuid","description":"Kennung des ausgefuehrten Schemas"},"scheduleName":{"type":"string","minLength":1,"maxLength":100,"description":"Name des ausgefuehrten Schemas"},"periode":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}$","description":"Die angefragte Periode (YYYY-MM); null, wenn ueber vom/bis oder gar nicht angegeben"},"vom":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Erster Tag des ausgewerteten Zeitraums (YYYY-MM-DD)"},"bis":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Letzter Tag des ausgewerteten Zeitraums (YYYY-MM-DD)"},"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":50,"description":"Kennung der Zeile"},"label":{"type":"string","maxLength":200,"description":"Beschriftung der Zeile"},"calculationType":{"type":"string","minLength":1,"description":"net_change = Bewegung im Zeitraum, balance_at_date = Saldo zum Stichtag, formula = Rechnung ueber Zeilen-Kennungen, heading = Ueberschrift ohne Wert, total = Summe ueber den Kontenfilter."},"bold":{"type":"boolean","description":"Zeile fett darstellen"},"indent":{"type":"integer","minimum":0,"maximum":5,"description":"Einrueckungstiefe 0-5"},"value":{"type":["number","null"],"description":"Gerechneter Wert in EUR; null bei Ueberschriften und bei Formeln ohne Ausdruck"},"formattedValue":{"type":["string","null"],"description":"Derselbe Wert als de-DE-Waehrungstext; null wenn kein Wert vorliegt"}},"required":["id","label","calculationType","bold","indent","value","formattedValue"]},"description":"Die gerechneten Zeilen in Berichtsreihenfolge"},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Berechnung"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, fuer den gerechnet wurde"}},"required":["tenantId"],"description":"Angaben zur Abfrage"}},"required":["scheduleId","scheduleName","periode","vom","bis","rows","generatedAt","meta"]},"example":{"scheduleId":"00000000-0000-4000-8000-000000000000","scheduleName":"string","periode":null,"vom":"2026-01-01","bis":"2026-01-01","rows":[{"id":"string","label":"string","calculationType":"string","bold":true,"indent":0,"value":0,"formattedValue":"string"}],"generatedAt":"2026-01-01T12:00:00.000Z","meta":{"tenantId":"string"}}}}},"400":{"description":"Ungültige Periode","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_periode","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext mit dem erwarteten Format"}},"required":["error","message"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Schema nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"account_schedule_not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Bericht nicht gerechnet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Account-schedulesByIdRun","tags":["account-schedules"],"parameters":[{"in":"query","name":"periode","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$"}},{"in":"query","name":"vom","schema":{"type":"string","format":"date"}},{"in":"query","name":"bis","schema":{"type":"string","format":"date"}},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Führt ein Kontenschema aus und liefert den Bericht für die angegebene Periode. Ohne `periode` und ohne `vom`/`bis` wird der laufende Monat gerechnet. Fehlt die Tabelle journal_entries oder sachkonten, zählt die betroffene Zeile 0 — der Bericht kommt trotzdem mit 200.","summary":"Führt ein Kontenschema aus und liefert den Bericht für die angegebene Periode","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/personal-beschaffung/stats":{"get":{"responses":{"200":{"description":"Recruiting-KPIs","content":{"application/json":{"schema":{"type":"object","properties":{"offeneStellen":{"type":"integer"},"veroeffentlichteStellen":{"type":"integer"},"neueBewerbungen":{"type":"integer"},"anstehendeInterviews":{"type":"integer"},"bewerbungenByStatus":{"type":"object","additionalProperties":{"type":"integer"}},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["offeneStellen","veroeffentlichteStellen","neueBewerbungen","anstehendeInterviews","bewerbungenByStatus","meta"],"additionalProperties":false},"example":{"offeneStellen":0,"veroeffentlichteStellen":0,"neueBewerbungen":0,"anstehendeInterviews":0,"bewerbungenByStatus":{"beispiel":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Personal-beschaffungStats","tags":["HR"],"parameters":[],"summary":"Get recruiting KPIs","description":"KPIs: offene Stellen, Bewerbungen pro Status, anstehende Interviews"}},"/api/v1/personal-beschaffung/stellen":{"get":{"responses":{"200":{"description":"Stellen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"titel":{"type":"string"},"abteilung":{"type":"string"},"beschreibung":{"type":["string","null"]},"anforderungen":{"type":["string","null"]},"beschaeftigungsart":{"type":"string","enum":["vollzeit","teilzeit","werkstudent","praktikum"]},"gehaltMin":{"type":["number","null"]},"gehaltMax":{"type":["number","null"]},"status":{"type":"string","enum":["draft","published","paused","filled","cancelled"]},"veroeffentlichtAm":{"type":["string","null"]},"deadline":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","titel","abteilung","beschreibung","anforderungen","beschaeftigungsart","gehaltMin","gehaltMax","status","veroeffentlichtAm","deadline","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","titel":"string","abteilung":"string","beschreibung":"string","anforderungen":"string","beschaeftigungsart":"vollzeit","gehaltMin":0,"gehaltMax":0,"status":"draft","veroeffentlichtAm":"string","deadline":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Personal-beschaffungStellen","tags":["HR"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"abteilung","schema":{"type":"string"}}],"summary":"List job postings","description":"Liste aller Stellenausschreibungen. Abgesagte Stellen (status=cancelled) sind enthalten — ohne Filter blendet die Route nichts aus."},"post":{"responses":{"201":{"description":"Stelle angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"titel":{"type":"string"},"abteilung":{"type":"string"},"beschreibung":{"type":["string","null"]},"anforderungen":{"type":["string","null"]},"beschaeftigungsart":{"type":"string","enum":["vollzeit","teilzeit","werkstudent","praktikum"]},"gehaltMin":{"type":["number","null"]},"gehaltMax":{"type":["number","null"]},"status":{"type":"string","enum":["draft","published","paused","filled","cancelled"]},"veroeffentlichtAm":{"type":["string","null"]},"deadline":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","titel","abteilung","beschreibung","anforderungen","beschaeftigungsart","gehaltMin","gehaltMax","status","veroeffentlichtAm","deadline","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","titel":"string","abteilung":"string","beschreibung":"string","anforderungen":"string","beschaeftigungsart":"vollzeit","gehaltMin":0,"gehaltMax":0,"status":"draft","veroeffentlichtAm":"string","deadline":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}}},"operationId":"postApiV1Personal-beschaffungStellen","tags":["HR"],"parameters":[],"summary":"Create job posting","description":"Neue Stellenausschreibung anlegen. Erfordert Rolle manager oder höher. Antwortet 201 mit der angelegten Stelle direkt, ohne data-Hülle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"titel":{"type":"string","minLength":1,"maxLength":200},"abteilung":{"type":"string","minLength":1,"maxLength":100},"beschreibung":{"type":["string","null"]},"anforderungen":{"type":["string","null"]},"beschaeftigungsart":{"type":"string","enum":["vollzeit","teilzeit","werkstudent","praktikum"]},"gehaltMin":{"type":["number","null"],"minimum":0},"gehaltMax":{"type":["number","null"],"minimum":0},"status":{"type":"string","enum":["draft","published","paused","filled","cancelled"],"default":"draft"},"veroeffentlichtAm":{"type":["string","null"],"format":"date"},"deadline":{"type":["string","null"],"format":"date"}},"required":["titel","abteilung","beschaeftigungsart"]},"example":{"titel":"string","abteilung":"string","beschreibung":"string","anforderungen":"string","beschaeftigungsart":"vollzeit","gehaltMin":0,"gehaltMax":0,"status":"draft","veroeffentlichtAm":"2026-01-01","deadline":"2026-01-01"}}}}}},"/api/v1/personal-beschaffung/stellen/{id}":{"get":{"responses":{"200":{"description":"Stelle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"titel":{"type":"string"},"abteilung":{"type":"string"},"beschreibung":{"type":["string","null"]},"anforderungen":{"type":["string","null"]},"beschaeftigungsart":{"type":"string","enum":["vollzeit","teilzeit","werkstudent","praktikum"]},"gehaltMin":{"type":["number","null"]},"gehaltMax":{"type":["number","null"]},"status":{"type":"string","enum":["draft","published","paused","filled","cancelled"]},"veroeffentlichtAm":{"type":["string","null"]},"deadline":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","titel","abteilung","beschreibung","anforderungen","beschaeftigungsart","gehaltMin","gehaltMax","status","veroeffentlichtAm","deadline","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","titel":"string","abteilung":"string","beschreibung":"string","anforderungen":"string","beschaeftigungsart":"vollzeit","gehaltMin":0,"gehaltMax":0,"status":"draft","veroeffentlichtAm":"string","deadline":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"stelle_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1Personal-beschaffungStellenById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get job posting","description":"Einzelne Stelle"},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"titel":{"type":"string"},"abteilung":{"type":"string"},"beschreibung":{"type":["string","null"]},"anforderungen":{"type":["string","null"]},"beschaeftigungsart":{"type":"string","enum":["vollzeit","teilzeit","werkstudent","praktikum"]},"gehaltMin":{"type":["number","null"]},"gehaltMax":{"type":["number","null"]},"status":{"type":"string","enum":["draft","published","paused","filled","cancelled"]},"veroeffentlichtAm":{"type":["string","null"]},"deadline":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","titel","abteilung","beschreibung","anforderungen","beschaeftigungsart","gehaltMin","gehaltMax","status","veroeffentlichtAm","deadline","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","titel":"string","abteilung":"string","beschreibung":"string","anforderungen":"string","beschaeftigungsart":"vollzeit","gehaltMin":0,"gehaltMax":0,"status":"draft","veroeffentlichtAm":"string","deadline":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"stelle_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1Personal-beschaffungStellenById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update job posting","description":"Stelle aktualisieren. Nicht gesendete Felder behalten ihren bisherigen Wert — trotz PUT ein Teil-Update. Erfordert Rolle manager oder höher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"titel":{"type":"string","minLength":1,"maxLength":200},"abteilung":{"type":"string","minLength":1,"maxLength":100},"beschreibung":{"type":["string","null"]},"anforderungen":{"type":["string","null"]},"beschaeftigungsart":{"type":"string","enum":["vollzeit","teilzeit","werkstudent","praktikum"]},"gehaltMin":{"type":["number","null"],"minimum":0},"gehaltMax":{"type":["number","null"],"minimum":0},"status":{"type":"string","enum":["draft","published","paused","filled","cancelled"],"default":"draft"},"veroeffentlichtAm":{"type":["string","null"],"format":"date"},"deadline":{"type":["string","null"],"format":"date"}}},"example":{"titel":"string","abteilung":"string","beschreibung":"string","anforderungen":"string","beschaeftigungsart":"vollzeit","gehaltMin":0,"gehaltMax":0,"status":"draft","veroeffentlichtAm":"2026-01-01","deadline":"2026-01-01"}}}}},"delete":{"responses":{"200":{"description":"Deaktiviert","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"stelle_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1Personal-beschaffungStellenById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Cancel job posting (soft delete)","description":"Setzt status=cancelled. Der Datensatz bleibt bestehen und erscheint weiter in GET /stellen; eine Route zum endgültigen Löschen gibt es nicht. Erfordert Rolle manager oder höher."}},"/api/v1/personal-beschaffung/bewerbungen":{"get":{"responses":{"200":{"description":"Bewerbungen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"stelleId":{"type":"string","format":"uuid"},"vorname":{"type":"string"},"nachname":{"type":"string"},"email":{"type":"string"},"telefon":{"type":["string","null"]},"lebenslaufUrl":{"type":["string","null"]},"anschreiben":{"type":["string","null"]},"status":{"type":"string","enum":["new","screening","interview","offer","hired","rejected","withdrawn"]},"bewertungScore":{"type":["number","null"]},"notizen":{"type":["string","null"]},"eingegangen_am":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","stelleId","vorname","nachname","email","telefon","lebenslaufUrl","anschreiben","status","bewertungScore","notizen","eingegangen_am","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","stelleId":"00000000-0000-4000-8000-000000000000","vorname":"string","nachname":"string","email":"string","telefon":"string","lebenslaufUrl":"string","anschreiben":"string","status":"new","bewertungScore":0,"notizen":"string","eingegangen_am":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Personal-beschaffungBewerbungen","tags":["HR"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"stelleId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"status","schema":{"type":"string"}}],"summary":"List applications","description":"Liste Bewerbungen. Enthält Bewerberdaten (Name, E-Mail, Telefon, Lebenslauf-Link, Anschreiben) unmaskiert; zum Lesen genügt eine Anmeldung, eine Rolle wird nicht verlangt. Ein Schlüssel fällt aus der Reihe: eingegangen_am kommt in snake_case."},"post":{"responses":{"201":{"description":"Bewerbung angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"stelleId":{"type":"string","format":"uuid"},"vorname":{"type":"string"},"nachname":{"type":"string"},"email":{"type":"string"},"telefon":{"type":["string","null"]},"lebenslaufUrl":{"type":["string","null"]},"anschreiben":{"type":["string","null"]},"status":{"type":"string","enum":["new","screening","interview","offer","hired","rejected","withdrawn"]},"bewertungScore":{"type":["number","null"]},"notizen":{"type":["string","null"]},"eingegangen_am":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","stelleId","vorname","nachname","email","telefon","lebenslaufUrl","anschreiben","status","bewertungScore","notizen","eingegangen_am","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","stelleId":"00000000-0000-4000-8000-000000000000","vorname":"string","nachname":"string","email":"string","telefon":"string","lebenslaufUrl":"string","anschreiben":"string","status":"new","bewertungScore":0,"notizen":"string","eingegangen_am":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}}},"operationId":"postApiV1Personal-beschaffungBewerbungen","tags":["HR"],"parameters":[],"summary":"Create application","description":"Neue Bewerbung anlegen. Erfordert Rolle manager oder höher. Antwortet 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"stelleId":{"type":"string","format":"uuid"},"vorname":{"type":"string","minLength":1,"maxLength":100},"nachname":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email","maxLength":200},"telefon":{"type":["string","null"],"maxLength":50},"lebenslaufUrl":{"type":["string","null"],"format":"uri"},"anschreiben":{"type":["string","null"]},"status":{"type":"string","enum":["new","screening","interview","offer","hired","rejected","withdrawn"],"default":"new"},"bewertungScore":{"type":["number","null"],"minimum":0,"maximum":100},"notizen":{"type":["string","null"]}},"required":["stelleId","vorname","nachname","email"]},"example":{"stelleId":"00000000-0000-4000-8000-000000000000","vorname":"string","nachname":"string","email":"beispiel@example.com","telefon":"string","lebenslaufUrl":"https://example.com","anschreiben":"string","status":"new","bewertungScore":0,"notizen":"string"}}}}}},"/api/v1/personal-beschaffung/bewerbungen/{id}":{"get":{"responses":{"200":{"description":"Bewerbung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"stelleId":{"type":"string","format":"uuid"},"vorname":{"type":"string"},"nachname":{"type":"string"},"email":{"type":"string"},"telefon":{"type":["string","null"]},"lebenslaufUrl":{"type":["string","null"]},"anschreiben":{"type":["string","null"]},"status":{"type":"string","enum":["new","screening","interview","offer","hired","rejected","withdrawn"]},"bewertungScore":{"type":["number","null"]},"notizen":{"type":["string","null"]},"eingegangen_am":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","stelleId","vorname","nachname","email","telefon","lebenslaufUrl","anschreiben","status","bewertungScore","notizen","eingegangen_am","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","stelleId":"00000000-0000-4000-8000-000000000000","vorname":"string","nachname":"string","email":"string","telefon":"string","lebenslaufUrl":"string","anschreiben":"string","status":"new","bewertungScore":0,"notizen":"string","eingegangen_am":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"bewerbung_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1Personal-beschaffungBewerbungenById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get application","description":"Einzelne Bewerbung. Bewerberdaten kommen unmaskiert; zum Lesen genügt eine Anmeldung, eine Rolle wird nicht verlangt."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"stelleId":{"type":"string","format":"uuid"},"vorname":{"type":"string"},"nachname":{"type":"string"},"email":{"type":"string"},"telefon":{"type":["string","null"]},"lebenslaufUrl":{"type":["string","null"]},"anschreiben":{"type":["string","null"]},"status":{"type":"string","enum":["new","screening","interview","offer","hired","rejected","withdrawn"]},"bewertungScore":{"type":["number","null"]},"notizen":{"type":["string","null"]},"eingegangen_am":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","stelleId","vorname","nachname","email","telefon","lebenslaufUrl","anschreiben","status","bewertungScore","notizen","eingegangen_am","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","stelleId":"00000000-0000-4000-8000-000000000000","vorname":"string","nachname":"string","email":"string","telefon":"string","lebenslaufUrl":"string","anschreiben":"string","status":"new","bewertungScore":0,"notizen":"string","eingegangen_am":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"bewerbung_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1Personal-beschaffungBewerbungenById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update application","description":"Bewerbung aktualisieren. Nicht gesendete Felder behalten ihren bisherigen Wert — trotz PUT ein Teil-Update. Erfordert Rolle manager oder höher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"stelleId":{"type":"string","format":"uuid"},"vorname":{"type":"string","minLength":1,"maxLength":100},"nachname":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email","maxLength":200},"telefon":{"type":["string","null"],"maxLength":50},"lebenslaufUrl":{"type":["string","null"],"format":"uri"},"anschreiben":{"type":["string","null"]},"status":{"type":"string","enum":["new","screening","interview","offer","hired","rejected","withdrawn"],"default":"new"},"bewertungScore":{"type":["number","null"],"minimum":0,"maximum":100},"notizen":{"type":["string","null"]}}},"example":{"stelleId":"00000000-0000-4000-8000-000000000000","vorname":"string","nachname":"string","email":"beispiel@example.com","telefon":"string","lebenslaufUrl":"https://example.com","anschreiben":"string","status":"new","bewertungScore":0,"notizen":"string"}}}}}},"/api/v1/personal-beschaffung/interviews":{"get":{"responses":{"200":{"description":"Interviews","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bewerbungId":{"type":"string","format":"uuid"},"datum":{"type":"string"},"dauerMinuten":{"type":"number"},"typ":{"type":"string","enum":["telefon","video","onsite"]},"interviewerIds":{"type":"array","items":{"type":"string"}},"ergebnis":{"type":"string","enum":["pending","positive","negative","hold"]},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","bewerbungId","datum","dauerMinuten","typ","interviewerIds","ergebnis","notizen","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","bewerbungId":"00000000-0000-4000-8000-000000000000","datum":"string","dauerMinuten":0,"typ":"telefon","interviewerIds":["string"],"ergebnis":"pending","notizen":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Personal-beschaffungInterviews","tags":["HR"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"bewerbungId","schema":{"type":"string","format":"uuid"}}],"summary":"List interviews","description":"Liste Interviews, optional gefiltert nach bewerbungId"},"post":{"responses":{"201":{"description":"Interview angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bewerbungId":{"type":"string","format":"uuid"},"datum":{"type":"string"},"dauerMinuten":{"type":"number"},"typ":{"type":"string","enum":["telefon","video","onsite"]},"interviewerIds":{"type":"array","items":{"type":"string"}},"ergebnis":{"type":"string","enum":["pending","positive","negative","hold"]},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","bewerbungId","datum","dauerMinuten","typ","interviewerIds","ergebnis","notizen","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","bewerbungId":"00000000-0000-4000-8000-000000000000","datum":"string","dauerMinuten":0,"typ":"telefon","interviewerIds":["string"],"ergebnis":"pending","notizen":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}}},"operationId":"postApiV1Personal-beschaffungInterviews","tags":["HR"],"parameters":[],"summary":"Create interview","description":"Neues Interview anlegen. Erfordert Rolle manager oder höher. Antwortet 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bewerbungId":{"type":"string","format":"uuid"},"datum":{"type":"string","format":"date-time"},"dauerMinuten":{"type":"number","minimum":1,"default":60},"typ":{"type":"string","enum":["telefon","video","onsite"]},"interviewerIds":{"type":"array","items":{"type":"string","format":"uuid"},"default":[]},"ergebnis":{"type":"string","enum":["pending","positive","negative","hold"],"default":"pending"},"notizen":{"type":["string","null"]}},"required":["bewerbungId","datum","typ"]},"example":{"bewerbungId":"00000000-0000-4000-8000-000000000000","datum":"2026-01-01T12:00:00.000Z","dauerMinuten":1,"typ":"telefon","interviewerIds":["00000000-0000-4000-8000-000000000000"],"ergebnis":"pending","notizen":"string"}}}}}},"/api/v1/personal-beschaffung/interviews/{id}":{"get":{"responses":{"200":{"description":"Interview","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bewerbungId":{"type":"string","format":"uuid"},"datum":{"type":"string"},"dauerMinuten":{"type":"number"},"typ":{"type":"string","enum":["telefon","video","onsite"]},"interviewerIds":{"type":"array","items":{"type":"string"}},"ergebnis":{"type":"string","enum":["pending","positive","negative","hold"]},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","bewerbungId","datum","dauerMinuten","typ","interviewerIds","ergebnis","notizen","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","bewerbungId":"00000000-0000-4000-8000-000000000000","datum":"string","dauerMinuten":0,"typ":"telefon","interviewerIds":["string"],"ergebnis":"pending","notizen":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"interview_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1Personal-beschaffungInterviewsById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get interview","description":"Einzelnes Interview"},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bewerbungId":{"type":"string","format":"uuid"},"datum":{"type":"string"},"dauerMinuten":{"type":"number"},"typ":{"type":"string","enum":["telefon","video","onsite"]},"interviewerIds":{"type":"array","items":{"type":"string"}},"ergebnis":{"type":"string","enum":["pending","positive","negative","hold"]},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","bewerbungId","datum","dauerMinuten","typ","interviewerIds","ergebnis","notizen","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","bewerbungId":"00000000-0000-4000-8000-000000000000","datum":"string","dauerMinuten":0,"typ":"telefon","interviewerIds":["string"],"ergebnis":"pending","notizen":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"interview_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1Personal-beschaffungInterviewsById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update interview","description":"Interview aktualisieren. Nicht gesendete Felder behalten ihren bisherigen Wert; interviewerIds wird dagegen als Ganzes ersetzt, nicht ergänzt. Erfordert Rolle manager oder höher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"bewerbungId":{"type":"string","format":"uuid"},"datum":{"type":"string","format":"date-time"},"dauerMinuten":{"type":"number","minimum":1,"default":60},"typ":{"type":"string","enum":["telefon","video","onsite"]},"interviewerIds":{"type":"array","items":{"type":"string","format":"uuid"},"default":[]},"ergebnis":{"type":"string","enum":["pending","positive","negative","hold"],"default":"pending"},"notizen":{"type":["string","null"]}}},"example":{"bewerbungId":"00000000-0000-4000-8000-000000000000","datum":"2026-01-01T12:00:00.000Z","dauerMinuten":1,"typ":"telefon","interviewerIds":["00000000-0000-4000-8000-000000000000"],"ergebnis":"pending","notizen":"string"}}}}}},"/api/v1/personal-beschaffung/onboarding":{"get":{"responses":{"200":{"description":"Checklisten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"templateName":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"done":{"type":"boolean"}},"required":["label","done"]}},"status":{"type":"string","enum":["open","in_progress","completed"]},"startedAt":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","templateName","items","status","startedAt","completedAt","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","templateName":"string","items":[{"label":"string","done":true}],"status":"open","startedAt":"string","completedAt":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Personal-beschaffungOnboarding","tags":["HR"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"mitarbeiterId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"status","schema":{"type":"string"}}],"summary":"List onboarding checklists","description":"Liste Onboarding-Checklisten, optional gefiltert nach mitarbeiterId und status"},"post":{"responses":{"201":{"description":"Checkliste angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"templateName":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"done":{"type":"boolean"}},"required":["label","done"]}},"status":{"type":"string","enum":["open","in_progress","completed"]},"startedAt":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","templateName","items","status","startedAt","completedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","templateName":"string","items":[{"label":"string","done":true}],"status":"open","startedAt":"string","completedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}}},"operationId":"postApiV1Personal-beschaffungOnboarding","tags":["HR"],"parameters":[],"summary":"Create onboarding checklist","description":"Neue Onboarding-Checkliste anlegen. Erfordert Rolle manager oder höher. Antwortet 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mitarbeiterId":{"type":"string","format":"uuid"},"templateName":{"type":"string","minLength":1,"maxLength":200},"items":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"done":{"type":"boolean","default":false}},"required":["label"]},"default":[]},"status":{"type":"string","enum":["open","in_progress","completed"],"default":"open"},"startedAt":{"type":["string","null"],"format":"date-time"},"completedAt":{"type":["string","null"],"format":"date-time"}},"required":["mitarbeiterId","templateName"]},"example":{"mitarbeiterId":"00000000-0000-4000-8000-000000000000","templateName":"string","items":[{"label":"string","done":true}],"status":"open","startedAt":"2026-01-01T12:00:00.000Z","completedAt":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/personal-beschaffung/onboarding/{id}":{"get":{"responses":{"200":{"description":"Checkliste","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"templateName":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"done":{"type":"boolean"}},"required":["label","done"]}},"status":{"type":"string","enum":["open","in_progress","completed"]},"startedAt":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","templateName","items","status","startedAt","completedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","templateName":"string","items":[{"label":"string","done":true}],"status":"open","startedAt":"string","completedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden"}},"operationId":"getApiV1Personal-beschaffungOnboardingById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get onboarding checklist","description":"Einzelne Onboarding-Checkliste. Der 404-Körper trägt den Code checkliste_not_found."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"templateName":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"done":{"type":"boolean"}},"required":["label","done"]}},"status":{"type":"string","enum":["open","in_progress","completed"]},"startedAt":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","templateName","items","status","startedAt","completedAt","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","templateName":"string","items":[{"label":"string","done":true}],"status":"open","startedAt":"string","completedAt":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Nicht gefunden"}},"operationId":"putApiV1Personal-beschaffungOnboardingById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update onboarding checklist","description":"Onboarding-Checkliste aktualisieren. items wird als Ganzes ersetzt, nicht zusammengeführt — wer einen Haken setzt, schickt die vollständige Liste. Erfordert Rolle manager oder höher; der 404-Körper trägt den Code checkliste_not_found.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mitarbeiterId":{"type":"string","format":"uuid"},"templateName":{"type":"string","minLength":1,"maxLength":200},"items":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"done":{"type":"boolean","default":false}},"required":["label"]},"default":[]},"status":{"type":"string","enum":["open","in_progress","completed"],"default":"open"},"startedAt":{"type":["string","null"],"format":"date-time"},"completedAt":{"type":["string","null"],"format":"date-time"}}},"example":{"mitarbeiterId":"00000000-0000-4000-8000-000000000000","templateName":"string","items":[{"label":"string","done":true}],"status":"open","startedAt":"2026-01-01T12:00:00.000Z","completedAt":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/personal-entwicklung/stats":{"get":{"responses":{"200":{"description":"Entwicklungs-KPIs","content":{"application/json":{"schema":{"type":"object","properties":{"schulungenGesamt":{"type":"integer"},"schulungenLaufend":{"type":"integer"},"schulungenAbgeschlossen":{"type":"integer"},"skillsWithGap":{"type":"integer"},"geplaenteGespraeche":{"type":"integer"},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["schulungenGesamt","schulungenLaufend","schulungenAbgeschlossen","skillsWithGap","geplaenteGespraeche","meta"],"additionalProperties":false},"example":{"schulungenGesamt":0,"schulungenLaufend":0,"schulungenAbgeschlossen":0,"skillsWithGap":0,"geplaenteGespraeche":0,"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1Personal-entwicklungStats","tags":["HR"],"parameters":[],"summary":"Get HR development KPIs","description":"KPIs: Schulungen gesamt/laufend/abgeschlossen, Skills ausstehend, Gespräche geplant. Zwei Antwort-Schlüssel fallen aus der Reihe und sind so gewollt: skillsWithGap (englisch) und geplaenteGespraeche (Dreher)."}},"/api/v1/personal-entwicklung/schulungen":{"get":{"responses":{"200":{"description":"Schulungen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"titel":{"type":"string"},"anbieter":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"datumVon":{"type":"string"},"datumBis":{"type":["string","null"]},"status":{"type":"string","enum":["geplant","laufend","abgeschlossen","abgebrochen"]},"kosten":{"type":["number","null"]},"zertifikatUrl":{"type":["string","null"]},"bewertung":{"type":["number","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","titel","anbieter","kategorie","datumVon","datumBis","status","kosten","zertifikatUrl","bewertung","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","titel":"string","anbieter":"string","kategorie":"string","datumVon":"string","datumBis":"string","status":"geplant","kosten":0,"zertifikatUrl":"string","bewertung":0,"createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Personal-entwicklungSchulungen","tags":["HR"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"mitarbeiterId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"kategorie","schema":{"type":"string"}}],"summary":"List trainings","description":"Liste Schulungen, optional gefiltert nach mitarbeiterId, status und kategorie. Abgebrochene Schulungen sind enthalten — die Löschroute setzt nur den Status."},"post":{"responses":{"201":{"description":"Schulung angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"titel":{"type":"string"},"anbieter":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"datumVon":{"type":"string"},"datumBis":{"type":["string","null"]},"status":{"type":"string","enum":["geplant","laufend","abgeschlossen","abgebrochen"]},"kosten":{"type":["number","null"]},"zertifikatUrl":{"type":["string","null"]},"bewertung":{"type":["number","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","titel","anbieter","kategorie","datumVon","datumBis","status","kosten","zertifikatUrl","bewertung","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","titel":"string","anbieter":"string","kategorie":"string","datumVon":"string","datumBis":"string","status":"geplant","kosten":0,"zertifikatUrl":"string","bewertung":0,"createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}}},"operationId":"postApiV1Personal-entwicklungSchulungen","tags":["HR"],"parameters":[],"summary":"Create training","description":"Neue Schulung anlegen. Erfordert Rolle manager oder höher. Antwortet 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mitarbeiterId":{"type":"string","format":"uuid"},"titel":{"type":"string","minLength":1,"maxLength":200},"anbieter":{"type":["string","null"],"maxLength":200},"kategorie":{"type":["string","null"],"maxLength":100},"datumVon":{"type":"string","format":"date"},"datumBis":{"type":["string","null"],"format":"date"},"status":{"type":"string","enum":["geplant","laufend","abgeschlossen","abgebrochen"],"default":"geplant"},"kosten":{"type":["number","null"],"minimum":0},"zertifikatUrl":{"type":["string","null"],"format":"uri"},"bewertung":{"type":["number","null"],"minimum":1,"maximum":5}},"required":["mitarbeiterId","titel","datumVon"]},"example":{"mitarbeiterId":"00000000-0000-4000-8000-000000000000","titel":"string","anbieter":"string","kategorie":"string","datumVon":"2026-01-01","datumBis":"2026-01-01","status":"geplant","kosten":0,"zertifikatUrl":"https://example.com","bewertung":1}}}}}},"/api/v1/personal-entwicklung/schulungen/{id}":{"get":{"responses":{"200":{"description":"Schulung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"titel":{"type":"string"},"anbieter":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"datumVon":{"type":"string"},"datumBis":{"type":["string","null"]},"status":{"type":"string","enum":["geplant","laufend","abgeschlossen","abgebrochen"]},"kosten":{"type":["number","null"]},"zertifikatUrl":{"type":["string","null"]},"bewertung":{"type":["number","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","titel","anbieter","kategorie","datumVon","datumBis","status","kosten","zertifikatUrl","bewertung","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","titel":"string","anbieter":"string","kategorie":"string","datumVon":"string","datumBis":"string","status":"geplant","kosten":0,"zertifikatUrl":"string","bewertung":0,"createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Keine gültige UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_id"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"schulung_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1Personal-entwicklungSchulungenById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get training","description":"Einzelne Schulung. Eine id, die keine UUID ist, antwortet 400 invalid_id."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"titel":{"type":"string"},"anbieter":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"datumVon":{"type":"string"},"datumBis":{"type":["string","null"]},"status":{"type":"string","enum":["geplant","laufend","abgeschlossen","abgebrochen"]},"kosten":{"type":["number","null"]},"zertifikatUrl":{"type":["string","null"]},"bewertung":{"type":["number","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","titel","anbieter","kategorie","datumVon","datumBis","status","kosten","zertifikatUrl","bewertung","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","titel":"string","anbieter":"string","kategorie":"string","datumVon":"string","datumBis":"string","status":"geplant","kosten":0,"zertifikatUrl":"string","bewertung":0,"createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Keine gültige UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_id"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"schulung_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1Personal-entwicklungSchulungenById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update training","description":"Schulung aktualisieren. Nicht gesendete Felder behalten ihren bisherigen Wert — trotz PUT ein Teil-Update. Erfordert Rolle manager oder höher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mitarbeiterId":{"type":"string","format":"uuid"},"titel":{"type":"string","minLength":1,"maxLength":200},"anbieter":{"type":["string","null"],"maxLength":200},"kategorie":{"type":["string","null"],"maxLength":100},"datumVon":{"type":"string","format":"date"},"datumBis":{"type":["string","null"],"format":"date"},"status":{"type":"string","enum":["geplant","laufend","abgeschlossen","abgebrochen"],"default":"geplant"},"kosten":{"type":["number","null"],"minimum":0},"zertifikatUrl":{"type":["string","null"],"format":"uri"},"bewertung":{"type":["number","null"],"minimum":1,"maximum":5}}},"example":{"mitarbeiterId":"00000000-0000-4000-8000-000000000000","titel":"string","anbieter":"string","kategorie":"string","datumVon":"2026-01-01","datumBis":"2026-01-01","status":"geplant","kosten":0,"zertifikatUrl":"https://example.com","bewertung":1}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Quittung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"400":{"description":"Keine gültige UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_id"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"schulung_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1Personal-entwicklungSchulungenById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Cancel training (soft delete)","description":"Setzt status=abgebrochen. Der Datensatz bleibt bestehen und erscheint weiter in GET /schulungen; eine Route zum endgültigen Löschen gibt es nicht. Erfordert Rolle manager oder höher."}},"/api/v1/personal-entwicklung/skill-matrix":{"get":{"responses":{"200":{"description":"Skills","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"skillName":{"type":"string"},"kategorie":{"type":["string","null"]},"level":{"type":"integer"},"zielLevel":{"type":["integer","null"]},"letzteBewertungAm":{"type":["string","null"]},"naechsteBewertungAm":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","skillName","kategorie","level","zielLevel","letzteBewertungAm","naechsteBewertungAm","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","skillName":"string","kategorie":"string","level":0,"zielLevel":0,"letzteBewertungAm":"string","naechsteBewertungAm":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Personal-entwicklungSkill-matrix","tags":["HR"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"mitarbeiterId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"kategorie","schema":{"type":"string"}}],"summary":"List skill matrix entries","description":"Skill-Matrix aller Mitarbeiter, optional gefiltert nach mitarbeiterId und kategorie. Der Parameter status wird angenommen und ignoriert — die Tabelle hat keine Statusspalte."},"post":{"responses":{"201":{"description":"Skill angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"skillName":{"type":"string"},"kategorie":{"type":["string","null"]},"level":{"type":"integer"},"zielLevel":{"type":["integer","null"]},"letzteBewertungAm":{"type":["string","null"]},"naechsteBewertungAm":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","skillName","kategorie","level","zielLevel","letzteBewertungAm","naechsteBewertungAm","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","skillName":"string","kategorie":"string","level":0,"zielLevel":0,"letzteBewertungAm":"string","naechsteBewertungAm":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}}},"operationId":"postApiV1Personal-entwicklungSkill-matrix","tags":["HR"],"parameters":[],"summary":"Create skill matrix entry","description":"Neuen Skill-Eintrag anlegen. Erfordert Rolle manager oder höher. Antwortet 201. Die Route prüft nicht auf Dubletten: derselbe Skill lässt sich für einen Mitarbeiter mehrfach anlegen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mitarbeiterId":{"type":"string","format":"uuid"},"skillName":{"type":"string","minLength":1,"maxLength":200},"kategorie":{"type":["string","null"],"maxLength":100},"level":{"type":"integer","minimum":1,"maximum":5},"zielLevel":{"type":["integer","null"],"minimum":1,"maximum":5},"letzteBewertungAm":{"type":["string","null"],"format":"date"},"naechsteBewertungAm":{"type":["string","null"],"format":"date"}},"required":["mitarbeiterId","skillName","level"]},"example":{"mitarbeiterId":"00000000-0000-4000-8000-000000000000","skillName":"string","kategorie":"string","level":1,"zielLevel":1,"letzteBewertungAm":"2026-01-01","naechsteBewertungAm":"2026-01-01"}}}}}},"/api/v1/personal-entwicklung/skill-matrix/{id}":{"get":{"responses":{"200":{"description":"Skill","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"skillName":{"type":"string"},"kategorie":{"type":["string","null"]},"level":{"type":"integer"},"zielLevel":{"type":["integer","null"]},"letzteBewertungAm":{"type":["string","null"]},"naechsteBewertungAm":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","skillName","kategorie","level","zielLevel","letzteBewertungAm","naechsteBewertungAm","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","skillName":"string","kategorie":"string","level":0,"zielLevel":0,"letzteBewertungAm":"string","naechsteBewertungAm":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Keine gültige UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_id"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"skill_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1Personal-entwicklungSkill-matrixById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get skill matrix entry","description":"Einzelner Skill-Eintrag. Eine id, die keine UUID ist, antwortet 400 invalid_id."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"skillName":{"type":"string"},"kategorie":{"type":["string","null"]},"level":{"type":"integer"},"zielLevel":{"type":["integer","null"]},"letzteBewertungAm":{"type":["string","null"]},"naechsteBewertungAm":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","skillName","kategorie","level","zielLevel","letzteBewertungAm","naechsteBewertungAm","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","skillName":"string","kategorie":"string","level":0,"zielLevel":0,"letzteBewertungAm":"string","naechsteBewertungAm":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Keine gültige UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_id"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"skill_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1Personal-entwicklungSkill-matrixById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update skill matrix entry","description":"Skill-Eintrag aktualisieren. Nicht gesendete Felder behalten ihren bisherigen Wert — trotz PUT ein Teil-Update. Erfordert Rolle manager oder höher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mitarbeiterId":{"type":"string","format":"uuid"},"skillName":{"type":"string","minLength":1,"maxLength":200},"kategorie":{"type":["string","null"],"maxLength":100},"level":{"type":"integer","minimum":1,"maximum":5},"zielLevel":{"type":["integer","null"],"minimum":1,"maximum":5},"letzteBewertungAm":{"type":["string","null"],"format":"date"},"naechsteBewertungAm":{"type":["string","null"],"format":"date"}}},"example":{"mitarbeiterId":"00000000-0000-4000-8000-000000000000","skillName":"string","kategorie":"string","level":1,"zielLevel":1,"letzteBewertungAm":"2026-01-01","naechsteBewertungAm":"2026-01-01"}}}}},"delete":{"responses":{"200":{"description":"Gelöscht — nur eine Quittung, kein Datensatz","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"example":{"message":"string"}}}},"400":{"description":"Keine gültige UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_id"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"skill_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"deleteApiV1Personal-entwicklungSkill-matrixById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Delete skill matrix entry","description":"Löscht die Zeile endgültig (DELETE, kein Soft-Delete) — anders als bei den Schulungen, wo nur der Status gesetzt wird. Nicht umkehrbar. Erfordert Rolle manager oder höher."}},"/api/v1/personal-entwicklung/gespraeche":{"get":{"responses":{"200":{"description":"Gespräche","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"vorgesetzterIds":{"type":"string","format":"uuid"},"typ":{"type":"string","enum":["jahresgespraech","zwischengespraech","probezeit","austritt"]},"datum":{"type":"string"},"themen":{"type":"array","items":{"type":"string"}},"vereinbarungen":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"faelligkeitAm":{"type":"string"}},"required":["text"]}},"status":{"type":"string","enum":["geplant","durchgefuehrt","abgesagt"]},"naechsterTermin":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","vorgesetzterIds","typ","datum","themen","vereinbarungen","status","naechsterTermin","createdAt","updatedAt"],"additionalProperties":false}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"required":["limit","offset","total"],"additionalProperties":false},"meta":{"type":"object","properties":{"tenantId":{"type":"string"},"source":{"type":"string","const":"db"}},"required":["tenantId","source"],"additionalProperties":false}},"required":["data","pagination","meta"],"additionalProperties":false},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","vorgesetzterIds":"00000000-0000-4000-8000-000000000000","typ":"jahresgespraech","datum":"string","themen":["string"],"vereinbarungen":[{"text":"string","faelligkeitAm":"string"}],"status":"geplant","naechsterTermin":"string","createdAt":"string","updatedAt":"string"}],"pagination":{"limit":0,"offset":0,"total":0},"meta":{"tenantId":"string","source":"db"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}}},"operationId":"getApiV1Personal-entwicklungGespraeche","tags":["HR"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"mitarbeiterId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"kategorie","schema":{"type":"string"}}],"summary":"List performance reviews","description":"Liste Mitarbeitergespräche mit Themen und Vereinbarungen. Personaldaten: schon das Lesen erfordert Rolle manager oder höher. Optional gefiltert nach mitarbeiterId und status; der Parameter kategorie wird angenommen und ignoriert."},"post":{"responses":{"201":{"description":"Gespräch angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"vorgesetzterIds":{"type":"string","format":"uuid"},"typ":{"type":"string","enum":["jahresgespraech","zwischengespraech","probezeit","austritt"]},"datum":{"type":"string"},"themen":{"type":"array","items":{"type":"string"}},"vereinbarungen":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"faelligkeitAm":{"type":"string"}},"required":["text"]}},"status":{"type":"string","enum":["geplant","durchgefuehrt","abgesagt"]},"naechsterTermin":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","vorgesetzterIds","typ","datum","themen","vereinbarungen","status","naechsterTermin","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","vorgesetzterIds":"00000000-0000-4000-8000-000000000000","typ":"jahresgespraech","datum":"string","themen":["string"],"vereinbarungen":[{"text":"string","faelligkeitAm":"string"}],"status":"geplant","naechsterTermin":"string","createdAt":"string","updatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}}},"operationId":"postApiV1Personal-entwicklungGespraeche","tags":["HR"],"parameters":[],"summary":"Create performance review","description":"Neues Mitarbeitergespräch anlegen. Erfordert Rolle manager oder höher. Antwortet 201. vorgesetzterIds ist trotz Plural genau EINE UUID.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mitarbeiterId":{"type":"string","format":"uuid"},"vorgesetzterIds":{"type":"string","format":"uuid"},"typ":{"type":"string","enum":["jahresgespraech","zwischengespraech","probezeit","austritt"]},"datum":{"type":"string","format":"date-time"},"themen":{"type":"array","items":{"type":"string"},"default":[]},"vereinbarungen":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"faelligkeitAm":{"type":"string","format":"date"}},"required":["text"]},"default":[]},"status":{"type":"string","enum":["geplant","durchgefuehrt","abgesagt"],"default":"geplant"},"naechsterTermin":{"type":["string","null"],"format":"date"}},"required":["mitarbeiterId","vorgesetzterIds","typ","datum"]},"example":{"mitarbeiterId":"00000000-0000-4000-8000-000000000000","vorgesetzterIds":"00000000-0000-4000-8000-000000000000","typ":"jahresgespraech","datum":"2026-01-01T12:00:00.000Z","themen":["string"],"vereinbarungen":[{"text":"string","faelligkeitAm":"2026-01-01"}],"status":"geplant","naechsterTermin":"2026-01-01"}}}}}},"/api/v1/personal-entwicklung/gespraeche/{id}":{"get":{"responses":{"200":{"description":"Gespräch","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"vorgesetzterIds":{"type":"string","format":"uuid"},"typ":{"type":"string","enum":["jahresgespraech","zwischengespraech","probezeit","austritt"]},"datum":{"type":"string"},"themen":{"type":"array","items":{"type":"string"}},"vereinbarungen":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"faelligkeitAm":{"type":"string"}},"required":["text"]}},"status":{"type":"string","enum":["geplant","durchgefuehrt","abgesagt"]},"naechsterTermin":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","vorgesetzterIds","typ","datum","themen","vereinbarungen","status","naechsterTermin","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","vorgesetzterIds":"00000000-0000-4000-8000-000000000000","typ":"jahresgespraech","datum":"string","themen":["string"],"vereinbarungen":[{"text":"string","faelligkeitAm":"string"}],"status":"geplant","naechsterTermin":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Keine gültige UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_id"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"gespraech_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1Personal-entwicklungGespraecheById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get performance review","description":"Einzelnes Mitarbeitergespräch. Personaldaten: schon das Lesen erfordert Rolle manager oder höher. Eine id, die keine UUID ist, antwortet 400 invalid_id."},"put":{"responses":{"200":{"description":"Aktualisiert","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mitarbeiterId":{"type":"string","format":"uuid"},"vorgesetzterIds":{"type":"string","format":"uuid"},"typ":{"type":"string","enum":["jahresgespraech","zwischengespraech","probezeit","austritt"]},"datum":{"type":"string"},"themen":{"type":"array","items":{"type":"string"}},"vereinbarungen":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"faelligkeitAm":{"type":"string"}},"required":["text"]}},"status":{"type":"string","enum":["geplant","durchgefuehrt","abgesagt"]},"naechsterTermin":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","mitarbeiterId","vorgesetzterIds","typ","datum","themen","vereinbarungen","status","naechsterTermin","createdAt","updatedAt"],"additionalProperties":false},"example":{"id":"00000000-0000-4000-8000-000000000000","mitarbeiterId":"00000000-0000-4000-8000-000000000000","vorgesetzterIds":"00000000-0000-4000-8000-000000000000","typ":"jahresgespraech","datum":"string","themen":["string"],"vereinbarungen":[{"text":"string","faelligkeitAm":"string"}],"status":"geplant","naechsterTermin":"string","createdAt":"string","updatedAt":"string"}}}},"400":{"description":"Keine gültige UUID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invalid_id"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"gespraech_not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"putApiV1Personal-entwicklungGespraecheById","tags":["HR"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update performance review","description":"Mitarbeitergespräch aktualisieren. Nicht gesendete Felder behalten ihren bisherigen Wert; themen und vereinbarungen werden dagegen als Ganzes ersetzt, nicht ergänzt. Erfordert Rolle manager oder höher.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mitarbeiterId":{"type":"string","format":"uuid"},"vorgesetzterIds":{"type":"string","format":"uuid"},"typ":{"type":"string","enum":["jahresgespraech","zwischengespraech","probezeit","austritt"]},"datum":{"type":"string","format":"date-time"},"themen":{"type":"array","items":{"type":"string"},"default":[]},"vereinbarungen":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"faelligkeitAm":{"type":"string","format":"date"}},"required":["text"]},"default":[]},"status":{"type":"string","enum":["geplant","durchgefuehrt","abgesagt"],"default":"geplant"},"naechsterTermin":{"type":["string","null"],"format":"date"}}},"example":{"mitarbeiterId":"00000000-0000-4000-8000-000000000000","vorgesetzterIds":"00000000-0000-4000-8000-000000000000","typ":"jahresgespraech","datum":"2026-01-01T12:00:00.000Z","themen":["string"],"vereinbarungen":[{"text":"string","faelligkeitAm":"2026-01-01"}],"status":"geplant","naechsterTermin":"2026-01-01"}}}}}},"/api/v1/ai/monatsabschluss/{periode}":{"post":{"responses":{"200":{"description":"Report generated. `pdfUrl` points at the download route; `kpis.marge` is a ratio, not a percentage.","content":{"application/json":{"schema":{"type":"object","properties":{"pdfUrl":{"type":"string"},"summary":{"type":"string"},"kpis":{"type":"object","properties":{"umsatz":{"type":"number"},"kosten":{"type":"number"},"ebit":{"type":"number"},"marge":{"type":"number"}},"required":["umsatz","kosten","ebit","marge"],"additionalProperties":false},"generatedAt":{"type":"string"}},"required":["pdfUrl","summary","kpis","generatedAt"],"additionalProperties":false},"example":{"pdfUrl":"string","summary":"string","kpis":{"umsatz":0,"kosten":0,"ebit":0,"marge":0},"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1AiMonatsabschlussByPeriode","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"periode","required":true}],"summary":"Generates the AI monthly closing report for the given period","description":"Generate AI monthly closing report (PDF + summary + KPIs) for the given YYYY-MM period. The period is validated twice: the format must be `YYYY-MM` and the month must be 01–12, otherwise 400. The optional body may carry `includeSections` (executive, guv, top10, liquiditaet, anomalien, steueroptimierung, susa) to narrow the report; a missing or unparsable body is ignored, not rejected, and produces the full report. The file is written to disk and fetched afterwards via `pdfUrl` — this response carries the numbers, not the document. A failing generation surfaces as 500 with the tool error message."}},"/api/v1/ai/monatsabschluss/download/{filename}":{"get":{"responses":{"200":{"description":"The file itself, streamed. `application/pdf` with a download disposition for a `.pdf` name, otherwise HTML shown inline.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"text/html":{"schema":{"type":"string"}}}},"400":{"description":"Ungueltiger Dateiname"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Datei gehoert zu einem anderen Mandanten"},"404":{"description":"Datei nicht gefunden"}},"operationId":"getApiV1AiMonatsabschlussDownloadByFilename","tags":["ai"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"filename","required":true}],"description":"Download a previously generated Monatsabschluss PDF or HTML file. The file is streamed from disk, so this endpoint answers only for reports generated on THIS instance — behind several replicas a link can 404 on the next request. Two guards apply: the filename may not contain a slash, a backslash or a leading dot, and it must start with the requesting tenant id followed by a hyphen (otherwise 403). The content type follows the extension: `.pdf` is sent as an attachment, everything else as inline HTML. Responses are marked `private, no-cache` and are never stored by a shared cache.","summary":"Download a previously generated Monatsabschluss PDF or HTML file","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/beta-crm/leads":{"post":{"responses":{"201":{"description":"Der angelegte Lead","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` im Erfolgsfall"},"lead":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Leads, von der Ablage vergeben"},"tenant_id":{"type":"string","minLength":1,"description":"Mandant, zu dem der Lead gehoert — fremde Mandanten sehen ihn nie"},"email":{"type":"string","format":"email","description":"E-Mail des Interessenten"},"company":{"type":"string","minLength":1,"description":"Firmenname des Interessenten"},"branche":{"type":"string","minLength":1,"description":"Branche des Interessenten; freier Text, keine feste Liste"},"plan_interest":{"type":"string","enum":["starter","pro","enterprise","unknown"],"description":"Angefragtes Paket; `unknown`, wenn nicht angegeben"},"signup_source":{"type":"string","enum":["hero","pricing","demo-page","blog","branchen","referral","other"],"description":"Stelle der Website, an der sich der Interessent gemeldet hat"},"status":{"type":"string","enum":["prospect","contacted","demo-booked","trial","converted","lost"],"description":"Stufe im Beta-Prozess. Wechsel laufen ueber PATCH und sind durch einen Automaten begrenzt"},"created_at":{"type":"string","minLength":1,"description":"Anlagezeitpunkt als Zeichenkette, wie die Ablage sie erzeugt"},"updated_at":{"type":"string","minLength":1,"description":"Zeitpunkt der letzten Aenderung, gleiche Form"},"notes":{"type":"string","description":"Freitext; FEHLT ganz, wenn beim Anlegen keiner mitgegeben wurde"}},"required":["id","tenant_id","email","company","branche","plan_interest","signup_source","status","created_at","updated_at"],"additionalProperties":false,"description":"Der betroffene Lead nach der Operation"}},"required":["ok","lead"],"additionalProperties":false},"example":{"ok":true,"lead":{"id":"string","tenant_id":"string","email":"beispiel@example.com","company":"string","branche":"string","plan_interest":"starter","signup_source":"hero","status":"prospect","created_at":"string","updated_at":"string","notes":"string"}}}}},"400":{"description":"Rumpf abgelehnt — die Beanstandungen stehen aufgeteilt unter `error`","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false` — die Anfrage wurde abgelehnt"},"error":{"type":"object","properties":{"formErrors":{"type":"array","items":{"type":"string"},"description":"Beanstandungen, die keinem einzelnen Feld zugeordnet sind"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Je Feldname die Liste seiner Beanstandungen, in englischer Zod-Formulierung"}},"required":["formErrors","fieldErrors"],"description":"Das aufgeteilte Zod-Ergebnis (`flatten()`)"}},"required":["ok","error"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"}},"operationId":"postApiV1Beta-crmLeads","tags":["beta-crm"],"parameters":[],"summary":"Beta-CRM-Lead anlegen","description":"Legt einen Beta-CRM-Lead fuer den Mandanten an. Der Status wird immer auf den Anfangswert gesetzt und laesst sich hier nicht mitgeben; dafuer gibt es PATCH /leads/{id}/status. Gleiche E-Mail zweimal ist erlaubt — es gibt keine Dubletten-Pruefung."},"get":{"responses":{"200":{"description":"Die passenden Leads","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` im Erfolgsfall"},"leads":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Leads, von der Ablage vergeben"},"tenant_id":{"type":"string","minLength":1,"description":"Mandant, zu dem der Lead gehoert — fremde Mandanten sehen ihn nie"},"email":{"type":"string","format":"email","description":"E-Mail des Interessenten"},"company":{"type":"string","minLength":1,"description":"Firmenname des Interessenten"},"branche":{"type":"string","minLength":1,"description":"Branche des Interessenten; freier Text, keine feste Liste"},"plan_interest":{"type":"string","enum":["starter","pro","enterprise","unknown"],"description":"Angefragtes Paket; `unknown`, wenn nicht angegeben"},"signup_source":{"type":"string","enum":["hero","pricing","demo-page","blog","branchen","referral","other"],"description":"Stelle der Website, an der sich der Interessent gemeldet hat"},"status":{"type":"string","enum":["prospect","contacted","demo-booked","trial","converted","lost"],"description":"Stufe im Beta-Prozess. Wechsel laufen ueber PATCH und sind durch einen Automaten begrenzt"},"created_at":{"type":"string","minLength":1,"description":"Anlagezeitpunkt als Zeichenkette, wie die Ablage sie erzeugt"},"updated_at":{"type":"string","minLength":1,"description":"Zeitpunkt der letzten Aenderung, gleiche Form"},"notes":{"type":"string","description":"Freitext; FEHLT ganz, wenn beim Anlegen keiner mitgegeben wurde"}},"required":["id","tenant_id","email","company","branche","plan_interest","signup_source","status","created_at","updated_at"],"additionalProperties":false},"description":"Die passenden Leads. KEINE Blaetterung — der Aufruf liefert immer alle Treffer"}},"required":["ok","leads"],"additionalProperties":false},"example":{"ok":true,"leads":[{"id":"string","tenant_id":"string","email":"beispiel@example.com","company":"string","branche":"string","plan_interest":"starter","signup_source":"hero","status":"prospect","created_at":"string","updated_at":"string","notes":"string"}]}}}},"400":{"description":"Der Wert von `status` gehoert nicht zur Aufzaehlung — `error` ist hier ein blosser Text","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false` — die Anfrage wurde abgelehnt"},"error":{"type":"string","minLength":1,"description":"Der Grund als Text, nicht als Objekt und ohne feste Kennung"}},"required":["ok","error"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"}},"operationId":"getApiV1Beta-crmLeads","tags":["beta-crm"],"parameters":[],"summary":"Beta-CRM-Leads auflisten","description":"Listet die Beta-CRM-Leads des Mandanten, wahlweise gefiltert nach `status`, `branche` sowie `fromDate`/`toDate`. Es gibt KEINE Blaetterung und kein Limit — der Aufruf liefert immer alle Treffer. Nur `status` wird geprueft; `branche` und die beiden Datumsangaben gehen ungeprueft an die Ablage."}},"/api/v1/beta-crm/leads/{id}/status":{"patch":{"responses":{"200":{"description":"Der Lead nach dem Wechsel","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` im Erfolgsfall"},"lead":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung des Leads, von der Ablage vergeben"},"tenant_id":{"type":"string","minLength":1,"description":"Mandant, zu dem der Lead gehoert — fremde Mandanten sehen ihn nie"},"email":{"type":"string","format":"email","description":"E-Mail des Interessenten"},"company":{"type":"string","minLength":1,"description":"Firmenname des Interessenten"},"branche":{"type":"string","minLength":1,"description":"Branche des Interessenten; freier Text, keine feste Liste"},"plan_interest":{"type":"string","enum":["starter","pro","enterprise","unknown"],"description":"Angefragtes Paket; `unknown`, wenn nicht angegeben"},"signup_source":{"type":"string","enum":["hero","pricing","demo-page","blog","branchen","referral","other"],"description":"Stelle der Website, an der sich der Interessent gemeldet hat"},"status":{"type":"string","enum":["prospect","contacted","demo-booked","trial","converted","lost"],"description":"Stufe im Beta-Prozess. Wechsel laufen ueber PATCH und sind durch einen Automaten begrenzt"},"created_at":{"type":"string","minLength":1,"description":"Anlagezeitpunkt als Zeichenkette, wie die Ablage sie erzeugt"},"updated_at":{"type":"string","minLength":1,"description":"Zeitpunkt der letzten Aenderung, gleiche Form"},"notes":{"type":"string","description":"Freitext; FEHLT ganz, wenn beim Anlegen keiner mitgegeben wurde"}},"required":["id","tenant_id","email","company","branche","plan_interest","signup_source","status","created_at","updated_at"],"additionalProperties":false,"description":"Der betroffene Lead nach der Operation"}},"required":["ok","lead"],"additionalProperties":false},"example":{"ok":true,"lead":{"id":"string","tenant_id":"string","email":"beispiel@example.com","company":"string","branche":"string","plan_interest":"starter","signup_source":"hero","status":"prospect","created_at":"string","updated_at":"string","notes":"string"}}}}},"400":{"description":"Rumpf abgelehnt — der Wert von `status` gehoert nicht zur Aufzaehlung","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false` — die Anfrage wurde abgelehnt"},"error":{"type":"object","properties":{"formErrors":{"type":"array","items":{"type":"string"},"description":"Beanstandungen, die keinem einzelnen Feld zugeordnet sind"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Je Feldname die Liste seiner Beanstandungen, in englischer Zod-Formulierung"}},"required":["formErrors","fieldErrors"],"description":"Das aufgeteilte Zod-Ergebnis (`flatten()`)"}},"required":["ok","error"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Kein Lead mit dieser Kennung IN DIESEM Mandanten — ein fremder sieht hier aus wie keiner","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false` — die Anfrage wurde abgelehnt"},"error":{"type":"string","minLength":1,"description":"Der Grund als Text, nicht als Objekt und ohne feste Kennung"}},"required":["ok","error"],"additionalProperties":false}}}},"409":{"description":"Der Uebergang ist von dieser Stufe aus nicht erlaubt; `error` nennt ihn im Klartext","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false` — die Anfrage wurde abgelehnt"},"error":{"type":"string","minLength":1,"description":"Der Grund als Text, nicht als Objekt und ohne feste Kennung"}},"required":["ok","error"],"additionalProperties":false}}}}},"operationId":"patchApiV1Beta-crmLeadsByIdStatus","tags":["beta-crm"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lead-Status wechseln","description":"Wechselt die Stufe eines Leads. Ein Automat laesst nur erlaubte Uebergaenge zu; ein unerlaubter wird mit 409 abgelehnt, nicht mit 400. Nach einem erfolgreichen Wechsel verschickt der Server die zugehoerigen Benachrichtigungen — scheitert der Versand, faellt das hier nicht auf."}},"/api/v1/beta-crm/leads/{id}/activities":{"post":{"responses":{"201":{"description":"Die angelegte Aktivitaet","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Immer `true` im Erfolgsfall"},"activity":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Kennung der Aktivitaet, von der Ablage vergeben"},"lead_id":{"type":"string","minLength":1,"description":"Lead, an dem die Aktivitaet haengt"},"tenant_id":{"type":"string","minLength":1,"description":"Mandant, zu dem die Aktivitaet gehoert"},"type":{"type":"string","enum":["note","email-sent","call","demo-scheduled","trial-started","status-change","lost-reason"],"description":"Art der Aktivitaet — Notiz, Anruf, verschickte Mail, Demo-Termin und so fort"},"note":{"type":"string","description":"Der Text der Aktivitaet"},"timestamp":{"type":"string","minLength":1,"description":"Zeitpunkt als Zeichenkette, wie die Ablage sie erzeugt"},"by_user":{"type":"string","minLength":1,"description":"Wer die Aktivitaet erfasst hat — aus dem Rumpf uebernommen, nicht geprueft"}},"required":["id","lead_id","tenant_id","type","note","timestamp","by_user"],"additionalProperties":false,"description":"Die angelegte Aktivitaet"}},"required":["ok","activity"],"additionalProperties":false},"example":{"ok":true,"activity":{"id":"string","lead_id":"string","tenant_id":"string","type":"note","note":"string","timestamp":"string","by_user":"string"}}}}},"400":{"description":"Rumpf abgelehnt — die Beanstandungen stehen aufgeteilt unter `error`","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false` — die Anfrage wurde abgelehnt"},"error":{"type":"object","properties":{"formErrors":{"type":"array","items":{"type":"string"},"description":"Beanstandungen, die keinem einzelnen Feld zugeordnet sind"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Je Feldname die Liste seiner Beanstandungen, in englischer Zod-Formulierung"}},"required":["formErrors","fieldErrors"],"description":"Das aufgeteilte Zod-Ergebnis (`flatten()`)"}},"required":["ok","error"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext — Klartext, kein JSON"},"404":{"description":"Kein Lead mit dieser Kennung IN DIESEM Mandanten — ein fremder sieht hier aus wie keiner","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false,"description":"Immer `false` — die Anfrage wurde abgelehnt"},"error":{"type":"string","minLength":1,"description":"Der Grund als Text, nicht als Objekt und ohne feste Kennung"}},"required":["ok","error"],"additionalProperties":false}}}}},"operationId":"postApiV1Beta-crmLeadsByIdActivities","tags":["beta-crm"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aktivität zu einem Lead erfassen","description":"Haengt eine Aktivitaet an einen Lead — Notiz, Anruf, verschickte Mail, Demo-Termin und so fort. `by_user` kommt aus dem Rumpf und wird NICHT gegen den angemeldeten Benutzer geprueft: als Urheber steht dort, was der Aufrufer hineinschreibt. Der Statuswechsel eines Leads erzeugt KEINE Aktivitaet von selbst — die Art `status-change` muss von Hand erfasst werden."}},"/api/v1/reports/reports":{"post":{"responses":{"201":{"description":"Stored. Only an acknowledgement and the id — the stored definition is NOT echoed back; read it with `GET /api/v1/reports/reports/{id}`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","id"],"additionalProperties":false},"example":{"ok":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ReportsReports","tags":["reports"],"parameters":[],"description":"Create or upsert a custom report definition. The `id` in the body decides which: an existing definition of this tenant is REPLACED, so a partial body silently drops the fields it leaves out — this is not a patch. A `tenantId` in the body is accepted but overwritten with the session tenant, so a report cannot be planted in another tenant. Nothing is executed here; the definition is only stored. Requires role `manager` or above. When no database is reachable the definition is kept in process memory and is lost on restart.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000},"source":{"type":"string","pattern":"^[a-z_][a-z0-9_]*$","minLength":1,"maxLength":64},"columns":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"alias":{"type":"string","pattern":"^[a-z_][a-z0-9_]*$","minLength":1,"maxLength":64},"aggregation":{"type":"string","enum":["sum","avg","count","count_distinct","min","max"]},"label":{"type":"string","minLength":1,"maxLength":120}},"required":["field"]},"minItems":1,"maxItems":50},"filters":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","in","nin","like","ilike","is_null","is_not_null","between"]},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}}]}},"required":["field","operator"]},"maxItems":50,"default":[]},"joins":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["inner","left","right"],"default":"inner"},"table":{"type":"string","pattern":"^[a-z_][a-z0-9_]*$","minLength":1,"maxLength":64},"on":{"type":"object","properties":{"left":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"right":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129}},"required":["left","right"]}},"required":["table","on"]},"maxItems":10,"default":[]},"groupBy":{"type":"array","items":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"maxItems":20,"default":[]},"orderBy":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":129},"direction":{"type":"string","enum":["asc","desc"],"default":"asc"}},"required":["field"]},"maxItems":10,"default":[]},"limit":{"type":"integer","minimum":1,"maximum":100000,"default":1000},"chart":{"type":"object","properties":{"type":{"type":"string","enum":["bar","line","area","pie","scatter","table"]},"xField":{"type":"string","minLength":1,"maxLength":129},"yFields":{"type":"array","items":{"type":"string","minLength":1,"maxLength":129},"minItems":1},"stacked":{"type":"boolean"},"colors":{"type":"array","items":{"type":"string","pattern":"^#[0-9a-f]{6}$"}},"title":{"type":"string","maxLength":200}},"required":["type","xField","yFields"]},"schedule":{"type":"object","properties":{"frequency":{"type":"string","enum":["daily","weekly","monthly"]},"hour":{"type":"integer","minimum":0,"maximum":23,"default":7},"dayOfWeek":{"type":"integer","minimum":0,"maximum":6},"dayOfMonth":{"type":"integer","minimum":1,"maximum":31},"recipients":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"enabled":{"type":"boolean","default":true}},"required":["frequency","recipients"]},"tenantId":{"type":"string"}},"required":["id","name","source","columns"]}}}},"summary":"Create or upsert a custom report definition","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/reports/reports/{id}":{"get":{"responses":{"200":{"description":"The definition, flat and exactly as stored.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000},"tenantId":{"type":"string","minLength":1,"maxLength":64},"source":{"type":"string","pattern":"^[a-z_][a-z0-9_]*$","minLength":1,"maxLength":64},"columns":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"alias":{"type":"string","pattern":"^[a-z_][a-z0-9_]*$","minLength":1,"maxLength":64},"aggregation":{"type":"string","enum":["sum","avg","count","count_distinct","min","max"]},"label":{"type":"string","minLength":1,"maxLength":120}},"required":["field"]},"minItems":1,"maxItems":50},"filters":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","in","nin","like","ilike","is_null","is_not_null","between"]},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}}]}},"required":["field","operator"]},"maxItems":50,"default":[]},"joins":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["inner","left","right"],"default":"inner"},"table":{"type":"string","pattern":"^[a-z_][a-z0-9_]*$","minLength":1,"maxLength":64},"on":{"type":"object","properties":{"left":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"right":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129}},"required":["left","right"]}},"required":["type","table","on"]},"maxItems":10,"default":[]},"groupBy":{"type":"array","items":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"maxItems":20,"default":[]},"orderBy":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":129},"direction":{"type":"string","enum":["asc","desc"],"default":"asc"}},"required":["field","direction"]},"maxItems":10,"default":[]},"limit":{"type":"integer","minimum":1,"maximum":100000,"default":1000},"chart":{"type":"object","properties":{"type":{"type":"string","enum":["bar","line","area","pie","scatter","table"]},"xField":{"type":"string","minLength":1,"maxLength":129},"yFields":{"type":"array","items":{"type":"string","minLength":1,"maxLength":129},"minItems":1},"stacked":{"type":"boolean"},"colors":{"type":"array","items":{"type":"string","pattern":"^#[0-9a-f]{6}$"}},"title":{"type":"string","maxLength":200}},"required":["type","xField","yFields"]},"schedule":{"type":"object","properties":{"frequency":{"type":"string","enum":["daily","weekly","monthly"]},"hour":{"type":"integer","minimum":0,"maximum":23,"default":7},"dayOfWeek":{"type":"integer","minimum":0,"maximum":6},"dayOfMonth":{"type":"integer","minimum":1,"maximum":31},"recipients":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"enabled":{"type":"boolean","default":true}},"required":["frequency","hour","recipients","enabled"]}},"required":["id","name","tenantId","source","columns","filters","joins","groupBy","orderBy","limit"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"not_found"}},"operationId":"getApiV1ReportsReportsById","tags":["reports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Fetch a single report definition by ID. Returns the stored DEFINITION only — no rows and no chart; running it is `POST /api/v1/reports/reports/{id}/run`. Looked up in this tenant's schema first, then in the in-process fallback store; a definition belonging to another tenant is not reachable and answers 404 like an unknown id. Requires role `user` or above — one step below what creating and running need.","summary":"Fetch a single report definition by ID","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/reports/reports/{id}/run":{"post":{"responses":{"200":{"description":"Definition, rows and chart payload side by side. `result.rowCount` counts the rows in THIS response, it is not a total over the underlying table.","content":{"application/json":{"schema":{"type":"object","properties":{"definition":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000},"tenantId":{"type":"string","minLength":1,"maxLength":64},"source":{"type":"string","pattern":"^[a-z_][a-z0-9_]*$","minLength":1,"maxLength":64},"columns":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"alias":{"type":"string","pattern":"^[a-z_][a-z0-9_]*$","minLength":1,"maxLength":64},"aggregation":{"type":"string","enum":["sum","avg","count","count_distinct","min","max"]},"label":{"type":"string","minLength":1,"maxLength":120}},"required":["field"]},"minItems":1,"maxItems":50},"filters":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","in","nin","like","ilike","is_null","is_not_null","between"]},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}}]}},"required":["field","operator"]},"maxItems":50,"default":[]},"joins":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["inner","left","right"],"default":"inner"},"table":{"type":"string","pattern":"^[a-z_][a-z0-9_]*$","minLength":1,"maxLength":64},"on":{"type":"object","properties":{"left":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"right":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129}},"required":["left","right"]}},"required":["type","table","on"]},"maxItems":10,"default":[]},"groupBy":{"type":"array","items":{"type":"string","pattern":"^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$","minLength":1,"maxLength":129},"maxItems":20,"default":[]},"orderBy":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":129},"direction":{"type":"string","enum":["asc","desc"],"default":"asc"}},"required":["field","direction"]},"maxItems":10,"default":[]},"limit":{"type":"integer","minimum":1,"maximum":100000,"default":1000},"chart":{"type":"object","properties":{"type":{"type":"string","enum":["bar","line","area","pie","scatter","table"]},"xField":{"type":"string","minLength":1,"maxLength":129},"yFields":{"type":"array","items":{"type":"string","minLength":1,"maxLength":129},"minItems":1},"stacked":{"type":"boolean"},"colors":{"type":"array","items":{"type":"string","pattern":"^#[0-9a-f]{6}$"}},"title":{"type":"string","maxLength":200}},"required":["type","xField","yFields"]},"schedule":{"type":"object","properties":{"frequency":{"type":"string","enum":["daily","weekly","monthly"]},"hour":{"type":"integer","minimum":0,"maximum":23,"default":7},"dayOfWeek":{"type":"integer","minimum":0,"maximum":6},"dayOfMonth":{"type":"integer","minimum":1,"maximum":31},"recipients":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"enabled":{"type":"boolean","default":true}},"required":["frequency","hour","recipients","enabled"]}},"required":["id","name","tenantId","source","columns","filters","joins","groupBy","orderBy","limit"]},"result":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]}}},"rowCount":{"type":"number"},"totals":{"type":"object","additionalProperties":{"type":"number"}}},"required":["rows","rowCount"],"additionalProperties":false},"chart":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["bar","line","area","pie","scatter","table"]},"data":{"type":"array","items":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]}}},"xKey":{"type":"string"},"series":{"type":"array","items":{"type":"object","properties":{"dataKey":{"type":"string"},"type":{"type":"string","enum":["bar","line","area","pie","scatter","table"]},"color":{"type":"string"},"stackId":{"type":"string"}},"required":["dataKey","type","color"],"additionalProperties":false}},"title":{"type":"string"}},"required":["type","data","xKey","series"],"additionalProperties":false}},"required":["definition","result","chart"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"not_found"}},"operationId":"postApiV1ReportsReportsByIdRun","tags":["reports"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Execute a report and return rows and chart data. The SQL is built from the stored definition and always seeded with the session tenant id as its first parameter, so a manipulated definition cannot read another tenant. `last_run_at` is stamped best-effort; a failure there does not affect the answer. `chart` is only rendered when the definition carries a chart configuration, otherwise it is `null`. If the query runner cannot be resolved the call still answers 200 — with ZERO rows, which is then indistinguishable from an empty result. Requires role `manager` or above.","summary":"Execute a report and return rows and chart data","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/time-entries":{"get":{"responses":{"200":{"description":"Die passenden Zeiteintraege, neueste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"time_entries":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["time_entries"]},"example":{"time_entries":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Keine Datenbankverbindung oder kein Mandantenkontext."}},"operationId":"getApiV1Time-entries","tags":["Zeiterfassung"],"parameters":[],"summary":"Zeiteintraege auflisten","description":"Die Zeiteintraege des Mandanten, neueste zuerst.\n\nFiltern ueber die Abfrage: `employee`, `project` (je eine Kennung), `from` und `to` (Datum, einschliesslich). Alle vier sind freiwillig und werden UNGEPRUEFT durchgereicht — ein `from=gestern` ergibt keinen 400, sondern einen Datenbankfehler und damit einen 500.\n\nKEINE BLAETTERUNG und keine Obergrenze: die Abfrage gibt alle passenden Zeilen zurueck. Bei einem Mandanten mit Jahren an Erfassung ist das die ganze Historie in einer Antwort.\n\nDie Abfrage ist ein `SELECT *` ohne Serialisierer — der Vertrag sagt keine Feldnamen zu. Die Tabelle traegt heute unter anderem `id`, `employee_id`, `project_id`, `task_id`, `date`, `hours`, `description`, `billable` und `invoice_id`.\n\nKeine Rollenpruefung beim Lesen: jeder angemeldete Benutzer des Mandanten sieht die Eintraege ALLER Mitarbeiter. Anlegen und Loeschen verlangen dagegen `manager`."},"post":{"responses":{"201":{"description":"Der angelegte Zeiteintrag, so wie er in der Tabelle steht.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Rumpf ungueltig — `hours` fehlt oder ist keine Zahl."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"503":{"description":"Keine Datenbankverbindung oder kein Mandantenkontext."}},"operationId":"postApiV1Time-entries","tags":["Zeiterfassung"],"parameters":[],"summary":"Einen Zeiteintrag anlegen","description":"Legt eine Zeile in der Zeiterfassung an. Pflicht ist allein `hours`.\n\nDIE STUNDEN WERDEN NICHT AUF PLAUSIBILITAET GEPRUEFT: 0, negative Werte und mehr als 24 Stunden am Tag werden angenommen. Es gibt auch keine Pruefung auf einen Doppeleintrag — derselbe Aufruf zweimal ergibt zwei Zeilen.\n\nOhne `date` gilt das heutige Datum nach UTC, nicht die Zeitzone des Aufrufers. Mit `date` wird die Zeichenkette UNGEPRUEFT durchgereicht: was die Datenbank nicht als Datum lesen kann, ergibt einen 500, keinen 400.\n\n`employee_id` und `project_id` werden ebenfalls ungeprueft uebernommen — keine Formpruefung, kein Fremdschluessel, keine Existenzpruefung. Ein Wert, der keine Kennung ist, ergibt einen 500; eine Kennung, die es nicht gibt, wird stillschweigend gespeichert.\n\n`billable` steht ohne Angabe auf `true`. Eine Rechnungszuordnung (`invoice_id`) laesst sich hier nicht setzen.\n\nDie Antwort ist die Datenbankzeile UNVERAENDERT (`RETURNING *`, kein Serialisierer). `hours` liegt als NUMERIC in der Tabelle und kommt deshalb als ZEICHENKETTE zurueck („3.50\"), nicht als Zahl — derselbe Stolperstein wie bei `GET /summary`.\n\nDie Tabelle wird bei Bedarf angelegt.\n\nMindestrolle `manager`. Es gibt keine Modul-Wache auf diesem Pfad.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"hours":{"type":"number"},"description":{"type":"string"},"project_id":{"type":"string"},"employee_id":{"type":"string"},"billable":{"type":"boolean"}},"required":["hours"]},"example":{"date":"string","hours":0,"description":"string","project_id":"string","employee_id":"string","billable":true}}}}}},"/api/v1/time-entries/summary":{"get":{"responses":{"200":{"description":"Zaehler und Stundensummen im gefilterten Zeitraum. Die Stundenwerte sind null, wenn im Zeitraum nichts gebucht wurde — SUM ueber null Zeilen ist NULL, nicht 0.","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"number"},"total_hours":{"type":["number","null"]},"billable_hours":{"type":["number","null"]}},"required":["entries","total_hours","billable_hours"],"additionalProperties":false},"example":{"entries":0,"total_hours":0,"billable_hours":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Time-entriesSummary","tags":["TimeTracking"],"parameters":[],"summary":"Zusammenfassung der erfassten Zeiten (Anzahl, Stunden, davon abrechenbar)","description":"Zaehlt und summiert die Zeiteintraege des Mandanten in EINER Zeile. Filtern laesst sich nach `employee`, `project` sowie ueber `from`/`to` auf das Erfassungsdatum; mehrere Filter wirken zusammen (UND), ohne Angabe zaehlt alles. `billable_hours` ist eine Teilmenge von `total_hours`, keine zusaetzliche Groesze. ACHTUNG bei leerem Ergebnis: `entries` ist dann 0, die beiden Stundenwerte sind aber NULL und nicht 0 — eine Summe ueber null Zeilen ist in SQL nicht null. Es gibt keine Blaetterung und keine Aufschluesselung je Mitarbeiter oder Projekt; dafuer ist die Liste da. Rein lesend; fehlt die Tabelle im Mandanten-Schema, legt der Aufruf sie an."}},"/api/v1/time-entries/{id}":{"delete":{"responses":{"200":{"description":"Der Loeschbefehl lief. Sagt NICHT, dass eine Zeile getroffen wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"503":{"description":"Keine Datenbankverbindung oder kein Mandantenkontext."}},"operationId":"deleteApiV1Time-entriesById","tags":["Zeiterfassung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen Zeiteintrag endgueltig loeschen","description":"Loescht die Zeile ENDGUELTIG — die Tabelle fuehrt kein `deleted_at`. War der Eintrag bereits einer Rechnung zugeordnet (`invoice_id`), verschwindet die Grundlage der Abrechnung, ohne dass die Rechnung davon erfaehrt.\n\nDIE ANTWORT SAGT NICHT, OB ETWAS GELOESCHT WURDE: `ok: true` kommt auch bei einer erfundenen Kennung und bei einem Eintrag eines fremden Mandanten. Es wird nicht geprueft, ob eine Zeile getroffen wurde — einen 404 gibt es hier nicht. Der Mandantenfilter greift trotzdem: fremde Zeilen werden nie geloescht, der Aufrufer merkt es nur nicht.\n\nMindestrolle `manager`."}},"/api/v1/time-entries/budget-burn/{projectId}":{"get":{"responses":{"200":{"description":"Die Schaetzung. `null` bei `budget` heisst „kein Budget ODER keine Budget-Spalte\".","content":{"application/json":{"schema":{"type":"object","properties":{"project_id":{"type":"string"},"budget":{"type":["number","null"]},"hours_billable":{"type":"number"},"cost_estimate":{"type":"number"},"burn_percent":{"type":["number","null"]},"remaining_budget":{"type":["number","null"]}},"required":["project_id","budget","hours_billable","cost_estimate","burn_percent","remaining_budget"]},"example":{"project_id":"string","budget":0,"hours_billable":0,"cost_estimate":0,"burn_percent":0,"remaining_budget":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Projekt mit dieser Kennung im eigenen Mandanten — als `text/plain`, nicht als JSON (`HTTPException`)."},"503":{"description":"Keine Datenbankverbindung oder kein Mandantenkontext."}},"operationId":"getApiV1Time-entriesBudget-burnByProjectId","tags":["Zeiterfassung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"projectId","required":true}],"summary":"Budgetverbrauch eines Projekts schaetzen","description":"Rechnet die abrechenbaren Stunden des Projekts gegen sein Budget.\n\nES IST EINE SCHAETZUNG, KEINE ABRECHNUNG — der Stundensatz kommt aus einer DREISTUFIGEN Kette: der `hourly_rate` des jeweiligen Mitarbeiters, ersatzweise der `default_hourly_rate` des Mandanten, ersatzweise **80 Euro** aus dem Quelltext. Welche Stufe je Mitarbeiter gegriffen hat, sagt die Antwort NICHT. Ein Betrieb ohne gepflegte Saetze bekommt deshalb eine plausibel aussehende Zahl, die auf einem erfundenen Satz beruht.\n\nNicht abrechenbare Stunden (`billable = false`) zaehlen weder in `hours_billable` noch in die Kosten.\n\n`budget`, `burn_percent` und `remaining_budget` sind `null`, wenn das Projekt kein Budget traegt — oder wenn die Spalte `budget` in diesem Mandanten gar nicht existiert. Beide Faelle sehen gleich aus. Der Prozentwert bleibt ausserdem `null` bei einem Budget von 0, statt durch null zu teilen.\n\nDie Spaltenpruefungen laufen ueber `information_schema`, auf DIESES Mandantenschema begrenzt — ein alter Mandant ohne `hourly_rate` oder `budget` faellt weich zurueck, statt einen Fehler zu werfen. Eine wirklich fehlende TABELLE schlaegt dagegen durch und wird nicht kaschiert.\n\nKeine Rollenpruefung beim Lesen: jeder angemeldete Benutzer des Mandanten sieht die Eintraege ALLER Mitarbeiter. Anlegen und Loeschen verlangen dagegen `manager`."}},"/api/v1/gaeb-lv":{"get":{"responses":{"200":{"description":"Die LV-Koepfe des Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"lvs":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["lvs"]},"example":{"lvs":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"getApiV1Gaeb-lv","tags":["GAEB-LV"],"parameters":[],"summary":"Leistungsverzeichnisse auflisten","description":"Alle LV-Koepfe des Mandanten, neueste zuerst. Ohne die Positionen — die liefert erst der Einzelabruf.\n\nEs gibt KEINE Blaetterung und keine Filter: die Abfrage gibt alle Zeilen zurueck.\n\nDie Zeilen kommen aus einem `SELECT *` und gehen ohne Serialisierer hinaus. Heute traegt ein Kopf `id`, `tenant_id`, `project_id`, `title`, `name`, `source`, `gaeb_version` und `created_at` — `title` und `name` werden beim Anlegen mit DEMSELBEN Wert gefuellt."},"post":{"responses":{"201":{"description":"Der angelegte LV-Kopf, ohne Positionen.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1Gaeb-lv","tags":["GAEB-LV"],"parameters":[],"summary":"Ein leeres Leistungsverzeichnis anlegen","description":"Legt einen LV-KOPF an, sonst nichts. Positionen entstehen hier keine — die traegt `POST /{id}/positions` einzeln nach.\n\n`title` und `name` werden mit DEMSELBEN Wert aus `name` gefuellt; zwei Spalten, ein Inhalt.\n\n`projectId` wird UNGEPRUEFT uebernommen: es wird nicht nachgesehen, ob es das Projekt gibt oder ob es diesem Mandanten gehoert.\n\nDie Tabellen werden bei Bedarf angelegt; ein frischer Mandant kann ohne Vorbereitung anlegen.\n\nDie Antwort ist die Zeile, wie sie in der Tabelle steht (`RETURNING *`, kein Serialisierer) — dieselbe Form wie in der Liste.\n\nUngegatet: jeder angemeldete Benutzer des Mandanten darf anlegen. Es gibt weder eine Rollenpruefung noch eine Modul-Wache auf diesem Pfad.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":500},"projectId":{"type":"string","format":"uuid"},"source":{"type":"string","enum":["import","manual"],"default":"manual"},"gaebVersion":{"type":"string","maxLength":20,"default":"DA83"}},"required":["name"]},"example":{"name":"string","projectId":"00000000-0000-4000-8000-000000000000","source":"import","gaebVersion":"string"}}}}}},"/api/v1/gaeb-lv/{id}":{"get":{"responses":{"200":{"description":"Kopf und Positionen.","content":{"application/json":{"schema":{"type":"object","properties":{"header":{"type":"object","additionalProperties":{}},"positions":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["header","positions"]},"example":{"header":{},"positions":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein LV mit dieser Kennung IM EIGENEN MANDANTEN — `error: \"NOT_FOUND\"`. Deckt „gibt es nicht\" und „gehoert einem anderen Mandanten\" gemeinsam ab.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"NOT_FOUND"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"getApiV1Gaeb-lvById","tags":["GAEB-LV"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ein Leistungsverzeichnis mit allen Positionen","description":"Kopf plus alle Positionen, sortiert nach `position_nr` — und zwar als TEXT sortiert, nicht numerisch: „10\" kommt vor „9\".\n\nBeide Teile sind `SELECT *` ohne Serialisierer. Eine Position traegt heute `id`, `lv_id`, `position_nr`, `kurztext`, `langtext`, `menge`, `einheit`, `ep` (Einzelpreis) und `gp` (Gesamtpreis).\n\nDiese Route legt die Tabellen NICHT an. Bei einem Mandanten, der noch nie die Liste geholt oder ein LV angelegt hat, laeuft sie deshalb in einen 500 statt in einen 404."}},"/api/v1/gaeb-lv/{id}/positions":{"post":{"responses":{"201":{"description":"Die angelegte Position, wie sie in der Tabelle steht.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein LV mit dieser Kennung IM EIGENEN MANDANTEN — `error: \"NOT_FOUND\"`. Deckt „gibt es nicht\" und „gehoert einem anderen Mandanten\" gemeinsam ab.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"NOT_FOUND"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1Gaeb-lvByIdPositions","tags":["GAEB-LV"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine Position an ein Leistungsverzeichnis anhaengen","description":"Haengt genau eine Position an den LV-Kopf. Der Kopf wird VORHER gegen den Mandanten geprueft — ein fremdes oder unbekanntes LV ergibt 404 und legt nichts an.\n\nALLE Felder sind freiwillig. Ein leerer Rumpf `{}` legt eine Position ohne Nummer, ohne Text, ohne Menge und ohne Preis an; das ist kein Fehler, sondern eine leere Zeile im LV. Es wird auch nicht geprueft, ob die Positionsnummer im LV schon vorkommt: Nummern duerfen sich doppeln.\n\nDer Gesamtpreis `gp` wird nur dann gerechnet, wenn er FEHLT und sowohl `menge` als auch `ep` gesetzt sind (menge × ep). Fehlt eines von beiden, bleibt `gp` leer — es wird NICHT 0. Ein mitgegebenes `gp` wird nie nachgerechnet, auch wenn es nicht zu menge × ep passt.\n\nDiese Route legt die Tabellen NICHT an. Bei einem Mandanten, der noch nie ein LV hatte, laeuft schon die Kopfpruefung in einen 500 statt in einen 404.\n\nUngegatet: jeder angemeldete Benutzer des Mandanten darf anlegen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"positionNr":{"type":"string","maxLength":20},"kurztext":{"type":"string"},"langtext":{"type":"string"},"menge":{"type":"number","minimum":0},"einheit":{"type":"string","maxLength":20},"ep":{"type":"number","minimum":0},"gp":{"type":"number","minimum":0}}},"example":{"positionNr":"string","kurztext":"string","langtext":"string","menge":0,"einheit":"string","ep":0,"gp":0}}}}}},"/api/v1/gaeb-lv/{id}/positions/{posId}":{"delete":{"responses":{"200":{"description":"Der Loeschbefehl lief. Sagt NICHT, dass eine Position getroffen wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein LV mit dieser Kennung IM EIGENEN MANDANTEN — `error: \"NOT_FOUND\"`. Deckt „gibt es nicht\" und „gehoert einem anderen Mandanten\" gemeinsam ab.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"NOT_FOUND"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"deleteApiV1Gaeb-lvByIdPositionsByPosId","tags":["GAEB-LV"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"posId","required":true}],"summary":"Position aus einem Leistungsverzeichnis loeschen","description":"Loescht die Position ENDGUELTIG — `lv_positions` fuehrt kein `deleted_at`.\n\nDer LV-Kopf wird vorher gegen den Mandanten geprueft; die Position muss ausserdem zu diesem LV gehoeren (`WHERE id = $1 AND lv_id = $2`). Eine fremde Positionskennung trifft deshalb nichts.\n\nDIE ANTWORT SAGT NICHT, OB ETWAS GELOESCHT WURDE. Der 404 bezieht sich allein auf den LV-KOPF. Existiert der LV, gibt es `ok: true` auch dann, wenn die Positionskennung ins Leere ging."}},"/api/v1/gaeb-lv/{id}/to-angebot":{"post":{"responses":{"200":{"description":"FEHLSCHLAG, als Erfolg verpackt: `error: \"QUOTES_TABLE_UNAVAILABLE\"`, `detail` mit der Datenbankmeldung und `mapping` mit dem Vorschlag. Es wurde nichts angelegt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"QUOTES_TABLE_UNAVAILABLE"},"detail":{"type":"string","description":"Die Meldung der Datenbank im Klartext."},"mapping":{"type":"object","properties":{"title":{"type":"string"},"customerId":{"type":["string","null"]},"lines":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["title","customerId","lines"]}},"required":["error","detail","mapping"]},"example":{"error":"QUOTES_TABLE_UNAVAILABLE","detail":"string","mapping":{"title":"string","customerId":"string","lines":[{}]}}}}},"201":{"description":"Der gemeinte Ausgang: `quote`, `lines` und `mapped` (Anzahl uebernommener Positionen). Wird derzeit nicht erreicht.","content":{"application/json":{"schema":{"type":"object","properties":{"quote":{"type":"object","additionalProperties":{}},"lines":{"type":"array","items":{"type":"object","additionalProperties":{}}},"lvId":{"type":"string"},"mapped":{"type":"integer"}},"required":["quote","lines","lvId","mapped"]},"example":{"quote":{},"lines":[{}],"lvId":"string","mapped":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein LV mit dieser Kennung IM EIGENEN MANDANTEN — `error: \"NOT_FOUND\"`. Deckt „gibt es nicht\" und „gehoert einem anderen Mandanten\" gemeinsam ab.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"NOT_FOUND"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1Gaeb-lvByIdTo-angebot","tags":["GAEB-LV"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aus einem Leistungsverzeichnis ein Angebot machen — WIRKUNGSLOS","description":"Soll die Positionen des LV in ein Angebot mit Angebotszeilen kopieren. NACH HEUTIGEM STAND ERZEUGT SIE KEIN ANGEBOT — ausfuehrlich im Kopf dieser Datei.\n\nDer INSERT nennt eine Spalte `source_lv_id`, die es in keinem Schema, keiner Migration und keiner `ensure`-Funktion gibt, und schreibt ausserdem in ein unqualifiziertes `quotes`, waehrend die echten Angebote im Mandantenschema liegen. Er scheitert deshalb IMMER, der Fehler wird gefangen, und die Route antwortet mit **HTTP 200** und `error: \"QUOTES_TABLE_UNAVAILABLE\"`.\n\nEIN 200 IST HIER KEIN ERFOLG. Wer diese Route aufruft, muss in der 200-Antwort auf das Feld `error` sehen: ist es gesetzt, wurde nichts angelegt, und `mapping` enthaelt nur den Vorschlag, wie das Angebot aussehen wuerde (Titel, Kunde, Zeilen aus den LV-Positionen). Das LV selbst bleibt unveraendert; es entsteht kein Beleg und keine Belegnummer.\n\nDer 201-Fall unten beschreibt den GEMEINTEN Ausgang. Er ist Teil des Vertrags, wird aber mit dem heutigen Quelltext nicht erreicht.\n\nWas vor dem Versuch trotzdem geprueft wird: das LV muss dem Mandanten gehoeren, sonst 404 und kein Versuch.\n\nUngegatet: jeder angemeldete Benutzer des Mandanten darf aufrufen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid"},"title":{"type":"string","maxLength":500}}},"example":{"customerId":"00000000-0000-4000-8000-000000000000","title":"string"}}}}}},"/api/v1/gaeb-lv/{id}/export":{"get":{"responses":{"200":{"description":"Das GAEB-DA83-Dokument als Datei, Dateiname `lv-<Kennung>.xml`.","content":{"application/xml":{"schema":{"type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein LV mit dieser Kennung IM EIGENEN MANDANTEN — `error: \"NOT_FOUND\"`. Deckt „gibt es nicht\" und „gehoert einem anderen Mandanten\" gemeinsam ab.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"NOT_FOUND"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"getApiV1Gaeb-lvByIdExport","tags":["GAEB-LV"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Leistungsverzeichnis als GAEB-XML herunterladen","description":"Liefert das LV als GAEB-DA83-XML zum Herunterladen (`Content-Disposition: attachment`).\n\nKEIN JSON — deshalb steht unter 200 auch kein JSON-Schema, sondern der Inhaltstyp `application/xml`. Nur der Fehlerfall antwortet in JSON.\n\nWas das XML NICHT ist: eine vollstaendige GAEB-Datei. Es traegt einen festen Rumpf mit `<GAEBInfo><Version>3.1</Version>`, `<DP>83</DP>` und je Position `Quantity`, `QU`, `UP`, `GP`, `ONum` und den KURZTEXT. Der Langtext, die Gliederung und alle Kopfdaten ausser dem Namen fehlen.\n\nSONDERZEICHEN WERDEN NICHT MASKIERT. Ein `&` oder `<` in Kurztext, Einheit oder Name landet unveraendert im Dokument und macht es fuer einen strengen Parser ungueltig."}},"/api/v1/gaeb-lv/import":{"post":{"responses":{"201":{"description":"Der angelegte LV-Kopf, so wie er in der Tabelle steht — OHNE Positionen, denn es wurden keine angelegt.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1Gaeb-lvImport","tags":["GAEB-LV"],"parameters":[],"summary":"GAEB-Datei hochladen — legt NUR den LV-Kopf an","description":"DIESE ROUTE IMPORTIERT KEINE POSITIONEN. Sie liest den hochgeladenen Text mit EINEM regulaeren Ausdruck (`<AddText>…</AddText>`), nimmt den Treffer als Namen und legt damit einen LEEREN LV-Kopf an. Die Positionen im Dokument werden nicht angesehen. Wer sie braucht, traegt sie einzeln ueber `POST /{id}/positions` nach.\n\nFindet der Ausdruck nichts, heisst das LV woertlich „Import\". Es gibt keine Pruefung, ob der Rumpf ueberhaupt GAEB oder XML ist: eine beliebige Textdatei wird mit 201 angenommen. Ein leerer Rumpf ebenfalls.\n\nDer Rumpf wird als reiner TEXT gelesen, nicht als JSON und nicht als Formulardatei — die Datei gehoert unverpackt in den Rumpf. `source` steht danach auf `import`, `gaeb_version` fest auf `DA83`, unabhaengig vom Inhalt."}},"/api/v1/forecasts":{"get":{"responses":{"200":{"description":"Die Forecasts dieser Seite samt Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"forecasts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Forecasts (UUID)"},"rep":{"type":"string","minLength":1,"maxLength":120,"description":"Vertriebskraft, auf die sich der Forecast bezieht"},"quarter":{"type":"string","pattern":"^\\d{4}-Q[1-4]$","description":"Quartal in der Form `YYYY-Qn`, z. B. `2026-Q3`"},"pipeline":{"type":"number","minimum":0,"description":"Gesamter Pipeline-Wert in Euro — alle Chancen, ungewichtet"},"bestCase":{"type":"number","minimum":0,"description":"Bestenfalls erreichbarer Wert in Euro"},"commit":{"type":"number","minimum":0,"description":"Verbindlich zugesagter Wert in Euro"},"closedWon":{"type":"number","minimum":0,"description":"Bereits gewonnener Wert in Euro"},"status":{"type":"string","enum":["open","committed","closed"],"description":"`open` = in Arbeit, `committed` = zugesagt, `closed` = abgeschlossen"},"notes":{"type":["string","null"],"description":"Freitext zur Einschaetzung; `null`, wenn nichts hinterlegt ist"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","rep","quarter","pipeline","bestCase","commit","closedWon","status","notes","createdAt","updatedAt"],"additionalProperties":false},"description":"Die Forecasts dieser Seite, absteigend nach Quartal und darin nach Vertriebskraft"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer fuer die gesetzten Filter — unabhaengig von `limit` und `offset`"}},"required":["forecasts","total"],"additionalProperties":false},"example":{"forecasts":[],"total":0}}}},"400":{"description":"Ungueltige Query-Parameter (z. B. `quarter` nicht in der Form `YYYY-Qn`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Meldung im Klartext, direkt anzeigbar"},"fields":{"type":"array","items":{"type":"string"},"description":"Namen der beanstandeten Felder; verschachtelte Pfade mit Punkt getrennt"}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"getApiV1Forecasts","tags":["Sales · Forecasts"],"parameters":[{"in":"query","name":"rep","schema":{"type":"string"}},{"in":"query","name":"quarter","schema":{"type":"string","pattern":"^\\d{4}-Q[1-4]$"}},{"in":"query","name":"status","schema":{"type":"string","enum":["open","committed","closed"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste Forecasts (Filter)","description":"Listet die Umsatzprognosen des Mandanten, wahlweise gefiltert nach Vertriebskraft, Quartal und Status. `total` zaehlt alle Treffer, nicht nur die gelieferte Seite."},"post":{"responses":{"201":{"description":"Der angelegte Forecast","content":{"application/json":{"schema":{"type":"object","properties":{"forecast":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Forecasts (UUID)"},"rep":{"type":"string","minLength":1,"maxLength":120,"description":"Vertriebskraft, auf die sich der Forecast bezieht"},"quarter":{"type":"string","pattern":"^\\d{4}-Q[1-4]$","description":"Quartal in der Form `YYYY-Qn`, z. B. `2026-Q3`"},"pipeline":{"type":"number","minimum":0,"description":"Gesamter Pipeline-Wert in Euro — alle Chancen, ungewichtet"},"bestCase":{"type":"number","minimum":0,"description":"Bestenfalls erreichbarer Wert in Euro"},"commit":{"type":"number","minimum":0,"description":"Verbindlich zugesagter Wert in Euro"},"closedWon":{"type":"number","minimum":0,"description":"Bereits gewonnener Wert in Euro"},"status":{"type":"string","enum":["open","committed","closed"],"description":"`open` = in Arbeit, `committed` = zugesagt, `closed` = abgeschlossen"},"notes":{"type":["string","null"],"description":"Freitext zur Einschaetzung; `null`, wenn nichts hinterlegt ist"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","rep","quarter","pipeline","bestCase","commit","closedWon","status","notes","createdAt","updatedAt"],"additionalProperties":false,"description":"Der betroffene Forecast nach der Operation"}},"required":["forecast"],"additionalProperties":false},"example":{"forecast":{"id":"8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f","rep":"Markus Lindner","quarter":"2026-Q4","pipeline":150000,"bestCase":110000,"commit":75000,"closedWon":0,"status":"open","notes":null,"createdAt":"2026-09-02T09:30:00.000Z","updatedAt":"2026-09-02T09:30:00.000Z"}}}}},"400":{"description":"Pflichtfeld fehlt oder Wert ausserhalb der Grenzen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Meldung im Klartext, direkt anzeigbar"},"fields":{"type":"array","items":{"type":"string"},"description":"Namen der beanstandeten Felder; verschachtelte Pfade mit Punkt getrennt"}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `manager`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"postApiV1Forecasts","tags":["Sales · Forecasts"],"parameters":[],"summary":"Forecast anlegen","description":"Legt einen Forecast an und gibt ihn in der gespeicherten Fassung zurueck — einschliesslich der von der Datenbank vergebenen `id` und Zeitstempel. Dieselbe Vertriebskraft darf mehrere Forecasts je Quartal haben; es gibt hier keine Eindeutigkeitspruefung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"rep":{"type":"string","minLength":1,"maxLength":120},"quarter":{"type":"string","pattern":"^\\d{4}-Q[1-4]$"},"pipeline":{"type":"number","minimum":0,"default":0},"bestCase":{"type":"number","minimum":0,"default":0},"commit":{"type":"number","minimum":0,"default":0},"closedWon":{"type":"number","minimum":0,"default":0},"status":{"type":"string","enum":["open","committed","closed"],"default":"open"},"notes":{"type":["string","null"]}},"required":["rep","quarter"]}}}}}},"/api/v1/forecasts/stats":{"get":{"responses":{"200":{"description":"Die Summen und die Anzahl der einbezogenen Datensaetze","content":{"application/json":{"schema":{"type":"object","properties":{"pipeline":{"type":"number","minimum":0,"description":"Summe aller Pipeline-Werte in Euro"},"bestCase":{"type":"number","minimum":0,"description":"Summe aller Bestfall-Werte in Euro"},"commit":{"type":"number","minimum":0,"description":"Summe aller zugesagten Werte in Euro"},"closedWon":{"type":"number","minimum":0,"description":"Summe aller gewonnenen Werte in Euro"},"count":{"type":"integer","minimum":0,"description":"Anzahl der Forecast-Datensaetze, ueber die summiert wurde"}},"required":["pipeline","bestCase","commit","closedWon","count"],"additionalProperties":false},"example":{"pipeline":0,"bestCase":0,"commit":0,"closedWon":0,"count":0}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"getApiV1ForecastsStats","tags":["Sales · Forecasts"],"parameters":[],"summary":"Roll-up KPIs","description":"Summiert Pipeline, Bestfall, Zusage und Gewonnenes ueber ALLE Forecasts des Mandanten. Der Aufruf kennt keine Filter — auch nicht nach Quartal; wer je Quartal rechnen will, nimmt die Liste und summiert selbst."}},"/api/v1/forecasts/{id}":{"get":{"responses":{"200":{"description":"Der Forecast","content":{"application/json":{"schema":{"type":"object","properties":{"forecast":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Forecasts (UUID)"},"rep":{"type":"string","minLength":1,"maxLength":120,"description":"Vertriebskraft, auf die sich der Forecast bezieht"},"quarter":{"type":"string","pattern":"^\\d{4}-Q[1-4]$","description":"Quartal in der Form `YYYY-Qn`, z. B. `2026-Q3`"},"pipeline":{"type":"number","minimum":0,"description":"Gesamter Pipeline-Wert in Euro — alle Chancen, ungewichtet"},"bestCase":{"type":"number","minimum":0,"description":"Bestenfalls erreichbarer Wert in Euro"},"commit":{"type":"number","minimum":0,"description":"Verbindlich zugesagter Wert in Euro"},"closedWon":{"type":"number","minimum":0,"description":"Bereits gewonnener Wert in Euro"},"status":{"type":"string","enum":["open","committed","closed"],"description":"`open` = in Arbeit, `committed` = zugesagt, `closed` = abgeschlossen"},"notes":{"type":["string","null"],"description":"Freitext zur Einschaetzung; `null`, wenn nichts hinterlegt ist"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","rep","quarter","pipeline","bestCase","commit","closedWon","status","notes","createdAt","updatedAt"],"additionalProperties":false,"description":"Der betroffene Forecast nach der Operation"}},"required":["forecast"],"additionalProperties":false},"example":{"forecast":{"id":"4e6f8a0b-1c2d-4e3f-9a5b-6c7d8e9f0a1b","rep":"Katrin Hoffmann","quarter":"2026-Q3","pipeline":245000,"bestCase":180000,"commit":120000,"closedWon":64500,"status":"committed","notes":"Zwei Großprojekte im Raum Stuttgart stehen kurz vor Abschluss.","createdAt":"2026-07-01T08:00:00.000Z","updatedAt":"2026-08-14T15:22:10.000Z"}}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Forecast mit dieser Kennung — Klartext `not found`, kein JSON"},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"getApiV1ForecastsById","tags":["Sales · Forecasts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Forecast Detail","description":"Liefert einen einzelnen Forecast anhand seiner UUID."},"put":{"responses":{"200":{"description":"Der Forecast nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"forecast":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Forecasts (UUID)"},"rep":{"type":"string","minLength":1,"maxLength":120,"description":"Vertriebskraft, auf die sich der Forecast bezieht"},"quarter":{"type":"string","pattern":"^\\d{4}-Q[1-4]$","description":"Quartal in der Form `YYYY-Qn`, z. B. `2026-Q3`"},"pipeline":{"type":"number","minimum":0,"description":"Gesamter Pipeline-Wert in Euro — alle Chancen, ungewichtet"},"bestCase":{"type":"number","minimum":0,"description":"Bestenfalls erreichbarer Wert in Euro"},"commit":{"type":"number","minimum":0,"description":"Verbindlich zugesagter Wert in Euro"},"closedWon":{"type":"number","minimum":0,"description":"Bereits gewonnener Wert in Euro"},"status":{"type":"string","enum":["open","committed","closed"],"description":"`open` = in Arbeit, `committed` = zugesagt, `closed` = abgeschlossen"},"notes":{"type":["string","null"],"description":"Freitext zur Einschaetzung; `null`, wenn nichts hinterlegt ist"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","rep","quarter","pipeline","bestCase","commit","closedWon","status","notes","createdAt","updatedAt"],"additionalProperties":false,"description":"Der betroffene Forecast nach der Operation"}},"required":["forecast"],"additionalProperties":false}}}},"400":{"description":"Ungueltiger Wert im Rumpf (JSON nach dem Schema). Ein Rumpf ganz OHNE aenderbares Feld antwortet stattdessen mit Klartext `no fields to update`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Meldung im Klartext, direkt anzeigbar"},"fields":{"type":"array","items":{"type":"string"},"description":"Namen der beanstandeten Felder; verschachtelte Pfade mit Punkt getrennt"}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `manager`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Forecast mit dieser Kennung — Klartext `not found`, kein JSON"},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"putApiV1ForecastsById","tags":["Sales · Forecasts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Forecast aktualisieren","description":"Aendert einzelne Felder eines Forecasts. Nur mitgesendete Felder werden geschrieben; `updatedAt` setzt der Server selbst.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"rep":{"type":"string","minLength":1,"maxLength":120},"quarter":{"type":"string","pattern":"^\\d{4}-Q[1-4]$"},"pipeline":{"type":"number","minimum":0,"default":0},"bestCase":{"type":"number","minimum":0,"default":0},"commit":{"type":"number","minimum":0,"default":0},"closedWon":{"type":"number","minimum":0,"default":0},"status":{"type":"string","enum":["open","committed","closed"],"default":"open"},"notes":{"type":["string","null"]}}},"example":{"rep":"string","pipeline":0,"bestCase":0,"commit":0,"closedWon":0,"status":"open","notes":"string"}}}}},"delete":{"responses":{"200":{"description":"Quittung ohne Nutzlast — der Datensatz ist entfernt","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true,"description":"Immer `true` — der Datensatz ist entfernt"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":true}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `manager`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Forecast mit dieser Kennung — Klartext `not found`, kein JSON"},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"deleteApiV1ForecastsById","tags":["Sales · Forecasts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Forecast löschen","description":"Entfernt den Forecast endgueltig aus der Tabelle — es gibt hier KEIN `deleted_at` und damit keinen Weg zurueck. Wer die Zeile nur stilllegen will, setzt stattdessen `status` auf `closed`."}},"/api/v1/territories":{"get":{"responses":{"200":{"description":"Die Gebiete dieser Seite samt Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"territories":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Gebiets (UUID)"},"code":{"type":"string","minLength":2,"maxLength":40,"description":"Kurzzeichen des Gebiets, mandantenweit eindeutig (z. B. „NORD-01\")"},"name":{"type":"string","minLength":1,"maxLength":160,"description":"Bezeichnung des Gebiets"},"region":{"type":"string","minLength":1,"maxLength":80,"description":"Uebergeordnete Region, nach der die Liste gefiltert werden kann"},"primaryRep":{"type":"string","minLength":1,"maxLength":120,"description":"Hauptverantwortliche Vertriebskraft"},"supportReps":{"type":"array","items":{"type":"string","maxLength":120},"description":"Unterstuetzende Vertriebskraefte; leeres Feld, wenn keine hinterlegt sind"},"customers":{"type":"integer","minimum":0,"description":"Anzahl der dem Gebiet zugeordneten Kunden"},"ytdRevenue":{"type":"number","minimum":0,"description":"Umsatz seit Jahresbeginn in Euro, zwei Nachkommastellen"},"status":{"type":"string","enum":["active","inactive"],"description":"`active` = bewirtschaftet, `inactive` = stillgelegt (Ergebnis von DELETE)"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","code","name","region","primaryRep","supportReps","customers","ytdRevenue","status","createdAt","updatedAt"],"additionalProperties":false},"description":"Die Gebiete dieser Seite, aufsteigend nach `code`"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer fuer die gesetzten Filter — unabhaengig von `limit` und `offset`"}},"required":["territories","total"],"additionalProperties":false},"example":{"territories":[{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","region":"string","primaryRep":"string","supportReps":["string"],"customers":0,"ytdRevenue":0,"status":"active","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}],"total":0}}}},"400":{"description":"Ungueltige Query-Parameter (z. B. `limit` groesser als 200)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Meldung im Klartext, direkt anzeigbar"},"fields":{"type":"array","items":{"type":"string"},"description":"Namen der beanstandeten Felder; verschachtelte Pfade mit Punkt getrennt (z. B. `address.zip`)"}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"getApiV1Territories","tags":["Sales · Territories"],"parameters":[{"in":"query","name":"region","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste Gebiete","description":"Listet die Vertriebsgebiete des Mandanten, wahlweise gefiltert nach Region und Status. Sortiert nach `code`; `total` zaehlt alle Treffer, nicht nur die gelieferte Seite."},"post":{"responses":{"201":{"description":"Das angelegte Gebiet","content":{"application/json":{"schema":{"type":"object","properties":{"territory":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Gebiets (UUID)"},"code":{"type":"string","minLength":2,"maxLength":40,"description":"Kurzzeichen des Gebiets, mandantenweit eindeutig (z. B. „NORD-01\")"},"name":{"type":"string","minLength":1,"maxLength":160,"description":"Bezeichnung des Gebiets"},"region":{"type":"string","minLength":1,"maxLength":80,"description":"Uebergeordnete Region, nach der die Liste gefiltert werden kann"},"primaryRep":{"type":"string","minLength":1,"maxLength":120,"description":"Hauptverantwortliche Vertriebskraft"},"supportReps":{"type":"array","items":{"type":"string","maxLength":120},"description":"Unterstuetzende Vertriebskraefte; leeres Feld, wenn keine hinterlegt sind"},"customers":{"type":"integer","minimum":0,"description":"Anzahl der dem Gebiet zugeordneten Kunden"},"ytdRevenue":{"type":"number","minimum":0,"description":"Umsatz seit Jahresbeginn in Euro, zwei Nachkommastellen"},"status":{"type":"string","enum":["active","inactive"],"description":"`active` = bewirtschaftet, `inactive` = stillgelegt (Ergebnis von DELETE)"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","code","name","region","primaryRep","supportReps","customers","ytdRevenue","status","createdAt","updatedAt"],"additionalProperties":false,"description":"Das betroffene Gebiet nach der Operation"}},"required":["territory"],"additionalProperties":false},"example":{"territory":{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","region":"string","primaryRep":"string","supportReps":["string"],"customers":0,"ytdRevenue":0,"status":"active","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"400":{"description":"Pflichtfeld fehlt oder Wert ausserhalb der Grenzen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Meldung im Klartext, direkt anzeigbar"},"fields":{"type":"array","items":{"type":"string"},"description":"Namen der beanstandeten Felder; verschachtelte Pfade mit Punkt getrennt (z. B. `address.zip`)"}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `manager`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"postApiV1Territories","tags":["Sales · Territories"],"parameters":[],"summary":"Gebiet anlegen","description":"Legt ein Vertriebsgebiet an und gibt es in der gespeicherten Fassung zurueck — einschliesslich der von der Datenbank vergebenen `id` und Zeitstempel.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":2,"maxLength":40},"name":{"type":"string","minLength":1,"maxLength":160},"region":{"type":"string","minLength":1,"maxLength":80},"primaryRep":{"type":"string","minLength":1,"maxLength":120},"supportReps":{"type":"array","items":{"type":"string","maxLength":120},"default":[]},"customers":{"type":"integer","minimum":0,"default":0},"ytdRevenue":{"type":"number","minimum":0,"default":0},"status":{"type":"string","enum":["active","inactive"],"default":"active"}},"required":["code","name","region","primaryRep"]},"example":{"code":"string","name":"string","region":"string","primaryRep":"string","supportReps":["string"],"customers":0,"ytdRevenue":0,"status":"active"}}}}}},"/api/v1/territories/{id}":{"get":{"responses":{"200":{"description":"Das Gebiet","content":{"application/json":{"schema":{"type":"object","properties":{"territory":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Gebiets (UUID)"},"code":{"type":"string","minLength":2,"maxLength":40,"description":"Kurzzeichen des Gebiets, mandantenweit eindeutig (z. B. „NORD-01\")"},"name":{"type":"string","minLength":1,"maxLength":160,"description":"Bezeichnung des Gebiets"},"region":{"type":"string","minLength":1,"maxLength":80,"description":"Uebergeordnete Region, nach der die Liste gefiltert werden kann"},"primaryRep":{"type":"string","minLength":1,"maxLength":120,"description":"Hauptverantwortliche Vertriebskraft"},"supportReps":{"type":"array","items":{"type":"string","maxLength":120},"description":"Unterstuetzende Vertriebskraefte; leeres Feld, wenn keine hinterlegt sind"},"customers":{"type":"integer","minimum":0,"description":"Anzahl der dem Gebiet zugeordneten Kunden"},"ytdRevenue":{"type":"number","minimum":0,"description":"Umsatz seit Jahresbeginn in Euro, zwei Nachkommastellen"},"status":{"type":"string","enum":["active","inactive"],"description":"`active` = bewirtschaftet, `inactive` = stillgelegt (Ergebnis von DELETE)"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","code","name","region","primaryRep","supportReps","customers","ytdRevenue","status","createdAt","updatedAt"],"additionalProperties":false,"description":"Das betroffene Gebiet nach der Operation"}},"required":["territory"],"additionalProperties":false},"example":{"territory":{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","region":"string","primaryRep":"string","supportReps":["string"],"customers":0,"ytdRevenue":0,"status":"active","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `user`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Gebiet mit dieser Kennung — Klartext `not found`, kein JSON"},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"getApiV1TerritoriesById","tags":["Sales · Territories"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Gebiet Detail","description":"Liefert ein einzelnes Vertriebsgebiet anhand seiner UUID."},"put":{"responses":{"200":{"description":"Das Gebiet nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"territory":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Gebiets (UUID)"},"code":{"type":"string","minLength":2,"maxLength":40,"description":"Kurzzeichen des Gebiets, mandantenweit eindeutig (z. B. „NORD-01\")"},"name":{"type":"string","minLength":1,"maxLength":160,"description":"Bezeichnung des Gebiets"},"region":{"type":"string","minLength":1,"maxLength":80,"description":"Uebergeordnete Region, nach der die Liste gefiltert werden kann"},"primaryRep":{"type":"string","minLength":1,"maxLength":120,"description":"Hauptverantwortliche Vertriebskraft"},"supportReps":{"type":"array","items":{"type":"string","maxLength":120},"description":"Unterstuetzende Vertriebskraefte; leeres Feld, wenn keine hinterlegt sind"},"customers":{"type":"integer","minimum":0,"description":"Anzahl der dem Gebiet zugeordneten Kunden"},"ytdRevenue":{"type":"number","minimum":0,"description":"Umsatz seit Jahresbeginn in Euro, zwei Nachkommastellen"},"status":{"type":"string","enum":["active","inactive"],"description":"`active` = bewirtschaftet, `inactive` = stillgelegt (Ergebnis von DELETE)"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","code","name","region","primaryRep","supportReps","customers","ytdRevenue","status","createdAt","updatedAt"],"additionalProperties":false,"description":"Das betroffene Gebiet nach der Operation"}},"required":["territory"],"additionalProperties":false},"example":{"territory":{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","region":"string","primaryRep":"string","supportReps":["string"],"customers":0,"ytdRevenue":0,"status":"active","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"400":{"description":"Ungueltiger Wert im Rumpf (JSON nach dem Schema). Ein Rumpf ganz OHNE aenderbares Feld antwortet stattdessen mit Klartext `no fields to update`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed","description":"Feste Fehlerkennung fuer die aufrufende Maschine"},"message":{"type":"string","description":"Meldung im Klartext, direkt anzeigbar"},"fields":{"type":"array","items":{"type":"string"},"description":"Namen der beanstandeten Felder; verschachtelte Pfade mit Punkt getrennt (z. B. `address.zip`)"}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `manager`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Gebiet mit dieser Kennung — Klartext `not found`, kein JSON"},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"putApiV1TerritoriesById","tags":["Sales · Territories"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Gebiet aktualisieren","description":"Aendert einzelne Felder eines Gebiets. Nur mitgesendete Felder werden geschrieben; `updatedAt` setzt der Server selbst.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":2,"maxLength":40},"name":{"type":"string","minLength":1,"maxLength":160},"region":{"type":"string","minLength":1,"maxLength":80},"primaryRep":{"type":"string","minLength":1,"maxLength":120},"supportReps":{"type":"array","items":{"type":"string","maxLength":120},"default":[]},"customers":{"type":"integer","minimum":0,"default":0},"ytdRevenue":{"type":"number","minimum":0,"default":0},"status":{"type":"string","enum":["active","inactive"],"default":"active"}}},"example":{"code":"string","name":"string","region":"string","primaryRep":"string","supportReps":["string"],"customers":0,"ytdRevenue":0,"status":"active"}}}}},"delete":{"responses":{"200":{"description":"Das stillgelegte Gebiet, `status` ist danach `inactive`","content":{"application/json":{"schema":{"type":"object","properties":{"territory":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Gebiets (UUID)"},"code":{"type":"string","minLength":2,"maxLength":40,"description":"Kurzzeichen des Gebiets, mandantenweit eindeutig (z. B. „NORD-01\")"},"name":{"type":"string","minLength":1,"maxLength":160,"description":"Bezeichnung des Gebiets"},"region":{"type":"string","minLength":1,"maxLength":80,"description":"Uebergeordnete Region, nach der die Liste gefiltert werden kann"},"primaryRep":{"type":"string","minLength":1,"maxLength":120,"description":"Hauptverantwortliche Vertriebskraft"},"supportReps":{"type":"array","items":{"type":"string","maxLength":120},"description":"Unterstuetzende Vertriebskraefte; leeres Feld, wenn keine hinterlegt sind"},"customers":{"type":"integer","minimum":0,"description":"Anzahl der dem Gebiet zugeordneten Kunden"},"ytdRevenue":{"type":"number","minimum":0,"description":"Umsatz seit Jahresbeginn in Euro, zwei Nachkommastellen"},"status":{"type":"string","enum":["active","inactive"],"description":"`active` = bewirtschaftet, `inactive` = stillgelegt (Ergebnis von DELETE)"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt (ISO 8601, UTC)"},"updatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der letzten Aenderung (ISO 8601, UTC)"}},"required":["id","code","name","region","primaryRep","supportReps","customers","ytdRevenue","status","createdAt","updatedAt"],"additionalProperties":false,"description":"Das betroffene Gebiet nach der Operation"}},"required":["territory"],"additionalProperties":false},"example":{"territory":{"id":"00000000-0000-4000-8000-000000000000","code":"string","name":"string","region":"string","primaryRep":"string","supportReps":["string"],"customers":0,"ytdRevenue":0,"status":"active","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}}},"401":{"description":"Nicht angemeldet oder kein Mandantenkontext — Klartext, kein JSON"},"403":{"description":"Rolle unterhalb `manager`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden","description":"Feste Fehlerkennung"},"code":{"type":"string","const":"INSUFFICIENT_ROLE","description":"Maschinen-Code der Ablehnung"},"required":{"type":"string","description":"Mindestens noetige Rolle, z. B. `manager`"},"actual":{"type":"string","description":"Rolle des Aufrufers"},"message":{"type":"string","description":"Deutscher Klartext mit Hinweis, wer die Rolle vergeben kann"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Kein Gebiet mit dieser Kennung — Klartext `not found`, kein JSON"},"503":{"description":"Datenbank nicht erreichbar — Klartext, kein JSON"}},"operationId":"deleteApiV1TerritoriesById","tags":["Sales · Territories"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Gebiet inaktiv setzen","description":"Setzt `status` auf `inactive`. Der Datensatz bleibt erhalten und wird zurueckgegeben — es wird also nichts geloescht."}},"/api/v1/aufmass":{"get":{"responses":{"200":{"description":"Kopfdaten der Seite plus Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"aufmasse":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Aufmassblatts"},"number":{"type":"string","description":"Aufmassnummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung"},"lvId":{"type":"string","description":"Kennung des Leistungsverzeichnisses; leerer String, wenn keins verknuepft ist"},"lvNumber":{"type":"string","description":"Nummer des Leistungsverzeichnisses; leerer String, wenn nicht erfasst"},"measuredBy":{"type":"string","description":"Wer gemessen hat — BFA-Pflichtangabe"},"measuredAt":{"type":"string","description":"Aufmassdatum (YYYY-MM-DD) — BFA-Pflichtangabe"},"status":{"type":"string","enum":["draft","approved","invoiced"],"description":"draft, approved oder invoiced"},"totalQty":{"type":"number","description":"Gespeicherte Gesamtmenge: Summe aus menge × faktor ueber alle Positionen, auf drei Stellen gerundet"},"unit":{"type":"string","description":"Einheit der ERSTEN Position; leerer String ohne Positionen. Gemischte Einheiten werden NICHT getrennt"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","number","name","lvId","lvNumber","measuredBy","measuredAt","status","totalQty","unit","createdAt","updatedAt"]},"description":"Die Aufmassblaetter der Seite, neueste zuerst — OHNE Positionen"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Treffer der Filter, unabhaengig von limit/offset"}},"required":["aufmasse","total"]},"example":{"aufmasse":[{"id":"string","number":"string","name":"string","lvId":"string","lvNumber":"string","measuredBy":"string","measuredAt":"string","status":"draft","totalQty":0,"unit":"string","createdAt":"string","updatedAt":"string"}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Aufmass","tags":["Bau · Aufmaß"],"parameters":[{"in":"query","name":"lvId","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","approved","invoiced"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste Aufmaßblätter","description":"Blaettert durch `aufmasse` des Mandanten, neueste zuerst. `lvId` und `status` filtern exakt, `limit` (1-200, Vorgabe 50) und `offset` blaettern; `total` zaehlt alle Treffer der Filter, nicht nur die Seite. Die Positionen sind hier ABSICHTLICH nicht dabei — nur die aufsummierte Gesamtmenge. Die einzelnen Positionen liefert `GET /aufmass/{id}`."},"post":{"responses":{"201":{"description":"Das angelegte Aufmassblatt","content":{"application/json":{"schema":{"type":"object","properties":{"aufmass":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Aufmassblatts"},"number":{"type":"string","description":"Aufmassnummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung"},"lvId":{"type":"string","description":"Kennung des Leistungsverzeichnisses; leerer String, wenn keins verknuepft ist"},"lvNumber":{"type":"string","description":"Nummer des Leistungsverzeichnisses; leerer String, wenn nicht erfasst"},"measuredBy":{"type":"string","description":"Wer gemessen hat — BFA-Pflichtangabe"},"measuredAt":{"type":"string","description":"Aufmassdatum (YYYY-MM-DD) — BFA-Pflichtangabe"},"status":{"type":"string","enum":["draft","approved","invoiced"],"description":"draft, approved oder invoiced"},"totalQty":{"type":"number","description":"Gespeicherte Gesamtmenge: Summe aus menge × faktor ueber alle Positionen, auf drei Stellen gerundet"},"unit":{"type":"string","description":"Einheit der ERSTEN Position; leerer String ohne Positionen. Gemischte Einheiten werden NICHT getrennt"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zeitstempel)"},"lvPositionNr":{"type":"string","description":"Verweis auf die Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","description":"Bezeichnung der gemessenen Leistung"},"menge":{"type":"number","description":"Gemessene Menge"},"faktor":{"type":"number","description":"Faktor auf die Menge; ohne Angabe 1"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"m2\""},"notiz":{"type":["string","null"],"description":"Bemerkung; fehlt oder null, wenn keine erfasst ist"}},"required":["id","lvPositionNr","bezeichnung","menge","faktor","einheit"]},"description":"Die Positionen in Eingabereihenfolge"}},"required":["id","number","name","lvId","lvNumber","measuredBy","measuredAt","status","totalQty","unit","createdAt","updatedAt","positionen"]}},"required":["aufmass"]},"example":{"aufmass":{"id":"string","number":"string","name":"string","lvId":"string","lvNumber":"string","measuredBy":"string","measuredAt":"string","status":"draft","totalQty":0,"unit":"string","createdAt":"string","updatedAt":"string","positionen":[{"id":"string","lvPositionNr":"string","bezeichnung":"string","menge":0,"faktor":0,"einheit":"string","notiz":"string"}]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Anlegen fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Aufmass","tags":["Bau · Aufmaß"],"parameters":[],"summary":"Aufmaßblatt anlegen","description":"Legt ein Aufmassblatt an. `measuredBy` und `measuredAt` sind Pflicht (BFA). Mitgegebene Positionen bekommen ihre Kennung hier vergeben; aus ihnen werden Gesamtmenge (Summe menge × faktor) und Einheit (die der ERSTEN Position) gerechnet und mitgespeichert. Ohne `status` gilt `draft`. `project_id` bleibt bei dieser Route immer NULL. Die Aufmassnummer wird NICHT vergeben und NICHT auf Eindeutigkeit geprueft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string","minLength":1,"maxLength":40},"name":{"type":"string","minLength":1,"maxLength":200},"lvId":{"type":"string","minLength":1,"maxLength":64},"lvNumber":{"type":"string","minLength":1,"maxLength":40},"measuredBy":{"type":"string","minLength":1,"maxLength":120},"measuredAt":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","approved","invoiced"],"default":"draft"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"lvPositionNr":{"type":"string","minLength":1,"maxLength":40},"bezeichnung":{"type":"string","minLength":1,"maxLength":400},"menge":{"type":"number","minimum":0},"faktor":{"type":"number","minimum":0,"default":1},"einheit":{"type":"string","minLength":1,"maxLength":10},"notiz":{"type":["string","null"]}},"required":["lvPositionNr","bezeichnung","menge","einheit"]},"default":[]}},"required":["number","name","lvId","lvNumber","measuredBy","measuredAt"]},"example":{"number":"string","name":"string","lvId":"string","lvNumber":"string","measuredBy":"string","measuredAt":"2026-01-01","status":"draft","positionen":[{"id":"string","lvPositionNr":"string","bezeichnung":"string","menge":0,"faktor":0,"einheit":"string","notiz":"string"}]}}}}}},"/api/v1/aufmass/{id}":{"get":{"responses":{"200":{"description":"Das Aufmassblatt mit Positionen","content":{"application/json":{"schema":{"type":"object","properties":{"aufmass":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Aufmassblatts"},"number":{"type":"string","description":"Aufmassnummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung"},"lvId":{"type":"string","description":"Kennung des Leistungsverzeichnisses; leerer String, wenn keins verknuepft ist"},"lvNumber":{"type":"string","description":"Nummer des Leistungsverzeichnisses; leerer String, wenn nicht erfasst"},"measuredBy":{"type":"string","description":"Wer gemessen hat — BFA-Pflichtangabe"},"measuredAt":{"type":"string","description":"Aufmassdatum (YYYY-MM-DD) — BFA-Pflichtangabe"},"status":{"type":"string","enum":["draft","approved","invoiced"],"description":"draft, approved oder invoiced"},"totalQty":{"type":"number","description":"Gespeicherte Gesamtmenge: Summe aus menge × faktor ueber alle Positionen, auf drei Stellen gerundet"},"unit":{"type":"string","description":"Einheit der ERSTEN Position; leerer String ohne Positionen. Gemischte Einheiten werden NICHT getrennt"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zeitstempel)"},"lvPositionNr":{"type":"string","description":"Verweis auf die Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","description":"Bezeichnung der gemessenen Leistung"},"menge":{"type":"number","description":"Gemessene Menge"},"faktor":{"type":"number","description":"Faktor auf die Menge; ohne Angabe 1"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"m2\""},"notiz":{"type":["string","null"],"description":"Bemerkung; fehlt oder null, wenn keine erfasst ist"}},"required":["id","lvPositionNr","bezeichnung","menge","faktor","einheit"]},"description":"Die Positionen in Eingabereihenfolge"}},"required":["id","number","name","lvId","lvNumber","measuredBy","measuredAt","status","totalQty","unit","createdAt","updatedAt","positionen"]}},"required":["aufmass"]},"example":{"aufmass":{"id":"string","number":"string","name":"string","lvId":"string","lvNumber":"string","measuredBy":"string","measuredAt":"string","status":"draft","totalQty":0,"unit":"string","createdAt":"string","updatedAt":"string","positionen":[{"id":"string","lvPositionNr":"string","bezeichnung":"string","menge":0,"faktor":0,"einheit":"string","notiz":"string"}]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Aufmassblatt mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1AufmassById","tags":["Bau · Aufmaß"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aufmaß Detail","description":"Liefert EIN Aufmassblatt samt aller Positionen aus dem JSONB-Feld `positionen`. `totalQty` ist die gespeicherte Gesamtmenge, nicht aus den Positionen neu gerechnet."},"put":{"responses":{"200":{"description":"Das Aufmassblatt nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"aufmass":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Aufmassblatts"},"number":{"type":"string","description":"Aufmassnummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung"},"lvId":{"type":"string","description":"Kennung des Leistungsverzeichnisses; leerer String, wenn keins verknuepft ist"},"lvNumber":{"type":"string","description":"Nummer des Leistungsverzeichnisses; leerer String, wenn nicht erfasst"},"measuredBy":{"type":"string","description":"Wer gemessen hat — BFA-Pflichtangabe"},"measuredAt":{"type":"string","description":"Aufmassdatum (YYYY-MM-DD) — BFA-Pflichtangabe"},"status":{"type":"string","enum":["draft","approved","invoiced"],"description":"draft, approved oder invoiced"},"totalQty":{"type":"number","description":"Gespeicherte Gesamtmenge: Summe aus menge × faktor ueber alle Positionen, auf drei Stellen gerundet"},"unit":{"type":"string","description":"Einheit der ERSTEN Position; leerer String ohne Positionen. Gemischte Einheiten werden NICHT getrennt"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zeitstempel)"},"lvPositionNr":{"type":"string","description":"Verweis auf die Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","description":"Bezeichnung der gemessenen Leistung"},"menge":{"type":"number","description":"Gemessene Menge"},"faktor":{"type":"number","description":"Faktor auf die Menge; ohne Angabe 1"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"m2\""},"notiz":{"type":["string","null"],"description":"Bemerkung; fehlt oder null, wenn keine erfasst ist"}},"required":["id","lvPositionNr","bezeichnung","menge","faktor","einheit"]},"description":"Die Positionen in Eingabereihenfolge"}},"required":["id","number","name","lvId","lvNumber","measuredBy","measuredAt","status","totalQty","unit","createdAt","updatedAt","positionen"]}},"required":["aufmass"]},"example":{"aufmass":{"id":"string","number":"string","name":"string","lvId":"string","lvNumber":"string","measuredBy":"string","measuredAt":"string","status":"draft","totalQty":0,"unit":"string","createdAt":"string","updatedAt":"string","positionen":[{"id":"string","lvPositionNr":"string","bezeichnung":"string","menge":0,"faktor":0,"einheit":"string","notiz":"string"}]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Aufmassblatt mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"503":{"description":"Aenderung fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"putApiV1AufmassById","tags":["Bau · Aufmaß"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Header aktualisieren","description":"Aendert nur die Kopfdaten (number, name, lvId, lvNumber, measuredBy, measuredAt, status) — jedes Feld ist freiwillig. Positionen, Gesamtmenge und Einheit bleiben unberuehrt; dafuer gibt es `POST /aufmass/{id}/positionen`. Auch `status` laesst sich hier setzen, also auch auf `approved` ohne die Rollenpruefung von `POST /aufmass/{id}/freigeben`. Ein leerer Rumpf schreibt NICHT und gibt den Stand unveraendert zurueck.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string","minLength":1,"maxLength":40},"name":{"type":"string","minLength":1,"maxLength":200},"lvId":{"type":"string","minLength":1,"maxLength":64},"lvNumber":{"type":"string","minLength":1,"maxLength":40},"measuredBy":{"type":"string","minLength":1,"maxLength":120},"measuredAt":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","approved","invoiced"],"default":"draft"}}},"example":{"number":"string","name":"string","lvId":"string","lvNumber":"string","measuredBy":"string","measuredAt":"2026-01-01","status":"draft"}}}}}},"/api/v1/aufmass/{id}/positionen":{"post":{"responses":{"200":{"description":"Das Aufmassblatt nach dem Anhaengen plus die neue Position","content":{"application/json":{"schema":{"type":"object","properties":{"aufmass":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Aufmassblatts"},"number":{"type":"string","description":"Aufmassnummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung"},"lvId":{"type":"string","description":"Kennung des Leistungsverzeichnisses; leerer String, wenn keins verknuepft ist"},"lvNumber":{"type":"string","description":"Nummer des Leistungsverzeichnisses; leerer String, wenn nicht erfasst"},"measuredBy":{"type":"string","description":"Wer gemessen hat — BFA-Pflichtangabe"},"measuredAt":{"type":"string","description":"Aufmassdatum (YYYY-MM-DD) — BFA-Pflichtangabe"},"status":{"type":"string","enum":["draft","approved","invoiced"],"description":"draft, approved oder invoiced"},"totalQty":{"type":"number","description":"Gespeicherte Gesamtmenge: Summe aus menge × faktor ueber alle Positionen, auf drei Stellen gerundet"},"unit":{"type":"string","description":"Einheit der ERSTEN Position; leerer String ohne Positionen. Gemischte Einheiten werden NICHT getrennt"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zeitstempel)"},"lvPositionNr":{"type":"string","description":"Verweis auf die Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","description":"Bezeichnung der gemessenen Leistung"},"menge":{"type":"number","description":"Gemessene Menge"},"faktor":{"type":"number","description":"Faktor auf die Menge; ohne Angabe 1"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"m2\""},"notiz":{"type":["string","null"],"description":"Bemerkung; fehlt oder null, wenn keine erfasst ist"}},"required":["id","lvPositionNr","bezeichnung","menge","faktor","einheit"]},"description":"Die Positionen in Eingabereihenfolge"}},"required":["id","number","name","lvId","lvNumber","measuredBy","measuredAt","status","totalQty","unit","createdAt","updatedAt","positionen"],"description":"Das Aufmassblatt nach dem Anhaengen, mit neuer Gesamtmenge"},"position":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zeitstempel)"},"lvPositionNr":{"type":"string","description":"Verweis auf die Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","description":"Bezeichnung der gemessenen Leistung"},"menge":{"type":"number","description":"Gemessene Menge"},"faktor":{"type":"number","description":"Faktor auf die Menge; ohne Angabe 1"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"m2\""},"notiz":{"type":["string","null"],"description":"Bemerkung; fehlt oder null, wenn keine erfasst ist"}},"required":["id","lvPositionNr","bezeichnung","menge","faktor","einheit"],"description":"Die angelegte Position mit vergebener Kennung"}},"required":["aufmass","position"]},"example":{"aufmass":{"id":"string","number":"string","name":"string","lvId":"string","lvNumber":"string","measuredBy":"string","measuredAt":"string","status":"draft","totalQty":0,"unit":"string","createdAt":"string","updatedAt":"string","positionen":[{"id":"string","lvPositionNr":"string","bezeichnung":"string","menge":0,"faktor":0,"einheit":"string","notiz":"string"}]},"position":{"id":"string","lvPositionNr":"string","bezeichnung":"string","menge":0,"faktor":0,"einheit":"string","notiz":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Aufmassblatt mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"503":{"description":"Anhaengen fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1AufmassByIdPositionen","tags":["Bau · Aufmaß"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Position hinzufügen","description":"Haengt EINE Position ans Ende des JSONB-Feldes `positionen` und schreibt Gesamtmenge und Einheit neu. Die Kennung vergibt der Server. Lesen und Schreiben laufen als ZWEI Anweisungen ohne Transaktion — zwei gleichzeitige Aufrufe koennen sich gegenseitig ueberschreiben. Der Status wird NICHT geprueft: auch ein bereits freigegebenes oder abgerechnetes Aufmass nimmt weitere Positionen an. Antwortet mit dem vollstaendigen Aufmassblatt UND der neuen Position einzeln.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lvPositionNr":{"type":"string","minLength":1,"maxLength":40},"bezeichnung":{"type":"string","minLength":1,"maxLength":400},"menge":{"type":"number","minimum":0},"faktor":{"type":"number","minimum":0,"default":1},"einheit":{"type":"string","minLength":1,"maxLength":10},"notiz":{"type":["string","null"]}},"required":["lvPositionNr","bezeichnung","menge","einheit"]},"example":{"lvPositionNr":"string","bezeichnung":"string","menge":0,"faktor":0,"einheit":"string","notiz":"string"}}}}}},"/api/v1/aufmass/{id}/freigeben":{"post":{"responses":{"200":{"description":"Das Aufmassblatt mit Status approved","content":{"application/json":{"schema":{"type":"object","properties":{"aufmass":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Aufmassblatts"},"number":{"type":"string","description":"Aufmassnummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung"},"lvId":{"type":"string","description":"Kennung des Leistungsverzeichnisses; leerer String, wenn keins verknuepft ist"},"lvNumber":{"type":"string","description":"Nummer des Leistungsverzeichnisses; leerer String, wenn nicht erfasst"},"measuredBy":{"type":"string","description":"Wer gemessen hat — BFA-Pflichtangabe"},"measuredAt":{"type":"string","description":"Aufmassdatum (YYYY-MM-DD) — BFA-Pflichtangabe"},"status":{"type":"string","enum":["draft","approved","invoiced"],"description":"draft, approved oder invoiced"},"totalQty":{"type":"number","description":"Gespeicherte Gesamtmenge: Summe aus menge × faktor ueber alle Positionen, auf drei Stellen gerundet"},"unit":{"type":"string","description":"Einheit der ERSTEN Position; leerer String ohne Positionen. Gemischte Einheiten werden NICHT getrennt"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zeitstempel)"},"lvPositionNr":{"type":"string","description":"Verweis auf die Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","description":"Bezeichnung der gemessenen Leistung"},"menge":{"type":"number","description":"Gemessene Menge"},"faktor":{"type":"number","description":"Faktor auf die Menge; ohne Angabe 1"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"m2\""},"notiz":{"type":["string","null"],"description":"Bemerkung; fehlt oder null, wenn keine erfasst ist"}},"required":["id","lvPositionNr","bezeichnung","menge","faktor","einheit"]},"description":"Die Positionen in Eingabereihenfolge"}},"required":["id","number","name","lvId","lvNumber","measuredBy","measuredAt","status","totalQty","unit","createdAt","updatedAt","positionen"]}},"required":["aufmass"]},"example":{"aufmass":{"id":"string","number":"string","name":"string","lvId":"string","lvNumber":"string","measuredBy":"string","measuredAt":"string","status":"draft","totalQty":0,"unit":"string","createdAt":"string","updatedAt":"string","positionen":[{"id":"string","lvPositionNr":"string","bezeichnung":"string","menge":0,"faktor":0,"einheit":"string","notiz":"string"}]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Aufmassblatt mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"503":{"description":"Freigabe fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1AufmassByIdFreigeben","tags":["Bau · Aufmaß"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aufmaß freigeben","description":"Setzt den Status hart auf `approved` und aktualisiert `updated_at`. Erfordert mindestens die Rolle `manager`. Der Aufruf ist wiederholbar und prueft den Vorzustand NICHT — auch ein bereits abgerechnetes Aufmass (`invoiced`) faellt damit auf `approved` zurueck. Der Rumpf wird nicht gelesen, es gibt keine Gegenbewegung (kein „zurueckziehen\") und es wird kein Ereignis ausgeloest."}},"/api/v1/lv":{"get":{"responses":{"200":{"description":"Kopfdaten der Seite plus Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"lvs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Leistungsverzeichnisses"},"number":{"type":"string","description":"LV-Nummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung (Spalte `titel`)"},"objectName":{"type":"string","description":"Bauvorhaben; leerer String, wenn nicht erfasst"},"gewerk":{"type":"string","description":"Gewerk"},"status":{"type":"string","enum":["draft","sent","awarded","completed","cancelled"],"description":"draft, sent, awarded, completed oder cancelled"},"positionsCount":{"type":"integer","minimum":0,"description":"Anzahl Positionen"},"totalGross":{"type":"number","description":"Summe BRUTTO in EUR — Netto zuzueglich 19 % Umsatzsteuer"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"}},"required":["id","number","name","objectName","gewerk","status","positionsCount","totalGross","createdAt","updatedAt"]},"description":"Die Leistungsverzeichnisse der Seite, neueste zuerst — OHNE Positionen"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Treffer der Filter, unabhaengig von limit/offset"}},"required":["lvs","total"]},"example":{"lvs":[{"id":"string","number":"string","name":"string","objectName":"string","gewerk":"string","status":"draft","positionsCount":0,"totalGross":0,"createdAt":"string","updatedAt":"string"}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Lv","tags":["Bau · LV"],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["draft","sent","awarded","completed","cancelled"]}},{"in":"query","name":"gewerk","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste LVs","description":"Blaettert durch `leistungsverzeichnisse` des Mandanten, neueste zuerst. `status` und `gewerk` filtern exakt, `limit` (1-200, Vorgabe 50) und `offset` blaettern; `total` zaehlt alle Treffer der Filter, nicht nur die Seite. Die Positionen sind hier ABSICHTLICH nicht dabei — nur ihre Anzahl. Die vollstaendige Liste liefert `GET /lv/{id}`."},"post":{"responses":{"201":{"description":"Das angelegte Leistungsverzeichnis","content":{"application/json":{"schema":{"type":"object","properties":{"lv":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Leistungsverzeichnisses"},"number":{"type":"string","description":"LV-Nummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung (Spalte `titel`)"},"objectName":{"type":"string","description":"Bauvorhaben; leerer String, wenn nicht erfasst"},"gewerk":{"type":"string","description":"Gewerk"},"status":{"type":"string","enum":["draft","sent","awarded","completed","cancelled"],"description":"draft, sent, awarded, completed oder cancelled"},"positionsCount":{"type":"integer","minimum":0,"description":"Anzahl Positionen"},"totalGross":{"type":"number","description":"Summe BRUTTO in EUR — Netto zuzueglich 19 % Umsatzsteuer"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zufall)"},"positionNr":{"type":"string","description":"Ordnungszahl, z. B. \"01.01.010\""},"kurztext":{"type":"string","description":"Kurztext der Position"},"langtext":{"type":["string","null"],"description":"Langtext; fehlt oder null, wenn keiner erfasst ist"},"menge":{"type":"number","description":"Menge"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"St\" oder \"m2\""},"ep":{"type":"number","description":"Einheitspreis in EUR, netto"},"gp":{"type":"number","description":"Gesamtpreis netto = menge × ep, auf zwei Stellen gerundet"}},"required":["id","positionNr","kurztext","menge","einheit","ep","gp"]},"description":"Die Positionen in Eingabereihenfolge"}},"required":["id","number","name","objectName","gewerk","status","positionsCount","totalGross","createdAt","updatedAt","positionen"]}},"required":["lv"]},"example":{"lv":{"id":"string","number":"string","name":"string","objectName":"string","gewerk":"string","status":"draft","positionsCount":0,"totalGross":0,"createdAt":"string","updatedAt":"string","positionen":[{"id":"string","positionNr":"string","kurztext":"string","langtext":"string","menge":0,"einheit":"string","ep":0,"gp":0}]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Anlegen fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1Lv","tags":["Bau · LV"],"parameters":[],"summary":"LV anlegen","description":"Legt ein Leistungsverzeichnis an. Mitgegebene Positionen bekommen ihre Kennung und ihren Gesamtpreis (`gp` = menge × ep) hier vergeben; die Summe `summe` wird daraus mit 19 % Umsatzsteuer gerechnet und mitgespeichert. Ohne `status` gilt `draft`, ohne `positionen` eine leere Liste. Die LV-Nummer wird NICHT vergeben und NICHT auf Eindeutigkeit geprueft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string","minLength":1,"maxLength":40},"name":{"type":"string","minLength":1,"maxLength":200},"objectName":{"type":"string","minLength":1,"maxLength":200},"gewerk":{"type":"string","minLength":1,"maxLength":80},"status":{"type":"string","enum":["draft","sent","awarded","completed","cancelled"],"default":"draft"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"positionNr":{"type":"string","minLength":1,"maxLength":40},"kurztext":{"type":"string","minLength":1,"maxLength":400},"langtext":{"type":["string","null"]},"menge":{"type":"number","minimum":0},"einheit":{"type":"string","minLength":1,"maxLength":10},"ep":{"type":"number","minimum":0}},"required":["positionNr","kurztext","menge","einheit","ep"]},"default":[]}},"required":["number","name","objectName","gewerk"]},"example":{"number":"string","name":"string","objectName":"string","gewerk":"string","status":"draft","positionen":[{"id":"string","positionNr":"string","kurztext":"string","langtext":"string","menge":0,"einheit":"string","ep":0}]}}}}}},"/api/v1/lv/{id}":{"get":{"responses":{"200":{"description":"Das Leistungsverzeichnis mit Positionen","content":{"application/json":{"schema":{"type":"object","properties":{"lv":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Leistungsverzeichnisses"},"number":{"type":"string","description":"LV-Nummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung (Spalte `titel`)"},"objectName":{"type":"string","description":"Bauvorhaben; leerer String, wenn nicht erfasst"},"gewerk":{"type":"string","description":"Gewerk"},"status":{"type":"string","enum":["draft","sent","awarded","completed","cancelled"],"description":"draft, sent, awarded, completed oder cancelled"},"positionsCount":{"type":"integer","minimum":0,"description":"Anzahl Positionen"},"totalGross":{"type":"number","description":"Summe BRUTTO in EUR — Netto zuzueglich 19 % Umsatzsteuer"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zufall)"},"positionNr":{"type":"string","description":"Ordnungszahl, z. B. \"01.01.010\""},"kurztext":{"type":"string","description":"Kurztext der Position"},"langtext":{"type":["string","null"],"description":"Langtext; fehlt oder null, wenn keiner erfasst ist"},"menge":{"type":"number","description":"Menge"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"St\" oder \"m2\""},"ep":{"type":"number","description":"Einheitspreis in EUR, netto"},"gp":{"type":"number","description":"Gesamtpreis netto = menge × ep, auf zwei Stellen gerundet"}},"required":["id","positionNr","kurztext","menge","einheit","ep","gp"]},"description":"Die Positionen in Eingabereihenfolge"}},"required":["id","number","name","objectName","gewerk","status","positionsCount","totalGross","createdAt","updatedAt","positionen"]}},"required":["lv"]},"example":{"lv":{"id":"string","number":"string","name":"string","objectName":"string","gewerk":"string","status":"draft","positionsCount":0,"totalGross":0,"createdAt":"string","updatedAt":"string","positionen":[{"id":"string","positionNr":"string","kurztext":"string","langtext":"string","menge":0,"einheit":"string","ep":0,"gp":0}]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Leistungsverzeichnis mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1LvById","tags":["Bau · LV"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"LV Detail","description":"Liefert EIN Leistungsverzeichnis samt aller Positionen aus dem JSONB-Feld `positionen`. `totalGross` ist die gespeicherte Summe brutto (netto plus 19 % Umsatzsteuer), nicht aus den Positionen neu gerechnet."},"put":{"responses":{"200":{"description":"Das Leistungsverzeichnis nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"lv":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Leistungsverzeichnisses"},"number":{"type":"string","description":"LV-Nummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung (Spalte `titel`)"},"objectName":{"type":"string","description":"Bauvorhaben; leerer String, wenn nicht erfasst"},"gewerk":{"type":"string","description":"Gewerk"},"status":{"type":"string","enum":["draft","sent","awarded","completed","cancelled"],"description":"draft, sent, awarded, completed oder cancelled"},"positionsCount":{"type":"integer","minimum":0,"description":"Anzahl Positionen"},"totalGross":{"type":"number","description":"Summe BRUTTO in EUR — Netto zuzueglich 19 % Umsatzsteuer"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zufall)"},"positionNr":{"type":"string","description":"Ordnungszahl, z. B. \"01.01.010\""},"kurztext":{"type":"string","description":"Kurztext der Position"},"langtext":{"type":["string","null"],"description":"Langtext; fehlt oder null, wenn keiner erfasst ist"},"menge":{"type":"number","description":"Menge"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"St\" oder \"m2\""},"ep":{"type":"number","description":"Einheitspreis in EUR, netto"},"gp":{"type":"number","description":"Gesamtpreis netto = menge × ep, auf zwei Stellen gerundet"}},"required":["id","positionNr","kurztext","menge","einheit","ep","gp"]},"description":"Die Positionen in Eingabereihenfolge"}},"required":["id","number","name","objectName","gewerk","status","positionsCount","totalGross","createdAt","updatedAt","positionen"]}},"required":["lv"]},"example":{"lv":{"id":"string","number":"string","name":"string","objectName":"string","gewerk":"string","status":"draft","positionsCount":0,"totalGross":0,"createdAt":"string","updatedAt":"string","positionen":[{"id":"string","positionNr":"string","kurztext":"string","langtext":"string","menge":0,"einheit":"string","ep":0,"gp":0}]}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Leistungsverzeichnis mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"503":{"description":"Aenderung fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"putApiV1LvById","tags":["Bau · LV"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"LV Header aktualisieren","description":"Aendert nur die Kopfdaten (number, name, objectName, gewerk, status) — jedes Feld ist freiwillig, mitgegebene werden gesetzt. Positionen und Summe bleiben unberuehrt; dafuer gibt es `POST /lv/{id}/positionen`. Ein leerer Rumpf schreibt NICHT und gibt den Stand unveraendert zurueck — auch `updated_at` bleibt dann stehen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string","minLength":1,"maxLength":40},"name":{"type":"string","minLength":1,"maxLength":200},"objectName":{"type":"string","minLength":1,"maxLength":200},"gewerk":{"type":"string","minLength":1,"maxLength":80},"status":{"type":"string","enum":["draft","sent","awarded","completed","cancelled"],"default":"draft"}}},"example":{"number":"string","name":"string","objectName":"string","gewerk":"string","status":"draft"}}}}}},"/api/v1/lv/{id}/positionen":{"post":{"responses":{"200":{"description":"Das Leistungsverzeichnis nach dem Anhaengen plus die neue Position","content":{"application/json":{"schema":{"type":"object","properties":{"lv":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Leistungsverzeichnisses"},"number":{"type":"string","description":"LV-Nummer (Spalte `nummer`)"},"name":{"type":"string","description":"Bezeichnung (Spalte `titel`)"},"objectName":{"type":"string","description":"Bauvorhaben; leerer String, wenn nicht erfasst"},"gewerk":{"type":"string","description":"Gewerk"},"status":{"type":"string","enum":["draft","sent","awarded","completed","cancelled"],"description":"draft, sent, awarded, completed oder cancelled"},"positionsCount":{"type":"integer","minimum":0,"description":"Anzahl Positionen"},"totalGross":{"type":"number","description":"Summe BRUTTO in EUR — Netto zuzueglich 19 % Umsatzsteuer"},"createdAt":{"type":"string","description":"Anlagezeitpunkt"},"updatedAt":{"type":"string","description":"Letzte Aenderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zufall)"},"positionNr":{"type":"string","description":"Ordnungszahl, z. B. \"01.01.010\""},"kurztext":{"type":"string","description":"Kurztext der Position"},"langtext":{"type":["string","null"],"description":"Langtext; fehlt oder null, wenn keiner erfasst ist"},"menge":{"type":"number","description":"Menge"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"St\" oder \"m2\""},"ep":{"type":"number","description":"Einheitspreis in EUR, netto"},"gp":{"type":"number","description":"Gesamtpreis netto = menge × ep, auf zwei Stellen gerundet"}},"required":["id","positionNr","kurztext","menge","einheit","ep","gp"]},"description":"Die Positionen in Eingabereihenfolge"}},"required":["id","number","name","objectName","gewerk","status","positionsCount","totalGross","createdAt","updatedAt","positionen"],"description":"Das Leistungsverzeichnis nach dem Anhaengen, mit neuer Summe"},"position":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zufall)"},"positionNr":{"type":"string","description":"Ordnungszahl, z. B. \"01.01.010\""},"kurztext":{"type":"string","description":"Kurztext der Position"},"langtext":{"type":["string","null"],"description":"Langtext; fehlt oder null, wenn keiner erfasst ist"},"menge":{"type":"number","description":"Menge"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"St\" oder \"m2\""},"ep":{"type":"number","description":"Einheitspreis in EUR, netto"},"gp":{"type":"number","description":"Gesamtpreis netto = menge × ep, auf zwei Stellen gerundet"}},"required":["id","positionNr","kurztext","menge","einheit","ep","gp"],"description":"Die angelegte Position mit vergebener Kennung und gerechnetem gp"}},"required":["lv","position"]},"example":{"lv":{"id":"string","number":"string","name":"string","objectName":"string","gewerk":"string","status":"draft","positionsCount":0,"totalGross":0,"createdAt":"string","updatedAt":"string","positionen":[{"id":"string","positionNr":"string","kurztext":"string","langtext":"string","menge":0,"einheit":"string","ep":0,"gp":0}]},"position":{"id":"string","positionNr":"string","kurztext":"string","langtext":"string","menge":0,"einheit":"string","ep":0,"gp":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Leistungsverzeichnis mit dieser Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"503":{"description":"Anhaengen fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"postApiV1LvByIdPositionen","tags":["Bau · LV"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Position hinzufügen","description":"Haengt EINE Position ans Ende des JSONB-Feldes `positionen` und schreibt die neue Summe (alle Positionen netto, zuzueglich 19 % Umsatzsteuer) zurueck. Kennung und `gp` vergibt der Server. Lesen und Schreiben laufen als ZWEI Anweisungen ohne Transaktion — zwei gleichzeitige Aufrufe koennen sich gegenseitig ueberschreiben. Antwortet mit dem vollstaendigen Leistungsverzeichnis UND der neuen Position einzeln.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"positionNr":{"type":"string","minLength":1,"maxLength":40},"kurztext":{"type":"string","minLength":1,"maxLength":400},"langtext":{"type":["string","null"]},"menge":{"type":"number","minimum":0},"einheit":{"type":"string","minLength":1,"maxLength":10},"ep":{"type":"number","minimum":0}},"required":["positionNr","kurztext","menge","einheit","ep"]},"example":{"positionNr":"string","kurztext":"string","langtext":"string","menge":0,"einheit":"string","ep":0}}}}}},"/api/v1/lv/import-gaeb":{"post":{"responses":{"202":{"description":"Immer dieselbe Antwort — nichts wurde entgegengenommen","content":{"application/json":{"schema":{"type":"object","properties":{"accepted":{"type":"boolean","const":true,"description":"Immer true — der Endpunkt nimmt nichts entgegen und verarbeitet nichts"},"message":{"type":"string","description":"Hinweis auf den Endpunkt, der den Upload wirklich verarbeitet"},"suggestedEndpoint":{"type":"string","description":"Pfad des echten Import-Endpunkts"}},"required":["accepted","message","suggestedEndpoint"]},"example":{"accepted":true,"message":"string","suggestedEndpoint":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1LvImport-gaeb","tags":["Bau · LV"],"parameters":[],"summary":"GAEB-Import (Stub — siehe /api/v1/gaeb-lv für vollständige Pipeline)","description":"ATTRAPPE. Der Endpunkt liest den Rumpf NICHT, legt nichts an und beruehrt die Datenbank nicht. Er antwortet immer mit derselben 202 und verweist auf `POST /api/v1/gaeb`, wo der Upload wirklich verarbeitet wird. Er ist da, damit die Oberflaeche vollstaendig ist — nicht, damit ein Aufrufer sich darauf verlaesst."}},"/api/v1/lv/ai-generate":{"post":{"responses":{"200":{"description":"Skelett-Positionen ohne Preise; nichts wurde gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"generated":{"type":"boolean","const":true},"gewerk":{"type":"string","description":"Das uebergebene Gewerk, unveraendert zurueckgegeben"},"positionen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung der Position; ohne Vorgabe vergeben (`p_` + Zufall)"},"positionNr":{"type":"string","description":"Ordnungszahl, z. B. \"01.01.010\""},"kurztext":{"type":"string","description":"Kurztext der Position"},"langtext":{"type":["string","null"],"description":"Langtext; fehlt oder null, wenn keiner erfasst ist"},"menge":{"type":"number","description":"Menge"},"einheit":{"type":"string","description":"Mengeneinheit, z. B. \"St\" oder \"m2\""},"ep":{"type":"number","description":"Einheitspreis in EUR, netto"},"gp":{"type":"number","description":"Gesamtpreis netto = menge × ep, auf zwei Stellen gerundet"}},"required":["id","positionNr","kurztext","menge","einheit","ep","gp"]},"description":"Skelett-Positionen mit ep=0 und gp=0 — hoechstens acht, unabhaengig von targetCount"},"hint":{"type":"string","description":"Hinweis, dass das echte Modell noch nicht verdrahtet ist"}},"required":["generated","gewerk","positionen","hint"]},"example":{"generated":true,"gewerk":"string","positionen":[{"id":"string","positionNr":"string","kurztext":"string","langtext":"string","menge":0,"einheit":"string","ep":0,"gp":0}],"hint":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1LvAi-generate","tags":["Bau · LV"],"parameters":[],"summary":"AI-LV aus Beschreibung generieren (Stub)","description":"ATTRAPPE. Es laeuft KEIN Sprachmodell und es wird nichts gespeichert. Der Endpunkt baut aus dem Gewerk hoechstens ACHT Skelett-Positionen — unabhaengig von `targetCount`, das bis 200 zulaesst — mit Menge 10, Einheit \"St\" sowie `ep` und `gp` auf 0. Die Beschreibung landet nur gekuerzt im Langtext. Erfordert mindestens die Rolle `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string","minLength":20,"maxLength":8000},"gewerk":{"type":"string","minLength":1,"maxLength":80},"targetCount":{"type":"integer","minimum":1,"maximum":200,"default":20}},"required":["description","gewerk"]},"example":{"description":"stringxxxxxxxxxxxxxx","gewerk":"string","targetCount":1}}}}}},"/api/v1/bau-abrechnung":{"get":{"responses":{"200":{"description":"Die passenden Abrechnungen samt Gesamtzahl","content":{"application/json":{"schema":{"type":"object","properties":{"abrechnungen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"typ":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"lvId":{"type":"string"},"aufmassIds":{"type":"array","items":{"type":"string"}},"netto":{"type":"number"},"ustSatz":{"type":"number"},"ustBetrag":{"type":"number"},"brutto":{"type":"number"},"sicherheitsEinbehaltProzent":{"type":"number"},"sicherheitsEinbehaltBetrag":{"type":"number"},"zahlbetrag":{"type":"number"},"faellig":{"type":"string"},"status":{"type":"string"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","number","typ","customerId","customerName","lvId","aufmassIds","netto","ustSatz","ustBetrag","brutto","sicherheitsEinbehaltProzent","sicherheitsEinbehaltBetrag","zahlbetrag","faellig","status","notizen","createdAt","updatedAt"],"additionalProperties":false}},"total":{"type":"number"}},"required":["abrechnungen","total"],"additionalProperties":false},"example":{"abrechnungen":[{"id":"string","number":"string","typ":"string","customerId":"string","customerName":"string","lvId":"string","aufmassIds":["string"],"netto":0,"ustSatz":0,"ustBetrag":0,"brutto":0,"sicherheitsEinbehaltProzent":0,"sicherheitsEinbehaltBetrag":0,"zahlbetrag":0,"faellig":"string","status":"string","notizen":"string","createdAt":"string","updatedAt":"string"}],"total":0}}}},"400":{"description":"Ungueltige Query-Parameter"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Bau-abrechnung","tags":["Bau · Abrechnung"],"parameters":[{"in":"query","name":"typ","schema":{"type":"string","enum":["abschlag","schluss"]}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","approved","sent","paid"]}},{"in":"query","name":"lvId","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste Bau-Abrechnungen","description":"Liest `bau_abrechnungen` des Mandanten, neueste zuerst. Filtert wahlweise nach `typ` (`abschlag` oder `schluss`), nach `status` und nach `lvId`; geblaettert wird ueber `limit` (1-200, Standard 50) und `offset`. Ein Soft-Delete gibt es hier nicht — was in der Tabelle steht, steht in der Liste. Fehlt die Tabelle im Mandanten-Schema noch, legt der Aufruf sie an und antwortet mit einer leeren Liste statt mit einem Fehler."},"post":{"responses":{"201":{"description":"Die angelegte Abrechnung samt gerechneter Betraege","content":{"application/json":{"schema":{"type":"object","properties":{"abrechnung":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"typ":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"lvId":{"type":"string"},"aufmassIds":{"type":"array","items":{"type":"string"}},"netto":{"type":"number"},"ustSatz":{"type":"number"},"ustBetrag":{"type":"number"},"brutto":{"type":"number"},"sicherheitsEinbehaltProzent":{"type":"number"},"sicherheitsEinbehaltBetrag":{"type":"number"},"zahlbetrag":{"type":"number"},"faellig":{"type":"string"},"status":{"type":"string"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","number","typ","customerId","customerName","lvId","aufmassIds","netto","ustSatz","ustBetrag","brutto","sicherheitsEinbehaltProzent","sicherheitsEinbehaltBetrag","zahlbetrag","faellig","status","notizen","createdAt","updatedAt"],"additionalProperties":false}},"required":["abrechnung"],"additionalProperties":false},"example":{"abrechnung":{"id":"string","number":"string","typ":"string","customerId":"string","customerName":"string","lvId":"string","aufmassIds":["string"],"netto":0,"ustSatz":0,"ustBetrag":0,"brutto":0,"sicherheitsEinbehaltProzent":0,"sicherheitsEinbehaltBetrag":0,"zahlbetrag":0,"faellig":"string","status":"string","notizen":"string","createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar — nichts gespeichert","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Bau-abrechnung","tags":["Bau · Abrechnung"],"parameters":[],"summary":"Abrechnung erzeugen","description":"Legt eine Abrechnung an (201). Umsatzsteuerbetrag, Brutto, Einbehaltsbetrag und Zahlbetrag rechnet der Server selbst und nimmt sie NICHT entgegen: Brutto = Netto plus Umsatzsteuer, der Einbehalt ist ein Prozentsatz vom BRUTTO, der Zahlbetrag ist Brutto minus Einbehalt — jeweils auf zwei Stellen gerundet. Das Belegdatum setzt der Server auf heute, `status` beginnt immer auf `draft`. Die `number` wird weder automatisch vergeben noch auf Eindeutigkeit geprueft, und `lvId` wie `aufmassIds` werden nicht gegen ihre Tabellen geprueft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string","minLength":1,"maxLength":40},"typ":{"type":"string","enum":["abschlag","schluss"]},"customerId":{"type":"string","minLength":1,"maxLength":64},"customerName":{"type":"string","minLength":1,"maxLength":200},"lvId":{"type":"string","minLength":1,"maxLength":64},"aufmassIds":{"type":"array","items":{"type":"string","minLength":1},"default":[]},"netto":{"type":"number","minimum":0},"ustSatz":{"type":"number","minimum":0,"maximum":30,"default":19},"sicherheitsEinbehaltProzent":{"type":"number","minimum":0,"maximum":20,"default":0},"faellig":{"type":"string","format":"date"},"notizen":{"type":["string","null"]}},"required":["number","typ","customerId","customerName","lvId","netto","faellig"]},"example":{"number":"string","typ":"abschlag","customerId":"string","customerName":"string","lvId":"string","aufmassIds":["string"],"netto":0,"ustSatz":0,"sicherheitsEinbehaltProzent":0,"faellig":"2026-01-01","notizen":"string"}}}}}},"/api/v1/bau-abrechnung/{id}":{"get":{"responses":{"200":{"description":"Die Abrechnung","content":{"application/json":{"schema":{"type":"object","properties":{"abrechnung":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"typ":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"lvId":{"type":"string"},"aufmassIds":{"type":"array","items":{"type":"string"}},"netto":{"type":"number"},"ustSatz":{"type":"number"},"ustBetrag":{"type":"number"},"brutto":{"type":"number"},"sicherheitsEinbehaltProzent":{"type":"number"},"sicherheitsEinbehaltBetrag":{"type":"number"},"zahlbetrag":{"type":"number"},"faellig":{"type":"string"},"status":{"type":"string"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","number","typ","customerId","customerName","lvId","aufmassIds","netto","ustSatz","ustBetrag","brutto","sicherheitsEinbehaltProzent","sicherheitsEinbehaltBetrag","zahlbetrag","faellig","status","notizen","createdAt","updatedAt"],"additionalProperties":false}},"required":["abrechnung"],"additionalProperties":false},"example":{"abrechnung":{"id":"string","number":"string","typ":"string","customerId":"string","customerName":"string","lvId":"string","aufmassIds":["string"],"netto":0,"ustSatz":0,"ustBetrag":0,"brutto":0,"sicherheitsEinbehaltProzent":0,"sicherheitsEinbehaltBetrag":0,"zahlbetrag":0,"faellig":"string","status":"string","notizen":"string","createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not Found"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Bau-abrechnungById","tags":["Bau · Abrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Detail","description":"Liest genau eine Abrechnung samt allen gerechneten Betraegen — Umsatzsteuer, Brutto, Sicherheitseinbehalt und Zahlbetrag —, eingepackt in `abrechnung`. Eine unbekannte id ergibt 404; ebenso eine im Mandanten-Schema noch fehlende Tabelle, damit ein frischer Mandant hier keinen Serverfehler sieht."}},"/api/v1/bau-abrechnung/{id}/freigeben":{"post":{"responses":{"200":{"description":"Die freigegebene Abrechnung","content":{"application/json":{"schema":{"type":"object","properties":{"abrechnung":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"typ":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"lvId":{"type":"string"},"aufmassIds":{"type":"array","items":{"type":"string"}},"netto":{"type":"number"},"ustSatz":{"type":"number"},"ustBetrag":{"type":"number"},"brutto":{"type":"number"},"sicherheitsEinbehaltProzent":{"type":"number"},"sicherheitsEinbehaltBetrag":{"type":"number"},"zahlbetrag":{"type":"number"},"faellig":{"type":"string"},"status":{"type":"string"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","number","typ","customerId","customerName","lvId","aufmassIds","netto","ustSatz","ustBetrag","brutto","sicherheitsEinbehaltProzent","sicherheitsEinbehaltBetrag","zahlbetrag","faellig","status","notizen","createdAt","updatedAt"],"additionalProperties":false}},"required":["abrechnung"],"additionalProperties":false},"example":{"abrechnung":{"id":"string","number":"string","typ":"string","customerId":"string","customerName":"string","lvId":"string","aufmassIds":["string"],"netto":0,"ustSatz":0,"ustBetrag":0,"brutto":0,"sicherheitsEinbehaltProzent":0,"sicherheitsEinbehaltBetrag":0,"zahlbetrag":0,"faellig":"string","status":"string","notizen":"string","createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Not Found"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Bau-abrechnungByIdFreigeben","tags":["Bau · Abrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Freigeben","description":"Setzt `status` auf `approved` und haelt den Zeitpunkt der Freigabe fest. Der bisherige Status wird dabei NICHT geprueft: eine bereits freigegebene Abrechnung laesst sich erneut freigeben, der Zeitpunkt wird dann ueberschrieben. Ein Weg zurueck fuehrt nicht ueber diese Route. Unbekannte id → 404. Erfordert mindestens die Rolle Manager."}},"/api/v1/bau-abrechnung/{id}/sicherheits-einbehalt":{"post":{"responses":{"200":{"description":"Die Abrechnung mit neu gerechnetem Einbehalt und Zahlbetrag","content":{"application/json":{"schema":{"type":"object","properties":{"abrechnung":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"typ":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"lvId":{"type":"string"},"aufmassIds":{"type":"array","items":{"type":"string"}},"netto":{"type":"number"},"ustSatz":{"type":"number"},"ustBetrag":{"type":"number"},"brutto":{"type":"number"},"sicherheitsEinbehaltProzent":{"type":"number"},"sicherheitsEinbehaltBetrag":{"type":"number"},"zahlbetrag":{"type":"number"},"faellig":{"type":"string"},"status":{"type":"string"},"notizen":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","number","typ","customerId","customerName","lvId","aufmassIds","netto","ustSatz","ustBetrag","brutto","sicherheitsEinbehaltProzent","sicherheitsEinbehaltBetrag","zahlbetrag","faellig","status","notizen","createdAt","updatedAt"],"additionalProperties":false}},"required":["abrechnung"],"additionalProperties":false},"example":{"abrechnung":{"id":"string","number":"string","typ":"string","customerId":"string","customerName":"string","lvId":"string","aufmassIds":["string"],"netto":0,"ustSatz":0,"ustBetrag":0,"brutto":0,"sicherheitsEinbehaltProzent":0,"sicherheitsEinbehaltBetrag":0,"zahlbetrag":0,"faellig":"string","status":"string","notizen":"string","createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Not Found"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Bau-abrechnungByIdSicherheits-einbehalt","tags":["Bau · Abrechnung"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Sicherheitseinbehalt setzen","description":"Setzt den Prozentsatz des Sicherheitseinbehalts (0-20) neu und rechnet Einbehaltsbetrag und Zahlbetrag aus dem GESPEICHERTEN Netto und Steuersatz nach. Netto, Umsatzsteuer und Brutto bleiben unveraendert. Der Status spielt keine Rolle — auch eine freigegebene Abrechnung laesst sich so noch aendern —, und anders als beim Freigeben verlangt diese Route keine Manager-Rolle. Unbekannte id → 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"prozent":{"type":"number","minimum":0,"maximum":20}},"required":["prozent"]},"example":{"prozent":0}}}}}},"/api/v1/nachtragsangebote":{"get":{"responses":{"200":{"description":"Liste der Nachträge; leer auch dann, wenn die Tabelle noch nie angelegt wurde","content":{"application/json":{"schema":{"type":"object","properties":{"nachtraege":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Nachtrags"},"number":{"type":"string","maxLength":40,"description":"Nachtragsnummer"},"title":{"type":"string","maxLength":200,"description":"Titel des Nachtrags"},"parentOrder":{"type":"string","maxLength":40,"description":"Nummer des Hauptauftrags, zu dem der Nachtrag gehoert"},"lvId":{"type":"string","maxLength":64,"description":"Kennung des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"lvNumber":{"type":"string","maxLength":40,"description":"Nummer des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"reason":{"type":"string","maxLength":500,"description":"Begruendung des Nachtrags; LEER, nicht null, wenn nicht gesetzt"},"delta":{"type":"number","description":"Betragsaenderung gegenueber dem Hauptauftrag; negativ bei einer Minderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"positionNr":{"type":"string","minLength":1,"maxLength":40,"description":"Ordnungsnummer der Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","minLength":1,"maxLength":400,"description":"Beschreibung der Leistung"},"menge":{"type":"number","description":"Menge; darf negativ sein, etwa bei einer Minderung"},"einheit":{"type":"string","minLength":1,"maxLength":10,"description":"Mengeneinheit, etwa Stk, m oder h"},"ep":{"type":"number","description":"Einzelpreis je Einheit"}},"required":["positionNr","bezeichnung","menge","einheit","ep"],"additionalProperties":false},"description":"Die Nachtragspositionen"},"status":{"type":"string","description":"draft, sent, accepted, rejected oder invoiced; der Altwert „entwurf\" wird zu draft"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO); LEER, wenn die Spalte nicht lesbar war"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO); LEER, wenn die Spalte nicht lesbar war"}},"required":["id","number","title","parentOrder","lvId","lvNumber","reason","delta","positionen","status","createdAt","updatedAt"],"additionalProperties":false},"description":"Die Nachtraege dieser Seite, neueste zuerst"},"total":{"type":"integer","minimum":0,"description":"Gesamtzahl der Treffer bei diesem Filter, unabhaengig von limit und offset"}},"required":["nachtraege","total"],"additionalProperties":false},"example":{"nachtraege":[{"id":"00000000-0000-4000-8000-000000000000","number":"string","title":"string","parentOrder":"string","lvId":"string","lvNumber":"string","reason":"string","delta":0,"positionen":[{"positionNr":"string","bezeichnung":"string","menge":0,"einheit":"string","ep":0}],"status":"string","createdAt":"string","updatedAt":"string"}],"total":0}}}},"400":{"description":"Ungültige Query-Parameter"},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext"},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1Nachtragsangebote","tags":["Bau · Nachträge"],"parameters":[{"in":"query","name":"parentOrder","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","sent","accepted","rejected","invoiced"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste","description":"Listet Nachträge des Mandanten, neueste zuerst, mit Filter auf Hauptauftrag und Status. Geblättert wird über limit/offset. ACHTUNG: fehlt die Tabelle im Mandanten-Schema, kommt trotzdem 200 mit einer leeren Liste — ein frischer Mandant ist von „keine Nachträge vorhanden\" nicht zu unterscheiden."},"post":{"responses":{"201":{"description":"Angelegt — der neue Nachtrag, eingepackt unter `nachtrag`","content":{"application/json":{"schema":{"type":"object","properties":{"nachtrag":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Nachtrags"},"number":{"type":"string","maxLength":40,"description":"Nachtragsnummer"},"title":{"type":"string","maxLength":200,"description":"Titel des Nachtrags"},"parentOrder":{"type":"string","maxLength":40,"description":"Nummer des Hauptauftrags, zu dem der Nachtrag gehoert"},"lvId":{"type":"string","maxLength":64,"description":"Kennung des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"lvNumber":{"type":"string","maxLength":40,"description":"Nummer des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"reason":{"type":"string","maxLength":500,"description":"Begruendung des Nachtrags; LEER, nicht null, wenn nicht gesetzt"},"delta":{"type":"number","description":"Betragsaenderung gegenueber dem Hauptauftrag; negativ bei einer Minderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"positionNr":{"type":"string","minLength":1,"maxLength":40,"description":"Ordnungsnummer der Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","minLength":1,"maxLength":400,"description":"Beschreibung der Leistung"},"menge":{"type":"number","description":"Menge; darf negativ sein, etwa bei einer Minderung"},"einheit":{"type":"string","minLength":1,"maxLength":10,"description":"Mengeneinheit, etwa Stk, m oder h"},"ep":{"type":"number","description":"Einzelpreis je Einheit"}},"required":["positionNr","bezeichnung","menge","einheit","ep"],"additionalProperties":false},"description":"Die Nachtragspositionen"},"status":{"type":"string","description":"draft, sent, accepted, rejected oder invoiced; der Altwert „entwurf\" wird zu draft"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO); LEER, wenn die Spalte nicht lesbar war"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO); LEER, wenn die Spalte nicht lesbar war"}},"required":["id","number","title","parentOrder","lvId","lvNumber","reason","delta","positionen","status","createdAt","updatedAt"],"additionalProperties":false,"description":"Der Nachtrag"}},"required":["nachtrag"],"additionalProperties":false},"example":{"nachtrag":{"id":"00000000-0000-4000-8000-000000000000","number":"string","title":"string","parentOrder":"string","lvId":"string","lvNumber":"string","reason":"string","delta":0,"positionen":[{"positionNr":"string","bezeichnung":"string","menge":0,"einheit":"string","ep":0}],"status":"string","createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext"},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1Nachtragsangebote","tags":["Bau · Nachträge"],"parameters":[],"summary":"Nachtrag anlegen","description":"Legt einen Nachtrag an. Nachtragsnummer und Hauptauftrag kommen aus dem Aufruf, der Server vergibt hier keine Nummer. Es wird NICHT geprüft, ob der Hauptauftrag existiert oder ob es die Nachtragsnummer schon gibt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string","minLength":1,"maxLength":40},"title":{"type":"string","minLength":1,"maxLength":200},"parentOrder":{"type":"string","minLength":1,"maxLength":40},"lvId":{"type":"string","minLength":1,"maxLength":64},"lvNumber":{"type":"string","minLength":1,"maxLength":40},"reason":{"type":"string","minLength":1,"maxLength":500},"delta":{"type":"number"},"positionen":{"type":"array","items":{"type":"object","properties":{"positionNr":{"type":"string","minLength":1,"maxLength":40},"bezeichnung":{"type":"string","minLength":1,"maxLength":400},"menge":{"type":"number"},"einheit":{"type":"string","minLength":1,"maxLength":10},"ep":{"type":"number"}},"required":["positionNr","bezeichnung","menge","einheit","ep"]},"default":[]},"status":{"type":"string","enum":["draft","sent","accepted","rejected","invoiced"],"default":"draft"}},"required":["number","title","parentOrder","lvId","lvNumber","reason","delta"]},"example":{"number":"string","title":"string","parentOrder":"string","lvId":"string","lvNumber":"string","reason":"string","delta":0,"positionen":[{"positionNr":"string","bezeichnung":"string","menge":0,"einheit":"string","ep":0}],"status":"draft"}}}}}},"/api/v1/nachtragsangebote/{id}":{"get":{"responses":{"200":{"description":"Der Nachtrag, eingepackt unter `nachtrag`","content":{"application/json":{"schema":{"type":"object","properties":{"nachtrag":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Nachtrags"},"number":{"type":"string","maxLength":40,"description":"Nachtragsnummer"},"title":{"type":"string","maxLength":200,"description":"Titel des Nachtrags"},"parentOrder":{"type":"string","maxLength":40,"description":"Nummer des Hauptauftrags, zu dem der Nachtrag gehoert"},"lvId":{"type":"string","maxLength":64,"description":"Kennung des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"lvNumber":{"type":"string","maxLength":40,"description":"Nummer des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"reason":{"type":"string","maxLength":500,"description":"Begruendung des Nachtrags; LEER, nicht null, wenn nicht gesetzt"},"delta":{"type":"number","description":"Betragsaenderung gegenueber dem Hauptauftrag; negativ bei einer Minderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"positionNr":{"type":"string","minLength":1,"maxLength":40,"description":"Ordnungsnummer der Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","minLength":1,"maxLength":400,"description":"Beschreibung der Leistung"},"menge":{"type":"number","description":"Menge; darf negativ sein, etwa bei einer Minderung"},"einheit":{"type":"string","minLength":1,"maxLength":10,"description":"Mengeneinheit, etwa Stk, m oder h"},"ep":{"type":"number","description":"Einzelpreis je Einheit"}},"required":["positionNr","bezeichnung","menge","einheit","ep"],"additionalProperties":false},"description":"Die Nachtragspositionen"},"status":{"type":"string","description":"draft, sent, accepted, rejected oder invoiced; der Altwert „entwurf\" wird zu draft"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO); LEER, wenn die Spalte nicht lesbar war"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO); LEER, wenn die Spalte nicht lesbar war"}},"required":["id","number","title","parentOrder","lvId","lvNumber","reason","delta","positionen","status","createdAt","updatedAt"],"additionalProperties":false,"description":"Der Nachtrag"}},"required":["nachtrag"],"additionalProperties":false},"example":{"nachtrag":{"id":"00000000-0000-4000-8000-000000000000","number":"string","title":"string","parentOrder":"string","lvId":"string","lvNumber":"string","reason":"string","delta":0,"positionen":[{"positionNr":"string","bezeichnung":"string","menge":0,"einheit":"string","ep":0}],"status":"string","createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext"},"404":{"description":"Nachtrag nicht gefunden — oder die Tabelle gibt es im Mandanten-Schema nicht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1NachtragsangeboteById","tags":["Bau · Nachträge"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Detail","description":"Liefert einen Nachtrag. Der Nachtrag steckt unter dem Schlüssel `nachtrag`, er ist nicht der Antwortkörper selbst. Fehlt die Tabelle im Mandanten-Schema, antwortet der Aufruf 404 — anders als die Liste, die dann 200 mit leerer Liste gibt."},"put":{"responses":{"200":{"description":"Der Nachtrag nach der Änderung, eingepackt unter `nachtrag`","content":{"application/json":{"schema":{"type":"object","properties":{"nachtrag":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Nachtrags"},"number":{"type":"string","maxLength":40,"description":"Nachtragsnummer"},"title":{"type":"string","maxLength":200,"description":"Titel des Nachtrags"},"parentOrder":{"type":"string","maxLength":40,"description":"Nummer des Hauptauftrags, zu dem der Nachtrag gehoert"},"lvId":{"type":"string","maxLength":64,"description":"Kennung des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"lvNumber":{"type":"string","maxLength":40,"description":"Nummer des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"reason":{"type":"string","maxLength":500,"description":"Begruendung des Nachtrags; LEER, nicht null, wenn nicht gesetzt"},"delta":{"type":"number","description":"Betragsaenderung gegenueber dem Hauptauftrag; negativ bei einer Minderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"positionNr":{"type":"string","minLength":1,"maxLength":40,"description":"Ordnungsnummer der Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","minLength":1,"maxLength":400,"description":"Beschreibung der Leistung"},"menge":{"type":"number","description":"Menge; darf negativ sein, etwa bei einer Minderung"},"einheit":{"type":"string","minLength":1,"maxLength":10,"description":"Mengeneinheit, etwa Stk, m oder h"},"ep":{"type":"number","description":"Einzelpreis je Einheit"}},"required":["positionNr","bezeichnung","menge","einheit","ep"],"additionalProperties":false},"description":"Die Nachtragspositionen"},"status":{"type":"string","description":"draft, sent, accepted, rejected oder invoiced; der Altwert „entwurf\" wird zu draft"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO); LEER, wenn die Spalte nicht lesbar war"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO); LEER, wenn die Spalte nicht lesbar war"}},"required":["id","number","title","parentOrder","lvId","lvNumber","reason","delta","positionen","status","createdAt","updatedAt"],"additionalProperties":false,"description":"Der Nachtrag"}},"required":["nachtrag"],"additionalProperties":false},"example":{"nachtrag":{"id":"00000000-0000-4000-8000-000000000000","number":"string","title":"string","parentOrder":"string","lvId":"string","lvNumber":"string","reason":"string","delta":0,"positionen":[{"positionNr":"string","bezeichnung":"string","menge":0,"einheit":"string","ep":0}],"status":"string","createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"Validierungsfehler"},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext"},"404":{"description":"Nachtrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1NachtragsangeboteById","tags":["Bau · Nachträge"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Nachtrag aktualisieren","description":"Ändert einzelne Felder eines Nachtrags — nur mitgeschickte werden geschrieben. Die Positionsliste lässt sich hier NICHT ändern, sie ist aus dem Eingabekörper ausgenommen. Ein leerer Körper ist erlaubt und liefert den unveränderten Nachtrag zurück, ohne dass etwas geschrieben wird.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string","minLength":1,"maxLength":40},"title":{"type":"string","minLength":1,"maxLength":200},"parentOrder":{"type":"string","minLength":1,"maxLength":40},"lvId":{"type":"string","minLength":1,"maxLength":64},"lvNumber":{"type":"string","minLength":1,"maxLength":40},"reason":{"type":"string","minLength":1,"maxLength":500},"delta":{"type":"number"},"status":{"type":"string","enum":["draft","sent","accepted","rejected","invoiced"],"default":"draft"}}},"example":{"number":"string","title":"string","parentOrder":"string","lvId":"string","lvNumber":"string","reason":"string","delta":0,"status":"draft"}}}}}},"/api/v1/nachtragsangebote/{id}/akzeptieren":{"post":{"responses":{"200":{"description":"Der Nachtrag nach der Entscheidung, eingepackt unter `nachtrag`","content":{"application/json":{"schema":{"type":"object","properties":{"nachtrag":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Nachtrags"},"number":{"type":"string","maxLength":40,"description":"Nachtragsnummer"},"title":{"type":"string","maxLength":200,"description":"Titel des Nachtrags"},"parentOrder":{"type":"string","maxLength":40,"description":"Nummer des Hauptauftrags, zu dem der Nachtrag gehoert"},"lvId":{"type":"string","maxLength":64,"description":"Kennung des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"lvNumber":{"type":"string","maxLength":40,"description":"Nummer des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"reason":{"type":"string","maxLength":500,"description":"Begruendung des Nachtrags; LEER, nicht null, wenn nicht gesetzt"},"delta":{"type":"number","description":"Betragsaenderung gegenueber dem Hauptauftrag; negativ bei einer Minderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"positionNr":{"type":"string","minLength":1,"maxLength":40,"description":"Ordnungsnummer der Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","minLength":1,"maxLength":400,"description":"Beschreibung der Leistung"},"menge":{"type":"number","description":"Menge; darf negativ sein, etwa bei einer Minderung"},"einheit":{"type":"string","minLength":1,"maxLength":10,"description":"Mengeneinheit, etwa Stk, m oder h"},"ep":{"type":"number","description":"Einzelpreis je Einheit"}},"required":["positionNr","bezeichnung","menge","einheit","ep"],"additionalProperties":false},"description":"Die Nachtragspositionen"},"status":{"type":"string","description":"draft, sent, accepted, rejected oder invoiced; der Altwert „entwurf\" wird zu draft"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO); LEER, wenn die Spalte nicht lesbar war"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO); LEER, wenn die Spalte nicht lesbar war"}},"required":["id","number","title","parentOrder","lvId","lvNumber","reason","delta","positionen","status","createdAt","updatedAt"],"additionalProperties":false,"description":"Der Nachtrag"}},"required":["nachtrag"],"additionalProperties":false},"example":{"nachtrag":{"id":"00000000-0000-4000-8000-000000000000","number":"string","title":"string","parentOrder":"string","lvId":"string","lvNumber":"string","reason":"string","delta":0,"positionen":[{"positionNr":"string","bezeichnung":"string","menge":0,"einheit":"string","ep":0}],"status":"string","createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Nachtrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1NachtragsangeboteByIdAkzeptieren","tags":["Bau · Nachträge"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Akzeptieren","description":"Setzt einen Nachtrag auf „accepted\" und hält den Entscheidungszeitpunkt fest. Es wird KEIN Statusübergang geprüft: ein bereits abgelehnter Nachtrag lässt sich so nachträglich annehmen. Der Hauptauftrag bleibt unberührt — die Betragsänderung wird nicht auf ihn gebucht."}},"/api/v1/nachtragsangebote/{id}/ablehnen":{"post":{"responses":{"200":{"description":"Der Nachtrag nach der Entscheidung, eingepackt unter `nachtrag`","content":{"application/json":{"schema":{"type":"object","properties":{"nachtrag":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Technische Kennung des Nachtrags"},"number":{"type":"string","maxLength":40,"description":"Nachtragsnummer"},"title":{"type":"string","maxLength":200,"description":"Titel des Nachtrags"},"parentOrder":{"type":"string","maxLength":40,"description":"Nummer des Hauptauftrags, zu dem der Nachtrag gehoert"},"lvId":{"type":"string","maxLength":64,"description":"Kennung des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"lvNumber":{"type":"string","maxLength":40,"description":"Nummer des Leistungsverzeichnisses; LEER, nicht null, wenn nicht gesetzt"},"reason":{"type":"string","maxLength":500,"description":"Begruendung des Nachtrags; LEER, nicht null, wenn nicht gesetzt"},"delta":{"type":"number","description":"Betragsaenderung gegenueber dem Hauptauftrag; negativ bei einer Minderung"},"positionen":{"type":"array","items":{"type":"object","properties":{"positionNr":{"type":"string","minLength":1,"maxLength":40,"description":"Ordnungsnummer der Position im Leistungsverzeichnis"},"bezeichnung":{"type":"string","minLength":1,"maxLength":400,"description":"Beschreibung der Leistung"},"menge":{"type":"number","description":"Menge; darf negativ sein, etwa bei einer Minderung"},"einheit":{"type":"string","minLength":1,"maxLength":10,"description":"Mengeneinheit, etwa Stk, m oder h"},"ep":{"type":"number","description":"Einzelpreis je Einheit"}},"required":["positionNr","bezeichnung","menge","einheit","ep"],"additionalProperties":false},"description":"Die Nachtragspositionen"},"status":{"type":"string","description":"draft, sent, accepted, rejected oder invoiced; der Altwert „entwurf\" wird zu draft"},"createdAt":{"type":"string","description":"Anlagezeitpunkt (ISO); LEER, wenn die Spalte nicht lesbar war"},"updatedAt":{"type":"string","description":"Letzte Aenderung (ISO); LEER, wenn die Spalte nicht lesbar war"}},"required":["id","number","title","parentOrder","lvId","lvNumber","reason","delta","positionen","status","createdAt","updatedAt"],"additionalProperties":false,"description":"Der Nachtrag"}},"required":["nachtrag"],"additionalProperties":false},"example":{"nachtrag":{"id":"00000000-0000-4000-8000-000000000000","number":"string","title":"string","parentOrder":"string","lvId":"string","lvNumber":"string","reason":"string","delta":0,"positionen":[{"positionNr":"string","bezeichnung":"string","menge":0,"einheit":"string","ep":0}],"status":"string","createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"Nicht authentifiziert oder kein Mandant im Kontext"},"403":{"description":"Keine Manager-Rolle"},"404":{"description":"Nachtrag nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found","description":"Fester Fehlerschluessel"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Datenbank nicht erreichbar. Fehlt der Datenbank-Client ganz, kommt stattdessen ein 503 als text/plain ohne diesen Rumpf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fester Fehlerschluessel"},"retryAfter":{"type":"integer","minimum":1,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiV1NachtragsangeboteByIdAblehnen","tags":["Bau · Nachträge"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Ablehnen","description":"Setzt einen Nachtrag auf „rejected\" und hält den Entscheidungszeitpunkt fest. Es wird KEIN Statusübergang geprüft: ein bereits angenommener oder sogar abgerechneter Nachtrag lässt sich so nachträglich ablehnen."}},"/api/v1/zugferd":{"get":{"responses":{"200":{"description":"Liste — ENTWEDER aus den Rechnungen des Mandanten ODER die vier Beispieldatensätze (siehe Beschreibung). Die Formen sind identisch.","content":{"application/json":{"schema":{"type":"object","properties":{"rechnungen":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"invoiceId":{"type":"string"},"invoiceNumber":{"type":"string"},"customer":{"type":"string"},"profile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"]},"total":{"type":"number"},"generatedAt":{"type":"string"},"validationMessage":{"type":"string"},"status":{"type":"string","enum":["generated","validated","invalid","sent"]}},"required":["id","invoiceId","invoiceNumber","customer","profile","total","generatedAt","validationMessage","status"],"additionalProperties":false}},"total":{"type":"integer"}},"required":["rechnungen","total"],"additionalProperties":false},"example":{"rechnungen":[{"id":"string","invoiceId":"string","invoiceNumber":"string","customer":"string","profile":"EN16931","total":0,"generatedAt":"string","validationMessage":"string","status":"generated"}],"total":0}}}},"400":{"description":"Validierungsfehler in den Abfrageparametern","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Zugferd","tags":["Bau · ZUGFeRD"],"parameters":[{"in":"query","name":"profile","schema":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"]}},{"in":"query","name":"status","schema":{"type":"string","enum":["generated","validated","invalid","sent"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"summary":"Liste E-Rechnungen","description":"Listet die Rechnungen des Mandanten in ZUGFeRD-Sicht. Es gibt KEINE Tabelle für E-Rechnungen — die Einträge werden bei jedem Aufruf aus den Rechnungen abgeleitet; `id`, `profile` und `status` sind berechnet, nicht gespeichert. ACHTUNG: Ohne Mandantenkontext, ohne Datenbank ODER nach einem beliebigen Datenbankfehler (der nur als Warnung protokolliert wird) antwortet dieser Aufruf mit VIER FEST VERDRAHTETEN BEISPIELRECHNUNGEN („Müller Bau GmbH\", „Stadt München\", „Holz & Hammer KG\", „Bauträger Süd AG\") — ebenfalls mit Status 200 und ohne jedes Kennzeichen im Rumpf. Wer diese Antwort weiterverarbeitet, kann echte Daten nicht von Beispieldaten unterscheiden."}},"/api/v1/zugferd/detail/{id}":{"get":{"responses":{"200":{"description":"Die Rechnung in ZUGFeRD-Sicht","content":{"application/json":{"schema":{"type":"object","properties":{"rechnung":{"type":"object","properties":{"id":{"type":"string"},"invoiceId":{"type":"string"},"invoiceNumber":{"type":"string"},"customer":{"type":"string"},"profile":{"type":"string","enum":["EN16931","XRECHNUNG"]},"total":{"type":"number"},"generatedAt":{"type":"string"},"status":{"type":"string"}},"required":["id","invoiceId","invoiceNumber","customer","profile","total","generatedAt","status"],"additionalProperties":false}},"required":["rechnung"],"additionalProperties":false},"example":{"rechnung":{"id":"string","invoiceId":"string","invoiceNumber":"string","customer":"string","profile":"EN16931","total":0,"generatedAt":"string","status":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht gefunden — ODER: kein Mandant, keine Datenbank, Datenbankfehler","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1ZugferdDetailById","tags":["Bau · ZUGFeRD"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Detail","description":"Eine Rechnung in ZUGFeRD-Sicht. Der Pfad lautet `/detail/:id` (die Kopfzeile der Quelldatei nennt fälschlich `/zugferd/:id`); ein Präfix `zf_` wird abgeschnitten. ZWEI Abweichungen zur Liste: `validationMessage` fehlt hier, und `status` ist der UNÜBERSETZTE Rechnungsstatus (`draft`, `paid`, …) statt des ZUGFeRD-Stands — dieselbe Rechnung meldet über beide Wege verschiedene Wörter. Ohne Mandantenkontext oder Datenbank sowie NACH EINEM DATENBANKFEHLER antwortet der Aufruf 404: ein Ausfall ist von „gibt es nicht\" nicht unterscheidbar."}},"/api/v1/zugferd/invoices/{id}/xml":{"get":{"responses":{"200":{"description":"Das E-Rechnungs-XML — oder das Demo-XML ohne Rechnungsdaten (siehe Beschreibung). Das verwendete Profil steht im Kopf `X-ZUGFeRD-Profile`.","content":{"application/xml":{"schema":{"type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Rechnung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Invoice not found"}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Eigene Firmen-Stammdaten unvollständig — ohne Name und Ort verletzt jede erzeugte XRechnung EN16931 BR-06/BR-08. Bewusst ein Fehler statt eines unbrauchbaren Dokuments.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"seller_stammdaten_incomplete"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"XML-Erzeugung fehlgeschlagen — `details` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"string"}},"required":["error","details"],"additionalProperties":false}}}}},"operationId":"getApiV1ZugferdInvoicesByIdXml","tags":["Bau · ZUGFeRD"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"ZUGFeRD XML","description":"Erzeugt das E-Rechnungs-XML zur Rechnung und liefert es als Datei aus (`?profile=EN16931|XRECHNUNG|EXTENDED`, Standard XRECHNUNG; ein unbekannter Wert wird stillschweigend zu XRECHNUNG). Nichts wird gespeichert. ACHTUNG: Ohne Mandantenkontext oder Datenbank kommt mit Status 200 ein FEST VERDRAHTETES DEMO-XML zurück, das keinerlei Rechnungsdaten enthält — nur ein Wurzelelement und ein Kommentar. Erkennbar allein daran, dass der Kopf `X-ZUGFeRD-Profile` fehlt."}},"/api/v1/zugferd/invoices/{id}/download":{"get":{"responses":{"200":{"description":"Die Rechnung als PDF/A-3 — ODER, wenn der PDF-Erzeuger fehlt, das XML als Anhang (Kopf `X-ZUGFeRD-Mode: xml-only-stub`).","content":{"application/pdf":{},"application/xml":{"schema":{"type":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Rechnung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Invoice not found"}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Eigene Firmen-Stammdaten unvollständig (EN16931 BR-06/BR-08)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"seller_stammdaten_incomplete"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"Download fehlgeschlagen — `details` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"string"}},"required":["error","details"],"additionalProperties":false}}}},"503":{"description":"Kein Mandantenkontext oder keine Datenbank","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"No DB context"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"getApiV1ZugferdInvoicesByIdDownload","tags":["Bau · ZUGFeRD"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"ZUGFeRD PDF/A-3 Download","description":"Lädt die Rechnung als PDF/A-3 mit eingebettetem E-Rechnungs-XML herunter. ACHTUNG: Ist der PDF-Erzeuger im Container nicht verfügbar oder liefert er 0 Bytes, kommt unter demselben Status 200 STATTDESSEN das nackte XML als Anhang — kein PDF. Erkennbar am Kopf `X-ZUGFeRD-Mode: xml-only-stub` und am Inhaltstyp; wer blind auf ein PDF baut, bekommt eine XML-Datei mit `.xml`-Endung."}},"/api/v1/zugferd/invoices/{id}/validate":{"post":{"responses":{"200":{"description":"Prüfergebnis mit Regelverstößen und Warnungen","content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string"},"invoiceNumber":{"type":"string"},"valid":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"}},"required":["rule","message"],"additionalProperties":false}},"warnings":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"}},"required":["rule","message"],"additionalProperties":false}},"profile":{"type":"string","enum":["xrechnung","en16931","basic","minimum","invalid"]},"checkedAt":{"type":"string"}},"required":["invoiceId","invoiceNumber","valid","errors","warnings","profile","checkedAt"],"additionalProperties":false},"example":{"invoiceId":"string","invoiceNumber":"string","valid":true,"errors":[{"rule":"string","message":"string","field":"string"}],"warnings":[{"rule":"string","message":"string","field":"string"}],"profile":"xrechnung","checkedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Rechnung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Invoice not found"}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Eigene Firmen-Stammdaten unvollständig (EN16931 BR-06/BR-08)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"seller_stammdaten_incomplete"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"Prüfung fehlgeschlagen — `details` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"string"}},"required":["error","details"],"additionalProperties":false}}}},"503":{"description":"Kein Mandantenkontext oder keine Datenbank","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"No DB context"}},"required":["error"],"additionalProperties":false}}}}},"operationId":"postApiV1ZugferdInvoicesByIdValidate","tags":["Bau · ZUGFeRD"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"EN-16931 Validierung","description":"Prüft eine gespeicherte Rechnung gegen die EN-16931-Regeln. Rein lesend — das Ergebnis wird NICHT gespeichert, `checkedAt` ist der Zeitpunkt dieses Aufrufs. Geprüft wird die aus der Rechnung ABGELEITETE Struktur, nicht ein zuvor erzeugtes XML; fehlen Positionen, setzt die Ableitung eine Ersatzposition über den Nettobetrag ein."}},"/api/v1/zugferd/generate":{"post":{"responses":{"201":{"description":"ENTWEDER das echte Ergebnis (mit `validation` und `xmlPreview`) ODER — ohne Mandant/Datenbank — ein Notbehelf ohne diese beiden Felder, dessen `id` eine Zufallszeichenkette ist, `invoiceNumber` `RE-PENDING-…` lautet, `customer` „Pending lookup\" heißt und `total` 0 ist. Beide tragen denselben Statuscode; unterscheidbar allein am Vorhandensein von `validation`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"rechnung":{"type":"object","properties":{"id":{"type":"string"},"invoiceId":{"type":"string"},"invoiceNumber":{"type":"string"},"customer":{"type":"string"},"profile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"]},"total":{"type":"number"},"generatedAt":{"type":"string"},"validationMessage":{"type":"string"},"status":{"type":"string","enum":["generated","validated","invalid","sent"]}},"required":["id","invoiceId","invoiceNumber","customer","profile","total","generatedAt","validationMessage","status"],"additionalProperties":false},"validation":{"type":"object","properties":{"valid":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"}},"required":["rule","message"],"additionalProperties":false}},"warnings":{"type":"array","items":{"type":"object","properties":{"rule":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"}},"required":["rule","message"],"additionalProperties":false}},"profile":{"type":"string","enum":["xrechnung","en16931","basic","minimum","invalid"]}},"required":["valid","errors","warnings","profile"],"additionalProperties":false},"embedAsPdfA3":{"type":"boolean"},"xmlPreview":{"type":"string"}},"required":["rechnung","validation","embedAsPdfA3","xmlPreview"],"additionalProperties":false},{"type":"object","properties":{"rechnung":{"type":"object","properties":{"id":{"type":"string"},"invoiceId":{"type":"string"},"invoiceNumber":{"type":"string"},"customer":{"type":"string"},"profile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"]},"total":{"type":"number"},"generatedAt":{"type":"string"},"validationMessage":{"type":"string"},"status":{"type":"string","enum":["generated","validated","invalid","sent"]}},"required":["id","invoiceId","invoiceNumber","customer","profile","total","generatedAt","validationMessage","status"],"additionalProperties":false},"embedAsPdfA3":{"type":"boolean"}},"required":["rechnung","embedAsPdfA3"],"additionalProperties":false}]},"example":{"rechnung":{"id":"string","invoiceId":"string","invoiceNumber":"string","customer":"string","profile":"EN16931","total":0,"generatedAt":"string","validationMessage":"string","status":"generated"},"validation":{"valid":true,"errors":[{"rule":"string","message":"string","field":"string"}],"warnings":[{"rule":"string","message":"string","field":"string"}],"profile":"xrechnung"},"embedAsPdfA3":true,"xmlPreview":"string"}}}},"400":{"description":"Validierungsfehler im Rumpf","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"404":{"description":"Rechnung nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Invoice not found"}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Eigene Firmen-Stammdaten unvollständig (EN16931 BR-06/BR-08)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"seller_stammdaten_incomplete"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"Erzeugung fehlgeschlagen — `details` trägt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"string"}},"required":["error","details"],"additionalProperties":false}}}}},"operationId":"postApiV1ZugferdGenerate","tags":["Bau · ZUGFeRD"],"parameters":[],"summary":"Generieren","description":"Erzeugt E-Rechnungs-XML zu einer Rechnung, prüft es und gibt eine Vorschau zurück. SPEICHERT NICHTS: die Datei hat keine Tabelle für E-Rechnungen, im Handler steht kein INSERT. Die zurückgegebene `id` ist aus der Rechnungs-id abgeleitet und lässt sich später nirgends abrufen; ein zweiter Aufruf erzeugt alles neu. Der Parameter `embedAsPdfA3` wird nur zurückgespiegelt — ein PDF entsteht hier nicht, dafür gibt es /invoices/:id/download. Ohne Mandantenkontext oder Datenbank kommt ein NOTBEHELF mit ausgedachten Werten (siehe 201).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string","minLength":1,"maxLength":64},"profile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"],"default":"XRECHNUNG"},"embedAsPdfA3":{"type":"boolean","default":true}},"required":["invoiceId"]},"example":{"invoiceId":"string","profile":"EN16931","embedAsPdfA3":true}}}}}},"/api/v1/zugferd/validate":{"post":{"responses":{"200":{"description":"Prüfergebnis. `errors`/`warnings` sind hier flache Zeichenketten, nicht die Objekte der Rechnungsprüfung.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"valid":{"type":"boolean"},"profile":{"type":"string"},"payloadType":{"type":"string","enum":["pdf","xml"]},"errors":{"type":"array","items":{"type":"string"}},"warnings":{"type":"array","items":{"type":"string"}}},"required":["ok","valid","profile","payloadType","errors","warnings"],"additionalProperties":false},"example":{"ok":true,"valid":true,"profile":"string","payloadType":"pdf","errors":["string"],"warnings":["string"]}}}},"400":{"description":"Zwei Formen: der übliche Validierungsfehler `{ error: \"validation_failed\", fields }` — ODER `{ ok: false, errors: [\"payload_not_base64\"] }`. Die zweite ist in der Praxis nicht erreichbar: die verwendete Dekodierung wirft bei ungültigem Base64 nicht, sondern liefert Bruchstücke.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"errors":{"type":"array","items":{"type":"string"}}},"required":["ok","errors"],"additionalProperties":false}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1ZugferdValidate","tags":["Bau · ZUGFeRD"],"parameters":[],"summary":"Validieren","description":"Prüft ein hochgeladenes Dokument (Base64) auf E-Rechnungs-Tauglichkeit. WAS DIESE PRÜFUNG WIRKLICH TUT: Der Rumpf wird nach dem Dekodieren auf 8192 Zeichen ABGESCHNITTEN — alles danach wird nie angesehen. Bei `payloadType: \"pdf\"` besteht die gesamte Prüfung aus dem Vergleich der ersten fünf Zeichen mit `%PDF-` und einer Textsuche nach „factur-x.xml\"; das eingebettete XML wird WEDER ausgepackt NOCH geprüft, `ok: true` heißt hier also nur „fängt mit %PDF- an\". Bei `payloadType: \"xml\"` läuft die echte Regelprüfung; fällt sie aus, greift ein Notbehelf aus zwei regulären Ausdrücken, erkennbar an der Warnung `stub_validator_only`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"payloadBase64":{"type":"string","minLength":20},"payloadType":{"type":"string","enum":["pdf","xml"]},"profile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"],"default":"XRECHNUNG"}},"required":["payloadBase64","payloadType"]},"example":{"payloadBase64":"stringxxxxxxxxxxxxxx","payloadType":"pdf","profile":"EN16931"}}}}}},"/api/v1/zugferd/bulk-generate":{"post":{"responses":{"200":{"description":"Ergebnis je Rechnung. `generated` zählt nur Zeilen mit `status: \"ok\"` — im Notbehelf ohne Datenbank sind das alle (siehe Beschreibung).","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"invoiceId":{"type":"string"},"status":{"type":"string","enum":["ok","error"]},"message":{"type":"string"},"invoiceNumber":{"type":"string"}},"required":["invoiceId","status"],"additionalProperties":false}},"generated":{"type":"integer"},"total":{"type":"integer"},"profile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"]}},"required":["results","generated","total","profile"],"additionalProperties":false},"example":{"results":[{"invoiceId":"string","status":"ok","message":"string","invoiceNumber":"string"}],"generated":0,"total":0,"profile":"EN16931"}}}},"400":{"description":"Validierungsfehler im Rumpf","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"422":{"description":"Eigene Firmen-Stammdaten unvollständig — bricht den GESAMTEN Lauf ab, bevor eine einzelne Rechnung geprüft wird (der Verkäufer ist für alle derselbe).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"seller_stammdaten_incomplete"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}},"operationId":"postApiV1ZugferdBulk-generate","tags":["Bau · ZUGFeRD"],"parameters":[],"summary":"Massenexport","description":"Prüft mehrere Rechnungen (max. 100) am Stück. Trotz des Namens entsteht KEIN Export: es wird weder XML erzeugt noch etwas gespeichert oder zum Abruf bereitgelegt — je Rechnung läuft nur die Regelprüfung, und `message` trägt deren Ergebnis. Teilerfolg ist der Normalfall: der Aufruf antwortet 200, auch wenn jede einzelne Zeile scheiterte. ACHTUNG: Ohne Mandantenkontext oder Datenbank werden ALLE Zeilen ungeprüft als `status: \"ok\"` mit `message: \"stub\"` gemeldet, `generated` entspricht dann der Gesamtzahl — ein Erfolg, hinter dem nichts steht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"invoiceIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":100},"profile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"],"default":"XRECHNUNG"}},"required":["invoiceIds"]},"example":{"invoiceIds":["string"],"profile":"EN16931"}}}}}},"/api/v1/zugferd/settings":{"get":{"responses":{"200":{"description":"Einstellungen des Mandanten oder die Standardwerte","content":{"application/json":{"schema":{"type":"object","properties":{"settings":{"type":"object","properties":{"defaultProfile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"]},"leitwegId":{"type":"string"},"autoGenerateOnInvoice":{"type":"boolean"}},"required":["defaultProfile","autoGenerateOnInvoice"],"additionalProperties":false}},"required":["settings"],"additionalProperties":false},"example":{"settings":{"defaultProfile":"EN16931","leitwegId":"string","autoGenerateOnInvoice":true}}}}},"401":{"description":"Kein Mandantenkontext — als text/plain, ohne JSON-Körper"},"503":{"description":"Datenbankfehler. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiV1ZugferdSettings","tags":["Bau · ZUGFeRD"],"parameters":[],"summary":"Einstellungen lesen","description":"Liest die ZUGFeRD-Einstellungen des Mandanten. Hat der Mandant nie etwas gespeichert, kommen die Standardwerte (`XRECHNUNG`, keine Leitweg-ID, keine automatische Erzeugung) — von hinterlegten Werten nicht unterscheidbar. `leitwegId` fehlt im Rumpf, wenn keine hinterlegt ist (kein `null`)."},"put":{"responses":{"200":{"description":"Gespeichert — `settings` ist der zurückgelesene Stand","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"settings":{"type":"object","properties":{"defaultProfile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"]},"leitwegId":{"type":"string"},"autoGenerateOnInvoice":{"type":"boolean"}},"required":["defaultProfile","autoGenerateOnInvoice"],"additionalProperties":false}},"required":["ok","settings"],"additionalProperties":false},"example":{"ok":true,"settings":{"defaultProfile":"EN16931","leitwegId":"string","autoGenerateOnInvoice":true}}}}},"400":{"description":"Validierungsfehler im Rumpf","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}},"required":["error","message","fields"],"additionalProperties":false}}}},"401":{"description":"Kein Mandantenkontext — als text/plain, ohne JSON-Körper"},"403":{"description":"Keine Manager-Rolle","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Forbidden"},"code":{"type":"string","const":"INSUFFICIENT_ROLE"},"required":{"type":"string"},"actual":{"type":"string"},"message":{"type":"string"}},"required":["error","code","required","actual","message"],"additionalProperties":false}}}},"503":{"description":"Datenbankfehler. Fehlt der Datenbank-Client ganz, kommt derselbe Code ohne JSON-Körper (text/plain).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"putApiV1ZugferdSettings","tags":["Bau · ZUGFeRD"],"parameters":[],"summary":"Einstellungen speichern","description":"Speichert die ZUGFeRD-Einstellungen des Mandanten dauerhaft (eine Zeile je Mandant). `settings` ist der ZURÜCKGELESENE Stand aus der Datenbank, nicht der gesendete Rumpf — die Antwort bestätigt also, was wirklich drinsteht. Der Aufruf ist vollständig ersetzend: ein nicht mitgeschicktes Feld wird auf seinen Standardwert zurückgesetzt, `leitwegId` sogar geleert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"defaultProfile":{"type":"string","enum":["EN16931","XRECHNUNG","EXTENDED"],"default":"XRECHNUNG"},"leitwegId":{"type":"string","maxLength":64},"autoGenerateOnInvoice":{"type":"boolean","default":false}}},"example":{"defaultProfile":"EN16931","leitwegId":"string","autoGenerateOnInvoice":true}}}}}},"/api/v1/gobd/verfahrensdokumentation":{"get":{"responses":{"200":{"description":"HTML-Dokument als Anhang, Dateiname `verfahrensdokumentation-gobd-<Mandant>-<Datum>.html`","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1GobdVerfahrensdokumentation","tags":["gobd"],"parameters":[],"summary":"Verfahrensdokumentation nach GoBD als HTML zum Ausdrucken","description":"Verfahrensdokumentation nach GoBD als HTML (Browser-Print-to-PDF), kein JSON-Rumpf. Gelesen wird nur die Rollenverteilung aus der users-Tabelle des Mandanten, geschrieben wird nichts. Fehlt die users-Tabelle, bleibt die Rollen-Tabelle im Dokument leer — das Dokument kommt trotzdem mit 200. Die Antwort traegt `Cache-Control: no-store`."}},"/api/v1/gobd/idea-export":{"get":{"responses":{"200":{"description":"ZIP-Archiv (index.xml + buchungen.csv) als Anhang, Dateiname `gobd-idea-export-<Mandant>-<Jahr>.zip`","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1GobdIdea-export","tags":["gobd"],"parameters":[{"in":"query","name":"year","schema":{"type":"integer","minimum":2000,"maximum":2100,"description":"Fiscal year. Defaults to the current year."}}],"description":"IDEA/GDPdU-Export des Buchungsjournals als ZIP, kein JSON-Rumpf. Liest die Buchungen des ueber `year` gewaehlten Geschaeftsjahres, schreibt nichts, und legt sie als Beschreibungsdatei `index.xml` plus Datendatei `buchungen.csv` ab. Das Archiv ist unkomprimiert abgelegt (ZIP-Methode „stored\") — IDEA-Werkzeuge lesen das. Die Antwort traegt `Cache-Control: no-store`.","summary":"IDEA/GDPdU-Export des Buchungsjournals als ZIP, kein JSON-Rumpf","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/gobd/sequence-gaps":{"get":{"responses":{"200":{"description":"Lückenanalyse für das angefragte Jahr","content":{"application/json":{"schema":{"type":"object","properties":{"year":{"type":"integer","minimum":2000,"maximum":2100,"description":"Geprueftes Geschaeftsjahr"},"tenantId":{"type":"string","minLength":1,"description":"Mandant, fuer den geprueft wurde"},"gaps":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","description":"Erste fehlende Nummer der Luecke (einschliesslich)"},"to":{"type":"integer","description":"Letzte fehlende Nummer der Luecke (einschliesslich)"},"count":{"type":"integer","minimum":1,"description":"Anzahl fehlender Nummern in dieser Luecke"}},"required":["from","to","count"],"description":"Eine zusammenhaengende Luecke in der Nummernfolge"},"description":"Die gefundenen Luecken; leer wenn die Folge lueckenlos ist"},"lastNumber":{"type":"integer","minimum":0,"description":"Groesste gefundene Nummer; 0 wenn es keine Belege gibt"},"totalIssued":{"type":"integer","minimum":0,"description":"Anzahl unterschiedlicher Nummern im Jahr"},"isComplete":{"type":"boolean","description":"true, wenn keine Luecke gefunden wurde"},"summary":{"type":"string","minLength":1,"description":"Ergebnis im Klartext, mehrzeilig bei Luecken — als Text fuer die Oberflaeche gedacht"},"checkedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung"}},"required":["year","tenantId","gaps","lastNumber","totalIssued","isComplete","summary","checkedAt"]},"example":{"year":2000,"tenantId":"string","gaps":[{"from":0,"to":0,"count":1}],"lastNumber":0,"totalIssued":0,"isComplete":true,"summary":"string","checkedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1GobdSequence-gaps","tags":["gobd"],"parameters":[{"in":"query","name":"year","schema":{"type":"integer","minimum":2000,"maximum":2100,"description":"Fiscal year. Defaults to the current year."}}],"description":"Lückenlose Belegnummernprüfung für ein Geschäftsjahr. Scheitert die Abfrage des Journals, wird auf einer LEEREN Nummernliste gerechnet — die Antwort ist dann `isComplete: true` mit `totalIssued: 0`. Das heißt „nichts geprüft\", nicht „keine Lücken\".","summary":"Lückenlose Belegnummernprüfung für ein Geschäftsjahr","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/me":{"get":{"responses":{"200":{"description":"Profil des angemeldeten Users","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Users; bei Anmeldung ueber einen API-Schluessel `api-key:<Schluesselkennung>`"},"email":{"type":"string","description":"E-Mail-Adresse; bei API-Schluessel-Anmeldung der Platzhalter api@nemix.app"},"name":{"type":["string","null"],"description":"Anzeigename; null wenn keiner hinterlegt ist"},"image":{"type":["string","null"],"description":"Profilbild-Adresse; null wenn keine hinterlegt ist"},"role":{"type":"string","description":"Systemrolle; ohne Eintrag `member`, bei API-Schluessel-Anmeldung `api`"},"tenantId":{"type":["string","null"],"description":"Mandant aus dem Anmeldekontext; null wenn keiner gesetzt ist"},"tenantNumber":{"type":["string","null"],"description":"Human-readable tenant number (M-0001). Null for API-key auth, for a session without a tenant, and for tenants created before migration 20260701090000_tenant_number backfilled them."},"createdAt":{"type":["string","null"],"format":"date-time","description":"Anlagezeitpunkt; bei API-Schluessel-Anmeldung null"},"updatedAt":{"type":["string","null"],"format":"date-time","description":"Letzte Aenderung; bei API-Schluessel-Anmeldung null"}},"required":["id","email","name","image","role","tenantId","tenantNumber","createdAt","updatedAt"],"description":"Profil des angemeldeten Users"},"example":{"id":"string","email":"string","name":"string","image":"string","role":"string","tenantId":"string","tenantNumber":"string","createdAt":"2026-01-01T12:00:00.000Z","updatedAt":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"Nicht authentifiziert"},"404":{"description":"User zur Kennung nicht gefunden (`user_not_found`)"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Me","tags":["me"],"parameters":[],"summary":"Gibt das Profil des aktuell eingeloggten Users zurück","description":"Liest die Zeile des angemeldeten Users aus public.users und ergaenzt den Mandanten aus dem Anmeldekontext. Wer sich mit einem API-Schluessel anmeldet, steht nicht in dieser Tabelle und bekommt ohne Datenbankzugriff ein Ersatzprofil mit der Rolle `api`; die Zeitstempel sind dann null. Ist die Kennung angemeldet, aber in public.users nicht auffindbar, antwortet der Endpunkt 404 mit `user_not_found`."}},"/api/v1/me/permissions":{"get":{"responses":{"200":{"description":"Rolle und abgeleitete Rechtematrix","content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","description":"Aufgeloeste Systemrolle des Users"},"permissions":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string","enum":["view","create","edit","delete","export","approve"]}},"description":"Je Modul die erlaubten Aktionen - die zwoelf Schluessel sind finance, hr, crm, orders, inventory, purchasing, projects, manufacturing, reporting, settings, admin und quotes"}},"required":["role","permissions"],"description":"Rolle und die daraus abgeleitete Rechtematrix"},"example":{"role":"string","permissions":{"beispiel":["view"]}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1MePermissions","tags":["me"],"parameters":[],"summary":"Rolle + aufgelöste Modul-/Aktions-Rechte des eingeloggten Users (RBAC v2)","description":"Liest nur die Rolle aus public.users und leitet daraus die Rechtematrix fuer die zwoelf Oberflaechen-Module ab - ohne Rolleneintrag gilt `user`. Die Matrix ist rein kosmetisch: sie steuert, welche Bedienelemente die Oberflaeche zeigt. Durchgesetzt wird der Zugriff serverseitig an der jeweiligen Route, nicht hier. API-Schluessel-Clients bekommen die Matrix der Rolle `api` ohne Datenbankzugriff."}},"/api/v1/me/preferences":{"get":{"responses":{"200":{"description":"Die gespeicherten Praeferenzen; leeres Objekt, wenn keine vorliegen","content":{"application/json":{"schema":{"type":"object","properties":{"preferences":{"type":"object","additionalProperties":{},"description":"Der gespeicherte Praeferenzen-Block; leeres Objekt, wenn nichts hinterlegt ist"}},"required":["preferences"],"description":"UI-Praeferenzen des Users"},"example":{"preferences":{}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1MePreferences","tags":["me"],"parameters":[],"summary":"Gibt die UI-Präferenzen des Users zurück (Tour-Status, Theme, etc.)","description":"Liest die JSONB-Spalte `preferences` aus public.users und gibt sie unveraendert weiter - der Inhalt ist nicht festgelegt, geschrieben wird er ueber PATCH /me/preferences. Ist noch nichts hinterlegt, kommt ein leeres Objekt. Auch ein Datenbankfehler fuehrt hier bewusst zu einem leeren Objekt mit Status 200 statt zu einem Fehler, damit die Oberflaeche nicht blockiert."},"patch":{"responses":{"200":{"description":"Quittung mit den uebergebenen Feldern","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Uebernahme wurde ausgefuehrt"},"preferences":{"type":"object","additionalProperties":{},"description":"NUR die uebergebenen Felder - nicht der zusammengefuehrte Gesamtstand"}},"required":["ok","preferences"],"description":"Quittung der Praeferenz-Aenderung"},"example":{"ok":true,"preferences":{}}}}},"400":{"description":"Validation error"},"401":{"description":"Nicht authentifiziert"}},"operationId":"patchApiV1MePreferences","tags":["me"],"parameters":[],"summary":"Aktualisiert die UI-Präferenzen des Users (merge, kein Replace)","description":"Fuehrt die uebergebenen Felder per JSONB-Verknuepfung mit dem bestehenden Block zusammen - nicht genannte Felder bleiben stehen, und `updated_at` wird neu gesetzt. `preferences` in der Antwort enthaelt NUR die uebergebenen Felder, nicht den zusammengefuehrten Gesamtstand; den liefert erst ein erneutes GET. Scheitert das Schreiben, antwortet der Endpunkt trotzdem mit 200 und `ok: true`, damit die Oberflaeche nicht blockiert - eine Bestaetigung ist hier also kein Beweis, dass geschrieben wurde.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tourCompleted":{"type":"boolean"},"tourStep":{"type":"integer","minimum":0},"sidebarCollapsed":{"type":"boolean"},"language":{"type":"string","maxLength":10},"theme":{"type":"string","enum":["light","dark","system"]},"notifications":{"type":"object","additionalProperties":{"type":"boolean"}},"extra":{"type":"object","additionalProperties":{}}}},"example":{"tourCompleted":true,"tourStep":0,"sidebarCollapsed":true,"language":"string","theme":"light","notifications":{"beispiel":true},"extra":{}}}}}}},"/api/v1/me/signature":{"get":{"responses":{"200":{"description":"Die Signatur des Users; leer, wenn keine gepflegt ist","content":{"application/json":{"schema":{"type":"object","properties":{"signature":{"type":"object","properties":{"text":{"type":"string","description":"Signaturtext; leere Zeichenkette wenn keiner gepflegt ist"},"logoUrl":{"type":["string","null"],"description":"data:-URI oder https-Adresse des Logos; null wenn keines hinterlegt ist"},"enabled":{"type":"boolean","description":"Ob die Signatur an ausgehende Mails angehaengt wird"}},"required":["text","logoUrl","enabled"],"description":"E-Mail-Signatur des Users"}},"required":["signature"]},"example":{"signature":{"text":"string","logoUrl":"string","enabled":true}}}}},"401":{"description":"Nicht authentifiziert"}},"operationId":"getApiV1MeSignature","tags":["me"],"parameters":[],"summary":"Gibt die E-Mail-Signatur des aktuell eingeloggten Users zurück","description":"Die Signatur liegt im Praeferenzen-Block desselben Users unter `emailSignature`, nicht in einer eigenen Tabelle. Fehlt sie, ist die Datenbank nicht erreichbar oder meldet sich ein API-Schluessel an, kommt die leere Signatur (leerer Text, kein Logo, `enabled: false`) mit Status 200 - kein Fehler."},"put":{"responses":{"200":{"description":"Die gespeicherte Signatur","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Signatur wurde geschrieben"},"signature":{"type":"object","properties":{"text":{"type":"string","description":"Signaturtext; leere Zeichenkette wenn keiner gepflegt ist"},"logoUrl":{"type":["string","null"],"description":"data:-URI oder https-Adresse des Logos; null wenn keines hinterlegt ist"},"enabled":{"type":"boolean","description":"Ob die Signatur an ausgehende Mails angehaengt wird"}},"required":["text","logoUrl","enabled"],"description":"Die Signatur, wie sie gespeichert wurde"}},"required":["ok","signature"]},"example":{"ok":true,"signature":{"text":"string","logoUrl":"string","enabled":true}}}}},"400":{"description":"Validation error - oder Anmeldung ueber API-Schluessel (`api_key_cannot_set_signature`)"},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"putApiV1MeSignature","tags":["me"],"parameters":[],"summary":"Aktualisiert die E-Mail-Signatur des Users (Text + optionales Logo)","description":"Schreibt Text, Logo und Ein-/Aus-Schalter in den Praeferenzen-Block des Users. Es ist ein vollstaendiges Ersetzen: weggelassene Felder werden auf ihren Vorgabewert gesetzt (leerer Text, kein Logo, ausgeschaltet), nicht auf den bisherigen Stand. `logoUrl` nimmt eine data:image-URI oder eine oeffentliche https-Adresse, bis 2 MB. Ein API-Schluessel darf keine Signatur setzen und erhaelt 400 `api_key_cannot_set_signature`; scheitert das Schreiben, kommt 503.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","maxLength":5000,"default":""},"logoUrl":{"type":["string","null"],"maxLength":2000000,"default":null},"enabled":{"type":"boolean","default":false}}},"example":{"text":"string","logoUrl":"string","enabled":true}}}}}},"/api/v1/theme":{"get":{"responses":{"200":{"description":"Theme-Konfiguration — leer, wenn nichts hinterlegt ist ODER die Datenbank fehlt","content":{"application/json":{"schema":{"type":"object","properties":{"theme":{"type":"object","additionalProperties":{}}},"required":["theme"],"additionalProperties":false},"example":{"theme":{}}}}},"400":{"description":"`tenant_required` — kein Mandanten-Kontext"},"401":{"description":"Unauthorized"}},"operationId":"getApiV1Theme","tags":["theme"],"parameters":[],"summary":"Gibt das aktuelle Tenant-Theme zurück","description":"Liest `settings->theme` aus der Zeile des Mandanten in `public.tenants`. Ist dort nichts hinterlegt, kommt ein leeres Objekt. Faellt die Datenbank aus, antwortet die Route EBENFALLS mit 200 und einem leeren Theme statt mit einem Fehler — leer heisst hier also nicht zwingend, dass nichts gespeichert ist. Ohne Mandanten-Kontext kommt 400 `tenant_required`."},"post":{"responses":{"200":{"description":"Gespeichert — `theme` ist der gesendete Block, nicht der zusammengefuehrte Stand","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"theme":{"type":"object","additionalProperties":{}}},"required":["ok","theme"],"additionalProperties":false},"example":{"ok":true,"theme":{}}}}},"400":{"description":"Validation error oder kein Mandanten-Kontext"},"401":{"description":"Unauthorized"},"500":{"description":"`save_failed` — nichts gespeichert"}},"operationId":"postApiV1Theme","tags":["theme"],"parameters":[],"summary":"Speichert das Tenant-Theme (merge in settings.theme)","description":"Fuehrt die gesendeten Werte mit dem gespeicherten Theme ZUSAMMEN: nicht gesendete Schluessel bleiben stehen, gesendete werden ueberschrieben. Das gilt nur fuer die oberste Ebene — ein verschachteltes Objekt wird als Ganzes ersetzt, nicht tief gemischt. Unbekannte Schluessel laesst die Pruefung durch und speichert sie mit. Die Antwort enthaelt NUR das gerade Gesendete, nicht das zusammengefuehrte Ergebnis. Ohne Mandanten-Kontext kommt 400, bei einem Schreibfehler 500 `save_failed`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"primaryColor":{"type":"string","maxLength":32},"accentColor":{"type":"string","maxLength":32},"backgroundColor":{"type":"string","maxLength":32},"fontFamily":{"type":"string","maxLength":128},"logoUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"faviconUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"borderRadius":{"type":"string","enum":["none","sm","md","lg","full"]},"darkMode":{"type":"boolean"},"extra":{"type":"object","additionalProperties":{}}},"additionalProperties":true},"example":{"primaryColor":"string","accentColor":"string","backgroundColor":"string","fontFamily":"string","logoUrl":"https://example.com","faviconUrl":"https://example.com","borderRadius":"none","darkMode":true,"extra":{}}}}}}},"/api/v1/cors-origins":{"get":{"responses":{"200":{"description":"Liste der freigegebenen Herkünfte.","content":{"application/json":{"schema":{"type":"object","properties":{"origins":{"type":"array","items":{"type":"string"}},"max":{"type":"integer"}},"required":["origins","max"]},"example":{"origins":["string"],"max":0}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Nur für Administratoren"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Cors-origins","tags":["settings"],"parameters":[],"description":"Listet die eigenen Herkünfte (CORS-Origins) des Mandanten. Braucht mindestens die Rolle admin. Gelesen wird das Feld cors_origins aus public.tenants.settings und vor der Ausgabe bereinigt — ungültige Einträge aus älteren Ständen fallen dabei still weg, die Liste kann also kürzer sein als die gespeicherte. max nennt die Obergrenze, die PUT annimmt.","summary":"Listet die eigenen Herkünfte (CORS-Origins) des Mandanten","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Gespeicherte Liste.","content":{"application/json":{"schema":{"type":"object","properties":{"origins":{"type":"array","items":{"type":"string"}},"abgelehnt":{"type":"array","items":{"type":"string"}}},"required":["origins","abgelehnt"]},"example":{"origins":["string"],"abgelehnt":["string"]}}}},"400":{"description":"Keine gültige Herkunft in der Eingabe"},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Nur für Administratoren"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1Cors-origins","tags":["settings"],"parameters":[],"description":"Ersetzt die Liste der eigenen Herkünfte. Erlaubt sind ausschließlich exakte Adressen der Form https://host[:port] (http nur für localhost). Platzhalter wie *.example.com werden abgelehnt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"origins":{"type":"array","items":{"type":"string","minLength":1,"maxLength":255},"maxItems":10}},"required":["origins"]},"example":{"origins":["string"]}}}},"summary":"Ersetzt die Liste der eigenen Herkünfte","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/uploads/presign":{"post":{"responses":{"200":{"description":"Pre-signed URL + storage key","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"Ziel des direkten PUT-Uploads"},"key":{"type":"string","description":"Ablagepfad tenants/<tenantId>/<purpose>/<uuid>-<dateiname>"},"publicUrl":{"type":"string","description":"Adresse, unter der die Datei nach dem Upload gelesen wird"},"expiresIn":{"type":"integer","description":"Gueltigkeit der signierten URL in Sekunden"},"method":{"type":"string","const":"PUT","description":"HTTP-Verfahren, mit dem hochzuladen ist"},"contentType":{"type":"string","description":"Content-Type, auf den die Signatur festgelegt ist"}},"required":["url","key","publicUrl","expiresIn","method","contentType"]},"example":{"url":"string","key":"string","publicUrl":"string","expiresIn":0,"method":"PUT","contentType":"string"}}}},"400":{"description":"Validation error, fehlender Mandant oder unerlaubter Content-Type"},"401":{"description":"Unauthorized"}},"operationId":"postApiV1UploadsPresign","tags":["uploads"],"parameters":[],"summary":"Signierte URL fuer einen direkten Datei-Upload anfordern","description":"Signiert einen direkten PUT-Upload nach S3 bzw. R2 und gibt die URL samt Ablagepfad zurueck; die Datei selbst laeuft nicht durch diese API. Der Pfad lautet `tenants/<tenantId>/<purpose>/<uuid>-<dateiname>`, der Dateiname wird dabei auf `[A-Za-z0-9._-]` entschaerft, und die Signatur gilt 300 Sekunden. Je `purpose` ist nur eine feste Liste von Content-Types erlaubt (`other` laesst alle zu); ein anderer Typ wird mit 400 und der erlaubten Liste abgewiesen. Ist kein Bucket konfiguriert oder scheitert das Signieren, antwortet der Endpunkt in gleicher Form mit einer lokalen Stub-URL statt mit einem Fehler.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"contentType":{"type":"string","minLength":1,"maxLength":128},"purpose":{"type":"string","enum":["logo","avatar","document","import","other"],"default":"other"}},"required":["filename","contentType"]},"example":{"filename":"string","contentType":"string","purpose":"logo"}}}}}},"/api/v1/banking":{"get":{"responses":{"200":{"description":"Bankverbindungen des Mandanten, neueste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"connections":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der Bankverbindung"},"provider":{"type":"string","minLength":1,"description":"Herkunft der Verbindung: \"finapi\" bei Open Banking, \"camt053\" bei Datei-Import"},"iban":{"type":["string","null"],"description":"IBAN des Kontos; null wenn die Quelle keine nannte"},"bic":{"type":["string","null"],"description":"BIC des Kontos; null wenn nicht erfasst"},"bank_name":{"type":["string","null"],"description":"Name der Bank; null wenn nicht bekannt"},"last_sync_at":{"type":["string","null"],"format":"date-time","description":"Letzter erfolgreicher Abgleich; null wenn nie abgeglichen"},"sync_status":{"type":["string","null"],"description":"Zustand der Verbindung, z. B. \"active\", \"synced\" oder \"consent_required\"; null wenn nie gesetzt"},"created_at":{"type":["string","null"],"format":"date-time","description":"Anlagezeitpunkt der Verbindung"},"external_ref":{"type":["string","null"],"description":"Kontokennung beim Anbieter; null bei Verbindungen aus einem Datei-Import"},"provider_connection_ref":{"type":["string","null"],"description":"Bankverbindungs-Kennung beim Anbieter; null bei Verbindungen aus einem Datei-Import"},"consent_expires_at":{"type":["string","null"],"format":"date-time","description":"Ablauf der Bank-Einwilligung; null wenn keine Einwilligung noetig ist"},"updated_at":{"type":["string","null"],"format":"date-time","description":"Letzte Aenderung an der Verbindung"}},"required":["id","provider","iban","bic","bank_name","last_sync_at","sync_status","created_at","external_ref","provider_connection_ref","consent_expires_at","updated_at"],"additionalProperties":false}}},"required":["connections"],"additionalProperties":false},"example":{"connections":[{"id":"00000000-0000-4000-8000-000000000000","provider":"string","iban":"string","bic":"string","bank_name":"string","last_sync_at":"2026-01-01T12:00:00.000Z","sync_status":"string","created_at":"2026-01-01T12:00:00.000Z","external_ref":"string","provider_connection_ref":"string","consent_expires_at":"2026-01-01T12:00:00.000Z","updated_at":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"Unauthorized"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Banking","tags":["banking"],"parameters":[],"summary":"Liste aller Banking-Verbindungen des Tenants","description":"Gibt ALLE Verbindungen in EINER Antwort zurueck — ohne Blaetterung, ohne Filter und einschlieszlich abgelaufener oder nie abgeglichener —, zuletzt angelegte zuerst. `provider` unterscheidet die Herkunft: „finapi\" fuer Open Banking, „camt053\" fuer den Datei-Import; bei letzterem bleiben die Anbieter-Kennungen leer. Ob eine Verbindung noch traegt, sagen `sync_status`, `last_sync_at` und `consent_expires_at` — nicht ihr Vorhandensein. Der Umschlag heiszt `connections`, nicht `data`. Rein lesend: es wird kein Abgleich angestoszen und keine Bank kontaktiert."}},"/api/v1/banking/import/camt":{"post":{"responses":{"201":{"description":"Import erfolgreich. Die Verbindung wird IMMER angelegt — auch wenn alle Umsätze schon vorlagen (`imported: 0`, `skipped` gleich `total`).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Datei wurde eingelesen"},"connectionId":{"type":"string","format":"uuid","description":"Kennung der neu angelegten Bankverbindung"},"iban":{"type":"string","description":"IBAN aus dem Kontoauszug; LEERER String, wenn die Datei keine nannte"},"imported":{"type":"integer","minimum":0,"description":"Anzahl neu gespeicherter Umsaetze"},"skipped":{"type":"integer","minimum":0,"description":"Anzahl uebersprungener Umsaetze — sie lagen bereits vor (Dubletten-Hash)"},"total":{"type":"integer","minimum":0,"description":"Anzahl Umsaetze in der Datei, imported plus skipped"}},"required":["ok","connectionId","iban","imported","skipped","total"]},"example":{"ok":true,"connectionId":"00000000-0000-4000-8000-000000000000","iban":"string","imported":0,"skipped":0,"total":0}}}},"400":{"description":"Validierungsfehler, malformed XML oder keine Transaktionen — vier Formen, zwei davon mit `details`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","enum":["file required","invalid form data","no transactions found in CAMT file"],"description":"Grund der Ablehnung als englischer Klartext, nicht als Kennung"}},"required":["error"],"description":"Ablehnung ohne weitere Angaben"},{"type":"object","properties":{"error":{"type":"string","const":"invalid camt file","description":"Die Datei liess sich nicht als CAMT.053 lesen"},"details":{"type":"string","description":"Meldung des Parsers"}},"required":["error","details"],"description":"Ablehnung mit Parser-Meldung"}]}}}},"401":{"description":"Unauthorized"},"500":{"description":"Import abgebrochen — die Meldung der Ausnahme geht mit hinaus","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"import failed","description":"Grund als englischer Klartext, nicht als Kennung"},"details":{"type":"string","description":"Meldung der zugrunde liegenden Ausnahme — sie geht hier an den Aufrufer hinaus"}},"required":["error","details"]}}}},"503":{"description":"Datenbank nicht verfügbar (Klartext)"}},"operationId":"postApiV1BankingImportCamt","tags":["banking"],"parameters":[],"summary":"Importiert eine CAMT.053-Datei und legt die Umsaetze an","description":"CAMT.053-Datei (ISO 20022 XML) importieren: legt eine Banking-Verbindung an und speichert Transaktionen."}},"/api/v1/banking/transactions":{"get":{"responses":{"200":{"description":"Umsätze der aktuellen Seite mit Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"transactions":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Die Umsaetze der aktuellen Seite als Rohzeilen (snake_case). Die Spaltenmenge ist hier NICHT zugesagt: die Abfrage liest SELECT * und der Umfang haengt am Wanderungsstand des Mandanten."},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Umsaetze, die dem Filter entsprechen"}},"required":["limit","offset","total"],"description":"Seitenangaben"}},"required":["transactions","pagination"]},"example":{"transactions":[{}],"pagination":{"limit":1,"offset":0,"total":0}}}}},"401":{"description":"Unauthorized"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"getApiV1BankingTransactions","tags":["banking"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"connectionId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"status","schema":{"type":"string"}}],"description":"Importierte Banking-Transaktionen mit Pagination. Die Zeilen kommen roh aus der Tabelle (SELECT *), daher snake_case — die Spaltenmenge ist bewusst nicht zugesagt.","summary":"Importierte Banking-Transaktionen mit Pagination","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/banking/summary":{"get":{"responses":{"200":{"description":"Kennzahlen über alle Umsätze und Verbindungen","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer","minimum":0,"description":"Anzahl aller Umsaetze, ueber alle Zustaende"},"matched":{"type":"integer","minimum":0,"description":"Anzahl zugeordneter Umsaetze"},"unmatched":{"type":"integer","minimum":0,"description":"Anzahl offener Umsaetze"},"matchedAmount":{"type":"number","description":"Summe der zugeordneten Betraege"},"unmatchedAmount":{"type":"number","description":"Summe der offenen Betraege"},"connectionCount":{"type":"integer","minimum":0,"description":"Anzahl Bankverbindungen des Mandanten"}},"required":["total","matched","unmatched","matchedAmount","unmatchedAmount","connectionCount"]},"example":{"total":0,"matched":0,"unmatched":0,"matchedAmount":0,"unmatchedAmount":0,"connectionCount":0}}}},"401":{"description":"Unauthorized"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"getApiV1BankingSummary","tags":["banking"],"parameters":[],"description":"Banking-Übersicht: Anzahl gesamt / gematcht / ungematcht. Gezählt wird über ALLE Umsätze des Mandanten, nicht über eine Seite. `total` zählt auch Zustände jenseits von matched/unmatched mit — deren Beträge tauchen in keiner der beiden Summen auf.","summary":"Banking-Übersicht: Anzahl gesamt / gematcht / ungematcht","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/banking/transactions/{id}/suggestions":{"get":{"responses":{"200":{"description":"Vorschläge — oder eine leere Liste mit `note: \"already_matched\"`, wenn nicht gesucht wurde","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"suggestions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der vorgeschlagenen Rechnung"},"invoice_number":{"type":["string","null"],"description":"Rechnungsnummer; null wenn noch keine vergeben ist"},"total_amount":{"type":"string","description":"Rechnungssumme — numeric, kommt als Zeichenkette"},"due_date":{"type":["string","null"],"format":"date-time","description":"Faelligkeit; DATE-Spalte, kommt als ISO-Zeitstempel. null wenn nicht gesetzt"},"status":{"type":"string","enum":["sent","overdue","partially_paid"],"description":"Nur offene Rechnungen werden vorgeschlagen"},"open_amount":{"type":"string","description":"Offener Restbetrag (Summe minus bereits gezahlt), als Zeichenkette"},"amount_diff":{"type":"string","description":"Abstand zwischen Restbetrag und Umsatzbetrag; die Liste ist danach sortiert"}},"required":["id","invoice_number","total_amount","due_date","status","open_amount","amount_diff"],"description":"Eine Rechnung, deren offener Rest zum Umsatzbetrag passt (Toleranz 1 %)"},"maxItems":10,"description":"Passende offene Rechnungen; leer, wenn keine im Toleranzbereich liegt"}},"required":["suggestions"],"description":"Zuordnungsvorschlaege"},{"type":"object","properties":{"suggestions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung der vorgeschlagenen Rechnung"},"invoice_number":{"type":["string","null"],"description":"Rechnungsnummer; null wenn noch keine vergeben ist"},"total_amount":{"type":"string","description":"Rechnungssumme — numeric, kommt als Zeichenkette"},"due_date":{"type":["string","null"],"format":"date-time","description":"Faelligkeit; DATE-Spalte, kommt als ISO-Zeitstempel. null wenn nicht gesetzt"},"status":{"type":"string","enum":["sent","overdue","partially_paid"],"description":"Nur offene Rechnungen werden vorgeschlagen"},"open_amount":{"type":"string","description":"Offener Restbetrag (Summe minus bereits gezahlt), als Zeichenkette"},"amount_diff":{"type":"string","description":"Abstand zwischen Restbetrag und Umsatzbetrag; die Liste ist danach sortiert"}},"required":["id","invoice_number","total_amount","due_date","status","open_amount","amount_diff"],"description":"Eine Rechnung, deren offener Rest zum Umsatzbetrag passt (Toleranz 1 %)"},"maxItems":0,"description":"Immer leer — es wurde nicht gesucht"},"note":{"type":"string","const":"already_matched","description":"Der Umsatz ist bereits einer Rechnung zugeordnet; die leere Liste heisst nicht \"nichts gefunden\""}},"required":["suggestions","note"],"description":"Keine Suche, weil der Umsatz schon zugeordnet ist"}]},"example":{"suggestions":[{"id":"00000000-0000-4000-8000-000000000000","invoice_number":"string","total_amount":"string","due_date":"2026-01-01T12:00:00.000Z","status":"sent","open_amount":"string","amount_diff":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Transaction not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"transaction_not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"getApiV1BankingTransactionsByIdSuggestions","tags":["banking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Schlaegt offene Rechnungen vor, deren Restbetrag zur Buchung passt","description":"Zuordnungsvorschläge: offene Rechnungen, deren RESTBETRAG (Summe minus bereits gezahlt) auf 1 % genau zum Transaktionsbetrag passt. Ist der Umsatz bereits zugeordnet, kommt eine leere Liste MIT `note: \"already_matched\"` — dann wurde gar nicht gesucht."}},"/api/v1/banking/transactions/{id}/match":{"patch":{"responses":{"200":{"description":"Matched — Zahlung gebucht und Mahnstand nachgeführt","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Zuordnung wurde gebucht"},"transactionId":{"type":"string","format":"uuid","description":"Kennung des zugeordneten Umsatzes"},"invoiceId":{"type":"string","format":"uuid","description":"Kennung der Rechnung, auf die gebucht wurde"}},"required":["ok","transactionId","invoiceId"]},"example":{"ok":true,"transactionId":"00000000-0000-4000-8000-000000000000","invoiceId":"00000000-0000-4000-8000-000000000000"}}}},"400":{"description":"Der Umsatz ist bereits zugeordnet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"already_matched","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Umsatz oder Rechnung nicht gefunden — die Kennung sagt, welches von beiden","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"transaction_not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]},{"type":"object","properties":{"error":{"type":"string","const":"invoice_not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}]}}}},"409":{"description":"Rechnung nicht offen — es wurde nichts gebucht","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"invoice_not_open","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Klartext: warum nicht zugeordnet wurde"}},"required":["error","message"]}}}},"500":{"description":"Zuordnung abgebrochen und zurückgerollt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["match_failed","unmatch_failed"],"description":"Fehlerkennung — match_failed beim Zuordnen, unmatch_failed beim Aufheben"},"message":{"type":"string","description":"Meldung der zugrunde liegenden Ausnahme — sie geht hier an den Aufrufer hinaus"}},"required":["error","message"]}}}},"503":{"description":"Datenbank nicht verfügbar — Kennung `db_unavailable`, nicht `database_unavailable`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"db_unavailable","description":"Fehlerkennung fuer die Auswertung — hier ohne \"data\", anders als bei den Leseaufrufen"}},"required":["error"]}}}}},"operationId":"patchApiV1BankingTransactionsByIdMatch","tags":["banking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Banküberweisung einer Rechnung zuordnen (Reconciliation). Bucht den Betrag auf `paid_amount` und setzt die Rechnung je nach Restbetrag auf „paid\" oder „partially_paid\". Nur offene Rechnungen (sent, overdue, partially_paid) werden angenommen — sonst 409.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"invoiceId":{"type":"string","format":"uuid"},"note":{"type":"string","maxLength":500}},"required":["invoiceId"]},"example":{"invoiceId":"00000000-0000-4000-8000-000000000000","note":"string"}}}},"summary":"Banküberweisung einer Rechnung zuordnen (Reconciliation)","x-nemix-summary-source":"description:first-sentence"},"delete":{"responses":{"200":{"description":"Unmatched — der Umsatz ist wieder offen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Zuordnung wurde aufgehoben"},"transactionId":{"type":"string","format":"uuid","description":"Kennung des wieder offenen Umsatzes"}},"required":["ok","transactionId"]},"example":{"ok":true,"transactionId":"00000000-0000-4000-8000-000000000000"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Umsatz nicht gefunden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"transaction_not_found","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}},"500":{"description":"Aufhebung abgebrochen und zurückgerollt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["match_failed","unmatch_failed"],"description":"Fehlerkennung — match_failed beim Zuordnen, unmatch_failed beim Aufheben"},"message":{"type":"string","description":"Meldung der zugrunde liegenden Ausnahme — sie geht hier an den Aufrufer hinaus"}},"required":["error","message"]}}}},"503":{"description":"Datenbank nicht verfügbar — Kennung `db_unavailable`, nicht `database_unavailable`","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"db_unavailable","description":"Fehlerkennung fuer die Auswertung — hier ohne \"data\", anders als bei den Leseaufrufen"}},"required":["error"]}}}}},"operationId":"deleteApiV1BankingTransactionsByIdMatch","tags":["banking"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Zuordnung einer Transaktion aufheben. Der gebuchte Betrag wird von `paid_amount` der Rechnung wieder abgezogen und der Mahnstand nachgeführt.","summary":"Zuordnung einer Transaktion aufheben","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/banking/connect/finapi":{"post":{"responses":{"200":{"description":"WebForm erstellt — die webFormId kommt nur hier zurück","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Adresse der finAPI-WebForm fuer den Bank-Login"},"webFormId":{"type":"string","minLength":1,"description":"Kennung der WebForm. Sie kommt NUR hier zurueck — das Frontend muss sie sich merken und nach dem Redirect an /connect/finapi/complete weiterreichen."}},"required":["url","webFormId"]},"example":{"url":"https://example.com","webFormId":"string"}}}},"401":{"description":"Unauthorized"},"501":{"description":"finAPI nicht konfiguriert — es wurde nichts an die Bank geschickt, es gibt keine WebForm","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"finapi_not_configured","description":"Fehlerkennung fuer die Auswertung"},"demo":{"type":"boolean","const":true,"description":"Der Server laeuft ohne finAPI-Zugangsdaten. Es wurde KEIN Aufruf an die Bank gemacht."}},"required":["error","demo"]}}}},"502":{"description":"finAPI-Aufruf fehlgeschlagen — Ursache nur im Serverlog","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"finapi_error","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Neutraler Klartext. Die eigentliche Ursache steht ABSICHTLICH nur im Serverlog (CWE-209)."}},"required":["error","message"]}}}}},"operationId":"postApiV1BankingConnectFinapi","tags":["banking"],"parameters":[],"summary":"Startet den finAPI-Zustimmungsablauf und liefert die WebForm-Adresse","description":"finAPI-Consent-Flow starten: legt (einmalig) den technischen finAPI-User des Tenants an und liefert die WebForm-URL für den Bank-Login."}},"/api/v1/banking/connect/finapi/complete":{"post":{"responses":{"200":{"description":"Konten verbunden und erstmals abgeglichen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die WebForm war abgeschlossen und die Konten wurden uebernommen"},"accounts":{"type":"integer","minimum":0,"description":"Anzahl angelegter oder aktualisierter Bankverbindungen"},"sync":{"type":"array","items":{"type":"object","properties":{"connectionId":{"type":"string","format":"uuid","description":"Kennung der abgeglichenen Verbindung"},"newTransactions":{"type":"integer","minimum":0,"description":"Anzahl neu uebernommener Umsaetze"},"balance":{"type":["number","null"],"description":"Zuletzt gezogener Saldo; null wenn finAPI keinen lieferte"},"error":{"type":"string","minLength":1,"description":"Grund, falls diese eine Verbindung scheiterte; Schluessel FEHLT im Erfolgsfall"}},"required":["connectionId","newTransactions","balance"],"description":"Das Ergebnis fuer EINE Bankverbindung"},"description":"Ergebnis des Erst-Abgleichs je Verbindung; leer wenn nichts abzugleichen war"}},"required":["ok","accounts","sync"]},"example":{"ok":true,"accounts":0,"sync":[{"connectionId":"00000000-0000-4000-8000-000000000000","newTransactions":0,"balance":0,"error":"string"}]}}}},"400":{"description":"WebForm nicht abgeschlossen — es wurde nichts übernommen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","pattern":"^webform_","description":"Fehlerkennung \"webform_\" plus dem finAPI-Status, z. B. webform_NOT_YET_OPENED"}},"required":["error"]}}}},"401":{"description":"Unauthorized"},"501":{"description":"finAPI nicht konfiguriert — es wurde nichts an die Bank geschickt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"finapi_not_configured","description":"Fehlerkennung fuer die Auswertung"},"demo":{"type":"boolean","const":true,"description":"Der Server laeuft ohne finAPI-Zugangsdaten. Es wurde KEIN Aufruf an die Bank gemacht."}},"required":["error","demo"]}}}},"502":{"description":"finAPI-Aufruf fehlgeschlagen — Ursache nur im Serverlog","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"finapi_error","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Neutraler Klartext. Die eigentliche Ursache steht ABSICHTLICH nur im Serverlog (CWE-209)."}},"required":["error","message"]}}}},"503":{"description":"Datenbank nicht verfügbar (Klartext)"}},"operationId":"postApiV1BankingConnectFinapiComplete","tags":["banking"],"parameters":[],"summary":"Schliesst das finAPI-WebForm ab und legt die Konten an","description":"finAPI-WebForm abschließen: Konten aus dem Import als banking_connections upserten und einen Erst-Sync fahren.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"webFormId":{"type":"string","minLength":1,"maxLength":100}},"required":["webFormId"]},"example":{"webFormId":"string"}}}}}},"/api/v1/banking/sync-now":{"post":{"responses":{"200":{"description":"Sync gelaufen — `results` ist leer, wenn es nichts abzugleichen gab","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Der Abgleich ist durchgelaufen"},"results":{"type":"array","items":{"type":"object","properties":{"connectionId":{"type":"string","format":"uuid","description":"Kennung der abgeglichenen Verbindung"},"newTransactions":{"type":"integer","minimum":0,"description":"Anzahl neu uebernommener Umsaetze"},"balance":{"type":["number","null"],"description":"Zuletzt gezogener Saldo; null wenn finAPI keinen lieferte"},"error":{"type":"string","minLength":1,"description":"Grund, falls diese eine Verbindung scheiterte; Schluessel FEHLT im Erfolgsfall"}},"required":["connectionId","newTransactions","balance"],"description":"Das Ergebnis fuer EINE Bankverbindung"},"description":"Ergebnis je Verbindung; LEER, wenn der Mandant keine finAPI-Verbindung hat"}},"required":["ok","results"]},"example":{"ok":true,"results":[{"connectionId":"00000000-0000-4000-8000-000000000000","newTransactions":0,"balance":0,"error":"string"}]}}}},"401":{"description":"Unauthorized"},"501":{"description":"finAPI nicht konfiguriert — es wurde nichts an die Bank geschickt","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"finapi_not_configured","description":"Fehlerkennung fuer die Auswertung"},"demo":{"type":"boolean","const":true,"description":"Der Server laeuft ohne finAPI-Zugangsdaten. Es wurde KEIN Aufruf an die Bank gemacht."}},"required":["error","demo"]}}}},"502":{"description":"finAPI-Aufruf fehlgeschlagen — Ursache nur im Serverlog","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"finapi_error","description":"Fehlerkennung fuer die Auswertung"},"message":{"type":"string","minLength":1,"description":"Neutraler Klartext. Die eigentliche Ursache steht ABSICHTLICH nur im Serverlog (CWE-209)."}},"required":["error","message"]}}}}},"operationId":"postApiV1BankingSync-now","tags":["banking"],"parameters":[],"description":"Manueller Sofort-Sync aller finAPI-Verbindungen des Tenants. Hat der Mandant keine finAPI-Verbindung, kommt 200 mit LEEREM `results` — es wurde dann nichts abgerufen.","summary":"Manueller Sofort-Sync aller finAPI-Verbindungen des Tenants","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/banking/balances":{"get":{"responses":{"200":{"description":"Salden je Verbindung — ENTWEDER `mode: \"live\"` (echte Snapshots) ODER `mode: \"demo\"` (nichts geholt, Beträge erfunden).","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"mode":{"type":"string","const":"live","description":"Die Salden stammen aus einem finAPI-Snapshot"},"balances":{"type":"array","items":{"type":"object","properties":{"connectionId":{"type":"string","format":"uuid","description":"Kennung der Bankverbindung"},"provider":{"type":"string","minLength":1,"description":"Herkunft der Verbindung, z. B. \"finapi\" oder ein Datei-Import"},"iban":{"type":["string","null"],"description":"IBAN des Kontos; null wenn die Bank keine lieferte"},"bankName":{"type":["string","null"],"description":"Name der Bank; null wenn nicht bekannt"},"balance":{"type":["number","null"],"description":"Kontostand; null bei Verbindungen ohne Saldo, etwa aus einem Datei-Import"},"available":{"type":["number","null"],"description":"Verfuegbarer Betrag; null wenn die Bank keinen lieferte"},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"Waehrung nach ISO 4217; EUR wenn nichts anderes bekannt ist"},"balanceDate":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Stichtag des Saldos (YYYY-MM-DD); null wenn kein Snapshot vorliegt"},"fetchedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Abrufs bei der Bank; null wenn nie abgerufen"},"syncStatus":{"type":["string","null"],"description":"Zustand der Verbindung, z. B. \"active\" oder \"consent_required\"; null wenn nie gesetzt"},"isDemo":{"type":"boolean","description":"true, wenn dieser EINE Saldo erfunden ist. Achtung: im Demo-Modus koennen beide Werte vorkommen."}},"required":["connectionId","provider","iban","bankName","balance","available","currency","balanceDate","fetchedAt","syncStatus","isDemo"],"description":"Der zuletzt bekannte Saldo einer Bankverbindung"},"description":"Ein Eintrag je Bankverbindung des Mandanten"}},"required":["mode","balances"],"description":"Echte Salden"},{"type":"object","properties":{"mode":{"type":"string","const":"demo","description":"Es liegen KEINE Live-Daten vor: finAPI ist nicht konfiguriert, es wurde nichts abgerufen. Die Betraege der finAPI-Verbindungen sind erfunden (deterministisch), damit die Oberflaeche etwas anzeigen kann. Verbindungen anderer Herkunft stehen mit balance null."},"balances":{"type":"array","items":{"type":"object","properties":{"connectionId":{"type":"string","format":"uuid","description":"Kennung der Bankverbindung"},"provider":{"type":"string","minLength":1,"description":"Herkunft der Verbindung, z. B. \"finapi\" oder ein Datei-Import"},"iban":{"type":["string","null"],"description":"IBAN des Kontos; null wenn die Bank keine lieferte"},"bankName":{"type":["string","null"],"description":"Name der Bank; null wenn nicht bekannt"},"balance":{"type":["number","null"],"description":"Kontostand; null bei Verbindungen ohne Saldo, etwa aus einem Datei-Import"},"available":{"type":["number","null"],"description":"Verfuegbarer Betrag; null wenn die Bank keinen lieferte"},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"Waehrung nach ISO 4217; EUR wenn nichts anderes bekannt ist"},"balanceDate":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Stichtag des Saldos (YYYY-MM-DD); null wenn kein Snapshot vorliegt"},"fetchedAt":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt des Abrufs bei der Bank; null wenn nie abgerufen"},"syncStatus":{"type":["string","null"],"description":"Zustand der Verbindung, z. B. \"active\" oder \"consent_required\"; null wenn nie gesetzt"},"isDemo":{"type":"boolean","description":"true, wenn dieser EINE Saldo erfunden ist. Achtung: im Demo-Modus koennen beide Werte vorkommen."}},"required":["connectionId","provider","iban","bankName","balance","available","currency","balanceDate","fetchedAt","syncStatus","isDemo"],"description":"Der zuletzt bekannte Saldo einer Bankverbindung"},"description":"Ein Eintrag je Bankverbindung des Mandanten"}},"required":["mode","balances"],"description":"Erfundene Salden — es wurde nichts von der Bank geholt"}]},"example":{"mode":"live","balances":[{"connectionId":"00000000-0000-4000-8000-000000000000","provider":"string","iban":"string","bankName":"string","balance":0,"available":0,"currency":"str","balanceDate":"2026-01-01","fetchedAt":"2026-01-01T12:00:00.000Z","syncStatus":"string","isDemo":true}]}}}},"401":{"description":"Unauthorized"},"503":{"description":"Salden nicht ermittelbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"}},"required":["error"]}}}}},"operationId":"getApiV1BankingBalances","tags":["banking"],"parameters":[],"description":"Letzter Saldo-Snapshot je Banking-Verbindung. ACHTUNG — 200 heißt hier NICHT, dass echte Kontostände zurückkommen: Ohne finAPI-Zugangsdaten antwortet der Endpunkt ebenfalls mit 200, dann aber mit `mode: \"demo\"` und deterministisch erfundenen Beträgen. Es wurde in diesem Fall nichts von einer Bank geholt. Nur `mode: \"live\"` steht für echte Salden. Jede Auswertung muss `mode` prüfen, bevor sie `balance` verwendet.","summary":"Letzter Saldo-Snapshot je Banking-Verbindung","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/dunning/config/basiszins":{"get":{"responses":{"200":{"description":"Staffel, heute geltender Satz und Bezugstag.","content":{"application/json":{"schema":{"type":"object","properties":{"staffel":{"type":"array","items":{"type":"object","properties":{"gueltigAb":{"type":"string","description":"Stichtag im Format YYYY-MM-DD."},"satz":{"type":"number","description":"Prozentsatz. Kann negativ sein — 2016 bis 2023 lag er bei -0,88."},"quelle":{"type":["string","null"],"description":"Fundstelle der Bekanntmachung, falls hinterlegt."}},"required":["gueltigAb","satz","quelle"]}},"aktuell":{"type":["number","null"],"description":"Satz zum heutigen Tag; `null`, wenn die Staffel leer ist."},"stand":{"type":"string","description":"Der Tag, auf den `aktuell` sich bezieht."}},"required":["staffel","aktuell","stand"]},"example":{"staffel":[{"gueltigAb":"string","satz":0,"quelle":"string"}],"aktuell":0,"stand":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1DunningConfigBasiszins","tags":["Mahnwesen"],"parameters":[],"summary":"Basiszinssatz nach § 247 BGB abrufen","description":"Gibt die gesamte Staffel der Basiszinssaetze zurueck und dazu den am\nheutigen Tag geltenden Satz.\n\nDIESE WERTE GEHOEREN NICHT DEM MANDANTEN. Der Basiszinssatz wird von der\nDeutschen Bundesbank zum 1.1. und 1.7. festgesetzt; er ist Gesetz, keine\nEinstellung. Alle Mandanten lesen dieselbe Staffel, und das Schreiben\ndaneben (`PUT /basiszins`) aendert sie fuer alle.\n\nDie alten Werte bleiben stehen, weil ein Verzug ueber einen Stichtag\nhinweg beide Saetze braucht. `aktuell` ist deshalb nur eine Bequemlich-\nkeit fuer heute; wer rueckwirkend rechnet, nimmt `staffel` und sucht den\nzum Stichtag passenden Eintrag selbst.\n\nGepflegt wird die Staffel von Hand: die Bundesbank veroeffentlicht den\nSatz auf einer Webseite, nicht ueber eine Schnittstelle.\n\nDer ganze Router verlangt `manager` — auch fuer diesen Lesezugriff."},"put":{"responses":{"200":{"description":"Die Staffel nach dem Schreiben und der heute geltende Satz.","content":{"application/json":{"schema":{"type":"object","properties":{"staffel":{"type":"array","items":{"type":"object","properties":{"gueltigAb":{"type":"string","description":"Stichtag im Format YYYY-MM-DD."},"satz":{"type":"number","description":"Prozentsatz. Kann negativ sein — 2016 bis 2023 lag er bei -0,88."},"quelle":{"type":["string","null"],"description":"Fundstelle der Bekanntmachung, falls hinterlegt."}},"required":["gueltigAb","satz","quelle"]}},"aktuell":{"type":["number","null"],"description":"Satz zum heutigen Tag; `null`, wenn die Staffel leer ist."}},"required":["staffel","aktuell"]},"example":{"staffel":[{"gueltigAb":"string","satz":0,"quelle":"string"}],"aktuell":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putApiV1DunningConfigBasiszins","tags":["Mahnwesen"],"parameters":[],"summary":"Einen Basiszinssatz nach § 247 BGB eintragen oder korrigieren","description":"Traegt den Satz zu einem Stichtag ein. Gibt es den Stichtag schon, wird\nsein Satz UEBERSCHRIEBEN; ist er neu, kommt er dazu. Andere Stichtage\nbleiben unangetastet — die Staffel waechst, sie wird nicht ersetzt.\n\nDIESE AENDERUNG GILT FUER ALLE MANDANTEN. `public.basiszins_saetze` hat\nkeine Mandantenspalte: wer hier schreibt, aendert die Grundlage der\nVerzugszinsen fuer JEDEN Mandanten der Installation. Der Basiszinssatz\nist Gesetz und keine Einstellung; die Deutsche Bundesbank setzt ihn zum\n1.1. und 1.7. fest. Gepflegt wird er von Hand, weil sie ihn auf einer\nWebseite veroeffentlicht und nicht ueber eine Schnittstelle.\n\nNegative Saetze sind zulaessig (erlaubt ist -10 bis 25): von 2016 bis\n2023 lag er bei -0,88.\n\nWANN DIE AENDERUNG WIRKT: die Staffel wird bei jeder Zinsberechnung\nfrisch gelesen, es gibt keinen Zwischenspeicher. Bereits erzeugte\nMahnungen werden nicht nachtraeglich neu gerechnet — deren Gebuehr steht\nin der Mahnzeile. Der Zins dagegen wird erst beim Erzeugen eines\nSchreibens ermittelt und folgt damit dem neuen Satz.\n\nSTILLER FEHLSCHLAG OHNE DATENBANK: `setzeBasiszins` bricht ohne\nVerbindung wortlos ab, die Route antwortet trotzdem 200 — mit dem\neingebauten Startbestand als `staffel`. „Gespeichert\" und „nicht\ngespeichert\" sind an der Antwort nicht zu unterscheiden.\n\nDer ganze Router verlangt `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"gueltigAb":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"satz":{"type":"number","minimum":-10,"maximum":25},"quelle":{"type":["string","null"],"maxLength":200}},"required":["gueltigAb","satz"]},"example":{"gueltigAb":"2026-01-01","satz":0,"quelle":"string"}}}}}},"/api/v1/dunning/config":{"get":{"responses":{"200":{"description":"Hauptschalter und drei Stufen — immer vollstaendig, siehe Vorbehalt.","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Hauptschalter der Mahnautomatik."},"levels":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Drei Stufen. Feldnamen nicht zugesagt: die Form stammt aus `serializeConfig`, nicht aus dieser Datei."}},"required":["enabled","levels"]},"example":{"enabled":true,"levels":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1DunningConfig","tags":["Mahnwesen"],"parameters":[],"summary":"Mahn-Einstellungen des Mandanten lesen","description":"Gibt den Hauptschalter der Mahnautomatik und alle drei Mahnstufen\nzurueck. Die Antwort ist IMMER vollstaendig: fuer jede Stufe, die der\nMandant nie eingestellt hat, stehen die hinterlegten Vorgaben darin.\n\nWAS DIE ANTWORT NICHT SAGT: ob ein Wert eingestellt oder nur vorgegeben\nist. `loadDunningConfig` faellt auch bei einem DATENBANKFEHLER auf\ndieselben Vorgaben zurueck — ein Aufrufer kann „so eingestellt\",\n„nie eingestellt\" und „nicht lesbar\" nicht auseinanderhalten. Die Route\nantwortet in allen drei Faellen 200 mit einer vollstaendigen\nKonfiguration.\n\nDas ist in einem Punkt vertretbar: die Vorgaben sind die historisch\nverwendeten Werte, ein Mahnlauf auf ihrer Grundlage waere also nicht\nfalsch. Wer aber ANZEIGEN will, was der Mandant selbst gesetzt hat,\nbekommt es hier nicht.\n\nDer ganze Router verlangt `manager`."},"put":{"responses":{"200":{"description":"Alle drei Stufen nach dem Schreiben.","content":{"application/json":{"schema":{"type":"object","properties":{"levels":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Drei Stufen. Feldnamen nicht zugesagt: die Form stammt aus `serializeConfig`, nicht aus dieser Datei."},"updated":{"type":"boolean","const":true,"description":"Steht immer auf `true`, wenn die Route ueberhaupt antwortet."}},"required":["levels","updated"]},"example":{"levels":[{}],"updated":true}}}},"400":{"description":"Zwei Stufen mit derselben Nummer im selben Aufruf.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"duplicate_level"},"level":{"type":"integer"}},"required":["error","level"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putApiV1DunningConfig","tags":["Mahnwesen"],"parameters":[],"summary":"Mahnstufen des Mandanten speichern","description":"Schreibt eine oder mehrere der drei Mahnstufen. Der Rumpf ist ENTWEDER\neine einzelne Stufe ODER `{ levels: [...] }` mit bis zu dreien.\n\nJE STUFE WIRD VOLLSTAENDIG ERSETZT, nicht zusammengefuehrt. Felder, die\nim Rumpf fehlen, werden mit ihrem Vorgabewert GESCHRIEBEN, nicht\nstehengelassen: `emailSubject` und `emailBody` mit dem leeren Text,\n`emailTemplateKey` ebenso, `active` mit `true`, `callEnabled` mit\n`false`, `voiceAgentId` mit `null`, `aufschlagPunkte` und `dueDays` mit\nNULL. Wer nur die Gebuehr aendern will und den Mahntext weglaesst,\nLOESCHT den Mahntext. Zum Aendern also immer die vollstaendige Stufe\nsenden.\n\nNULL bei `aufschlagPunkte` und `dueDays` heisst „nie eingestellt\": dann\ngelten die Systemvorgaben (9 Punkte ueber Basiszins ab Stufe 2, Fristen\n7/7/14 Tage).\n\nZwei Stufen mit derselben Nummer in einem Aufruf ergeben 400\n`duplicate_level` — geprueft VOR dem ersten Schreiben, es wird dann\nnichts gespeichert.\n\nKEINE TRANSAKTION UEBER MEHRERE STUFEN: die Stufen werden nacheinander\ngeschrieben. Bricht die zweite ab, bleibt die erste gespeichert.\n\nWANN DIE AENDERUNG WIRKT: der Mahnlauf liest die Stufen zu Beginn jedes\nDurchgangs neu, es gibt keinen Zwischenspeicher. Bereits erzeugte\nMahnungen behalten die Gebuehr, mit der sie angelegt wurden — sie steht\nin der Mahnzeile und wird nicht nachgerechnet. Neue Werte greifen also\nerst bei der naechsten erzeugten Mahnstufe.\n\nDie Antwort ist die VOLLSTAENDIG aufgeloeste Konfiguration aller drei\nStufen — gespeicherte Werte plus Vorgaben fuer die unberuehrten. Sie\nsagt weder, welche Stufe gerade geschrieben wurde, noch ob ein Wert\neingestellt oder nur vorgegeben ist (derselbe Vorbehalt wie bei `GET /`).\n\nDer ganze Router verlangt `manager`.","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"level":{"type":"integer","minimum":1,"maximum":3},"graceDays":{"type":"integer","minimum":0,"maximum":365},"feeEur":{"type":"number","minimum":0,"maximum":100000},"interestPct":{"type":"number","minimum":0,"maximum":100,"default":0},"aufschlagPunkte":{"type":"number","minimum":0,"maximum":30},"emailTemplateKey":{"type":"string","maxLength":120,"default":""},"emailSubject":{"type":"string","maxLength":300,"default":""},"emailBody":{"type":"string","maxLength":20000,"default":""},"dueDays":{"type":"integer","minimum":1,"maximum":90},"active":{"type":"boolean","default":true},"callEnabled":{"type":"boolean","default":false},"voiceAgentId":{"type":["string","null"],"format":"uuid","default":null}},"required":["level","graceDays","feeEur"]},{"type":"object","properties":{"levels":{"type":"array","items":{"type":"object","properties":{"level":{"type":"integer","minimum":1,"maximum":3},"graceDays":{"type":"integer","minimum":0,"maximum":365},"feeEur":{"type":"number","minimum":0,"maximum":100000},"interestPct":{"type":"number","minimum":0,"maximum":100,"default":0},"aufschlagPunkte":{"type":"number","minimum":0,"maximum":30},"emailTemplateKey":{"type":"string","maxLength":120,"default":""},"emailSubject":{"type":"string","maxLength":300,"default":""},"emailBody":{"type":"string","maxLength":20000,"default":""},"dueDays":{"type":"integer","minimum":1,"maximum":90},"active":{"type":"boolean","default":true},"callEnabled":{"type":"boolean","default":false},"voiceAgentId":{"type":["string","null"],"format":"uuid","default":null}},"required":["level","graceDays","feeEur"]},"minItems":1,"maxItems":3}},"required":["levels"]}]},"example":{"level":1,"graceDays":0,"feeEur":0,"interestPct":0,"aufschlagPunkte":0,"emailTemplateKey":"string","emailSubject":"string","emailBody":"string","dueDays":1,"active":true,"callEnabled":true,"voiceAgentId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/dunning/config/enabled":{"put":{"responses":{"200":{"description":"Der gesendete Wert, zurueckgespiegelt — siehe Vorbehalt oben.","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]},"example":{"enabled":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putApiV1DunningConfigEnabled","tags":["Mahnwesen"],"parameters":[],"summary":"Mahnautomatik ein- oder ausschalten","description":"Legt den Hauptschalter der Mahnautomatik dieses Mandanten um\n(`public.dunning_settings`, eine Zeile je Mandant).\n\nWANN DIE AENDERUNG WIRKT: der naechtliche Mahnlauf fragt den Schalter\nEINMAL je Mandant und Durchgang ab (alle 24 Stunden). Wer ihn waehrend\neines Durchgangs umlegt, aendert diesen Durchgang fuer den gerade\nbearbeiteten Mandanten nicht mehr; ab dem naechsten gilt der neue Wert.\n\nAusschalten HAELT NUR NEUE Mahnungen auf. Bereits erzeugte Mahnungen\nbleiben stehen, mit Gebuehr und Frist, und werden nicht zurueckgenommen.\nDafuer gibt es `POST /invoices/dunning/{id}/zuruecknehmen`.\n\nDER SCHALTER FAELLT OFFEN AUS: kann er nicht gelesen werden — Tabelle\nfehlt, Datenbankfehler —, gilt die Automatik als EINGESCHALTET. Ein\n„aus\" ist damit nur so verlaesslich wie die Lesbarkeit der Tabelle.\n\nSTILLER FEHLSCHLAG OHNE DATENBANK: ohne Verbindung schreibt\n`setDunningEnabled` nichts und die Route antwortet trotzdem 200 mit dem\ngesendeten Wert. Die Antwort spiegelt die Eingabe, sie bestaetigt keine\nSpeicherung.\n\nDer ganze Router verlangt `manager`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]},"example":{"enabled":true}}}}}},"/api/v1/sequence-audit":{"get":{"responses":{"200":{"description":"Liste der Findings mit Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Kennung des Befunds"},"sequenceType":{"type":"string","enum":["invoices","credit_notes","vendor_invoices","journal_entries"],"description":"Geprueftes Belegwesen"},"year":{"type":"integer","minimum":2000,"maximum":2100,"description":"Geprueftes Jahr"},"gapCount":{"type":"integer","minimum":0,"description":"Summe der fehlenden Nummern ueber alle Luecken"},"gapDetails":{"type":"array","items":{"type":"object","properties":{"from":{"type":"integer","description":"Erste fehlende Nummer der Luecke"},"to":{"type":"integer","description":"Letzte fehlende Nummer der Luecke"},"count":{"type":"integer","minimum":0,"description":"Anzahl fehlender Nummern in dieser Luecke"},"missing":{"type":"array","items":{"type":"string","minLength":1},"maxItems":20,"description":"Die fehlenden Belegnummern im Klartext, je Luecke auf 20 begrenzt"}},"required":["from","to","count","missing"],"description":"Eine zusammenhaengende Luecke in der Nummernfolge"},"description":"Die einzelnen Luecken; leer wenn keine gefunden wurden"},"severity":{"type":"string","enum":["none","warning","critical"],"description":"none = keine Luecke, warning = wenige, critical = viele (GoBD-relevant)"},"totalIssued":{"type":"integer","minimum":0,"description":"Anzahl tatsaechlich vergebener Nummern"},"firstNumber":{"type":["integer","null"],"description":"Kleinste vergebene Nummer; null wenn keine vergeben wurde"},"lastNumber":{"type":["integer","null"],"description":"Groesste vergebene Nummer; null wenn keine vergeben wurde"},"checkedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Pruefung, die diesen Befund erzeugt hat"},"createdAt":{"type":"string","format":"date-time","description":"Anlagezeitpunkt des Befunds"}},"required":["id","sequenceType","year","gapCount","gapDetails","severity","totalIssued","firstNumber","lastNumber","checkedAt","createdAt"],"description":"Ein Lueckenbefund fuer ein Belegwesen und ein Jahr"},"description":"Die Befunde der aktuellen Seite, neueste Pruefung zuerst"},"pagination":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Befunde, die dem Filter entsprechen"}},"required":["limit","offset","total"],"description":"Seitenangaben"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, fuer den gesucht wurde"}},"required":["tenantId"],"description":"Angaben zur Abfrage"}},"required":["data","pagination","meta"]},"example":{"data":[{"id":"00000000-0000-4000-8000-000000000000","sequenceType":"invoices","year":2000,"gapCount":0,"gapDetails":[{"from":0,"to":0,"count":0,"missing":["string"]}],"severity":"none","totalIssued":0,"firstNumber":0,"lastNumber":0,"checkedAt":"2026-01-01T12:00:00.000Z","createdAt":"2026-01-01T12:00:00.000Z"}],"pagination":{"limit":1,"offset":0,"total":0},"meta":{"tenantId":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Sequence-audit","tags":["sequence-audit"],"parameters":[{"in":"query","name":"sequence_type","schema":{"type":"string","enum":["invoices","credit_notes","vendor_invoices","journal_entries"]}},{"in":"query","name":"year","schema":{"type":"integer","minimum":2000,"maximum":2100}},{"in":"query","name":"severity","schema":{"type":"string","enum":["none","warning","critical"]}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}}],"description":"Listet Belegnummern-Lückenbefunde für den Mandanten. Fehlt die Befund-Tabelle noch, kommt 200 mit leerer Liste — eine fehlende Tabelle ist kein Ausfall.","summary":"Listet Belegnummern-Lückenbefunde für den Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sequence-audit/summary":{"get":{"responses":{"200":{"description":"Zusammenfassung je Belegwesen und Jahr","content":{"application/json":{"schema":{"type":"object","properties":{"overallStatus":{"type":"string","enum":["ok","warning","critical"],"description":"Schlechtester Schweregrad ueber alle Zeilen; ok auch dann, wenn nie geprueft wurde"},"totalGaps":{"type":"integer","minimum":0,"description":"Summe aller fehlenden Nummern ueber alle Zeilen"},"lastChecked":{"type":["string","null"],"format":"date-time","description":"Zeitpunkt der juengsten Pruefung; null wenn noch nie geprueft wurde"},"sequences":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["invoices","credit_notes","vendor_invoices","journal_entries"],"description":"Geprueftes Belegwesen"},"year":{"type":"integer","minimum":2000,"maximum":2100,"description":"Geprueftes Jahr"},"gapCount":{"type":"integer","minimum":0,"description":"Summe der fehlenden Nummern"},"severity":{"type":"string","enum":["none","warning","critical"],"description":"Schweregrad dieser Zeile"},"totalIssued":{"type":"integer","minimum":0,"description":"Anzahl tatsaechlich vergebener Nummern"},"checkedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der juengsten Pruefung"}},"required":["type","year","gapCount","severity","totalIssued","checkedAt"],"description":"Der juengste Stand je Belegwesen und Jahr"},"description":"Je Belegwesen und Jahr eine Zeile"},"meta":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1,"description":"Mandant, fuer den zusammengefasst wurde"}},"required":["tenantId"],"description":"Angaben zur Abfrage"}},"required":["overallStatus","totalGaps","lastChecked","sequences","meta"]},"example":{"overallStatus":"ok","totalGaps":0,"lastChecked":"2026-01-01T12:00:00.000Z","sequences":[{"type":"invoices","year":2000,"gapCount":0,"severity":"none","totalIssued":0,"checkedAt":"2026-01-01T12:00:00.000Z"}],"meta":{"tenantId":"string"}}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable","description":"Fehlerkennung fuer die Auswertung"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden vor dem naechsten Versuch"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiV1Sequence-auditSummary","tags":["sequence-audit"],"parameters":[],"description":"Zusammenfassung der letzten Belegnummern-Prüfung pro Typ und Jahr. Fehlt die Befund-Tabelle noch, kommt 200 mit `overallStatus: \"ok\"` und leerer Liste — das heißt „nie geprüft\", nicht „keine Lücken\".","summary":"Zusammenfassung der letzten Belegnummern-Prüfung pro Typ und Jahr","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/sequence-audit/run":{"post":{"responses":{"200":{"description":"Prüfung abgeschlossen, Ergebnis je Belegwesen und Jahr","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Die Pruefung ist durchgelaufen"},"ranAt":{"type":"string","format":"date-time","description":"Zeitpunkt des Laufs"},"overallStatus":{"type":"string","enum":["ok","warning","critical"],"description":"Schlechtester Schweregrad nach dem Lauf"},"totalGaps":{"type":"integer","minimum":0,"description":"Summe aller fehlenden Nummern nach dem Lauf"},"sequences":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["invoices","credit_notes","vendor_invoices","journal_entries"],"description":"Geprueftes Belegwesen"},"year":{"type":"integer","minimum":2000,"maximum":2100,"description":"Geprueftes Jahr"},"gapCount":{"type":"integer","minimum":0,"description":"Summe der fehlenden Nummern"},"severity":{"type":"string","enum":["none","warning","critical"],"description":"Schweregrad dieser Zeile"},"totalIssued":{"type":"integer","minimum":0,"description":"Anzahl tatsaechlich vergebener Nummern"},"checkedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der juengsten Pruefung"}},"required":["type","year","gapCount","severity","totalIssued","checkedAt"],"description":"Der juengste Stand je Belegwesen und Jahr"},"description":"Je Belegwesen und Jahr eine Zeile"}},"required":["ok","ranAt","overallStatus","totalGaps","sequences"]},"example":{"ok":true,"ranAt":"2026-01-01T12:00:00.000Z","overallStatus":"ok","totalGaps":0,"sequences":[{"type":"invoices","year":2000,"gapCount":0,"severity":"none","totalIssued":0,"checkedAt":"2026-01-01T12:00:00.000Z"}]}}}},"401":{"description":"Nicht authentifiziert"},"403":{"description":"Keine Manager-Rolle"},"500":{"description":"Prüfung fehlgeschlagen — hier steht 500, nicht 503 wie bei den Leseaufrufen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"Prüfung fehlgeschlagen","description":"Feste deutsche Meldung, keine auswertbare Kennung"},"details":{"type":"string","description":"Meldung der zugrunde liegenden Ausnahme"}},"required":["error","details"]}}}}},"operationId":"postApiV1Sequence-auditRun","tags":["sequence-audit"],"parameters":[],"summary":"Startet eine Belegnummern-Lueckenpruefung und liefert den neuen Stand","description":"Startet eine sofortige Belegnummern-Lückenprüfung für den Mandanten und liefert danach den neuen Stand zurück."}},"/api/v1/permissions/custom-roles":{"get":{"responses":{"200":{"description":"Rollen des Mandanten, aelteste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"getApiV1PermissionsCustom-roles","tags":["permissions"],"parameters":[],"summary":"Alle eigenen Rollen des Mandanten auflisten","description":"Listet die selbst angelegten Rollen des Mandanten, je Zeile mit\n`user_count` — der Zahl der Mitglieder, die diese Rolle tragen.\n\nDie Zeilen kommen aus `SELECT cr.*` und werden NICHT serialisiert. Sie\ntragen deshalb snake_case und die Spalten der Tabelle, nicht eine\nausgewaehlte Aussenform. Der Vertrag sagt hier keine Feldnamen zu.\n\n`user_count` faellt auf 0 zurueck, wenn die Tabelle\n`organization_members` auf diesem Mandanten schlummert. Eine 0 heisst\nalso entweder „niemand hat diese Rolle\" oder „die Zahl war nicht\nermittelbar\". Die Antwort trennt das nicht."},"post":{"responses":{"201":{"description":"Rolle angelegt.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"409":{"description":"Eine Rolle mit diesem Namen gibt es bereits."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"postApiV1PermissionsCustom-roles","tags":["permissions"],"parameters":[],"summary":"Eigene Rolle anlegen","description":"Legt eine Rolle an. Die angelegte Zeile kommt roh aus `RETURNING`\nzurueck, also in snake_case und ohne ausgewaehlte Aussenform. Der\nVertrag sagt hier keine Feldnamen zu.\n\nEin bereits vergebener Name fuehrt zu 409, nicht zu einer stillen\nZweitanlage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z0-9_-]+$","minLength":1,"maxLength":50},"displayName":{"type":"string","minLength":1,"maxLength":100},"baseSystemRole":{"type":"string","enum":["admin","hr_manager","accountant","manager","warehouse","sales_rep","user","viewer"],"default":"user"},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","default":"#6366f1"},"icon":{"type":"string","maxLength":50,"default":"shield"},"description":{"type":"string","maxLength":500}},"required":["name","displayName"]},"example":{"name":"00000000-0000-4000-8000-000000000000","displayName":"string","baseSystemRole":"admin","icon":"string","description":"string"}}}}}},"/api/v1/permissions/custom-roles/{id}":{"get":{"responses":{"200":{"description":"Rolle mit Rechte-Matrix.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"displayName":{},"baseRole":{"type":"string"},"baseSystemRole":{"type":"string"},"color":{},"icon":{},"description":{},"createdAt":{},"updatedAt":{},"permissions":{"type":"object","additionalProperties":{"type":"object","properties":{"read":{"type":"object","properties":{"value":{"type":"boolean"},"inherited":{"type":"boolean"},"fromRole":{"type":"string"}},"required":["value","inherited"],"additionalProperties":false},"write":{"type":"object","properties":{"value":{"type":"boolean"},"inherited":{"type":"boolean"},"fromRole":{"type":"string"}},"required":["value","inherited"],"additionalProperties":false},"delete":{"type":"object","properties":{"value":{"type":"boolean"},"inherited":{"type":"boolean"},"fromRole":{"type":"string"}},"required":["value","inherited"],"additionalProperties":false},"export":{"type":"object","properties":{"value":{"type":"boolean"},"inherited":{"type":"boolean"},"fromRole":{"type":"string"}},"required":["value","inherited"],"additionalProperties":false}},"required":["read","write","delete","export"],"additionalProperties":false}}},"required":["baseRole","baseSystemRole","permissions"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"baseRole":"string","baseSystemRole":"string","permissions":{"beispiel":{"read":{"value":true,"inherited":true,"fromRole":"string"},"write":{"value":true,"inherited":true,"fromRole":"string"},"delete":{"value":true,"inherited":true,"fromRole":"string"},"export":{"value":true,"inherited":true,"fromRole":"string"}}}}}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found` — unbekannt oder fremder Mandant."}},"operationId":"getApiV1PermissionsCustom-rolesById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine eigene Rolle samt Rechte-Matrix laden","description":"Liefert die Stammdaten der Rolle UND ihre Modul-Rechte als Matrix, damit\ndie Detailmaske ohne zweiten Aufruf rendern kann.\n\nIn `permissions` stehen NUR die Module, fuer die diese Rolle etwas\nausdruecklich gesetzt hat. Ein Modul, das dort fehlt, ist nicht\nverboten — es erbt vom Grundrecht in `baseSystemRole`. Wer die Matrix\nals vollstaendige Rechteliste liest, haelt geerbte Rechte faelschlich\nfuer fehlende.\n\nDie Werte der Stammfelder kommen aus einer untypisierten Zeile. Die\nSchluessel stehen fest, die Typen sagt der Vertrag nicht zu."},"put":{"responses":{"200":{"description":"Rolle geaendert.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"`NO_FIELDS_TO_UPDATE` — der Rumpf enthaelt kein aenderbares Feld."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found`."}},"operationId":"putApiV1PermissionsCustom-rolesById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigene Rolle bearbeiten","description":"Aendert die Stammdaten einer Rolle. Es ist ein Teil-Update: nur die\ngesendeten Felder werden geschrieben.\n\nEin Rumpf OHNE ein einziges bekanntes Feld ist ein 400\n(`NO_FIELDS_TO_UPDATE`), kein stiller Erfolg. Die Antwort traegt die\ngeaenderte Zeile roh, ohne Feldzusage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"displayName":{"type":"string","minLength":1,"maxLength":100},"baseSystemRole":{"type":"string","enum":["admin","hr_manager","accountant","manager","warehouse","sales_rep","user","viewer"],"default":"user"},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","default":"#6366f1"},"icon":{"type":"string","maxLength":50,"default":"shield"},"description":{"type":"string","maxLength":500},"modulePermissions":{"type":"array","items":{"type":"object","properties":{"module":{"type":"string","minLength":1,"maxLength":50},"canRead":{"type":"boolean","default":false},"canWrite":{"type":"boolean","default":false},"canDelete":{"type":"boolean","default":false},"canExport":{"type":"boolean","default":false}},"required":["module"]}}}},"example":{"displayName":"string","baseSystemRole":"admin","icon":"string","description":"string","modulePermissions":[{"module":"string","canRead":true,"canWrite":true,"canDelete":true,"canExport":true}]}}}}},"delete":{"responses":{"200":{"description":"Rolle geloescht. `resetUserCount` nennt die zurueckgesetzten Mitglieder.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"resetUserCount":{"type":"number"}},"required":["success","resetUserCount"],"additionalProperties":false},"example":{"success":true,"resetUserCount":0}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found`."}},"operationId":"deleteApiV1PermissionsCustom-rolesById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigene Rolle loeschen","description":"Loescht die Rolle. Mitglieder, die sie tragen, werden dabei auf ihre\nGrundrolle zurueckgesetzt — `resetUserCount` sagt, wie viele das waren.\n\nDas Loeschen scheitert also NICHT daran, dass die Rolle noch benutzt\nwird. Wer eine Sperre erwartet, bekommt stattdessen stillschweigend\nzurueckgestufte Mitglieder. `resetUserCount: 0` heisst, dass niemand\nbetroffen war."}},"/api/v1/permissions/custom-roles/{id}/module-permissions":{"get":{"responses":{"200":{"description":"Ausdruecklich gesetzte Modul-Rechte. Leer, wenn alles geerbt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found`."}},"operationId":"getApiV1PermissionsCustom-rolesByIdModule-permissions","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Modul-Rechte einer Rolle laden","description":"Liefert die AUSDRUECKLICH gesetzten Modul-Rechte dieser Rolle, als rohe\nZeilen aus der Datenbank.\n\nWas hier fehlt, ist nicht verboten, sondern geerbt. Eine leere Liste\nheisst „diese Rolle setzt nichts eigenes\", nicht „diese Rolle darf\nnichts\"."},"put":{"responses":{"200":{"description":"Rechte ersetzt. `count` ist die Zahl der geschriebenen Regeln.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"count":{"type":"number"}},"required":["success","count"],"additionalProperties":false},"example":{"success":true,"count":0}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found`."}},"operationId":"putApiV1PermissionsCustom-rolesByIdModule-permissions","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Modul-Rechte einer Rolle setzen (ersetzt alles)","description":"Setzt die Modul-Rechte der Rolle. Das ist ein VOLLSTAENDIGER ERSATZ,\nkein Teil-Update: was im Rumpf fehlt, ist danach nicht mehr\nausdruecklich gesetzt und faellt auf das Grundrecht zurueck.\n\n`count` nennt die Zahl der geschriebenen Regeln, nicht die der\nbetroffenen Mitglieder.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"permissions":{"type":"array","items":{"type":"object","properties":{"module":{"type":"string","minLength":1,"maxLength":50},"canRead":{"type":"boolean","default":false},"canWrite":{"type":"boolean","default":false},"canDelete":{"type":"boolean","default":false},"canExport":{"type":"boolean","default":false}},"required":["module"]}}},"required":["permissions"]},"example":{"permissions":[{"module":"string","canRead":true,"canWrite":true,"canDelete":true,"canExport":true}]}}}}}},"/api/v1/permissions/field-visibility":{"get":{"responses":{"200":{"description":"Eigene Regeln und globale Vorgaben gemischt, unterscheidbar an `is_global`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."}},"operationId":"getApiV1PermissionsField-visibility","tags":["permissions"],"parameters":[],"summary":"Feld-Sichtbarkeitsregeln auflisten","description":"Listet die Regeln, ab welcher Rolle ein Feld sichtbar ist.\n\nDie Liste MISCHT ZWEI HERKUENFTE: eigene Regeln des Mandanten und\nglobale Vorgaben, die fuer alle gelten. Zu unterscheiden sind sie am\nFeld `is_global`. Wer das uebersieht, haelt eine Plattformvorgabe fuer\neine eigene Einstellung und wundert sich, dass sie sich nicht loeschen\nlaesst.\n\nDie Zeilen kommen roh aus der Abfrage, also in snake_case und samt\n`tenant_id`. Der Vertrag sagt keine Feldnamen zu."},"post":{"responses":{"201":{"description":"Regel angelegt ODER eine bestehende ueberschrieben.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."}},"operationId":"postApiV1PermissionsField-visibility","tags":["permissions"],"parameters":[],"summary":"Feld-Sichtbarkeitsregel anlegen oder ueberschreiben","description":"Legt eine Regel an. Gibt es fuer dieselbe Kombination aus Entitaet und\nFeld schon eine, wird deren Mindestrolle UEBERSCHRIEBEN.\n\nDer Code ist deshalb immer 201, auch wenn nichts Neues entstanden ist.\nAus der Antwort ist nicht zu erkennen, ob angelegt oder ersetzt wurde.\n\nDie Zeile kommt roh aus `RETURNING *`, ohne Feldzusage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","minLength":1,"maxLength":50},"fieldName":{"type":"string","minLength":1,"maxLength":100},"minRole":{"type":"string","minLength":1,"maxLength":50}},"required":["entity","fieldName","minRole"]},"example":{"entity":"string","fieldName":"string","minRole":"string"}}}}}},"/api/v1/permissions/field-visibility/{id}":{"delete":{"responses":{"200":{"description":"Regel geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"],"additionalProperties":false},"example":{"success":true}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Rule not found` — unbekannt, oder es ist eine globale Vorgabe."}},"operationId":"deleteApiV1PermissionsField-visibilityById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Feld-Sichtbarkeitsregel loeschen","description":"Loescht eine EIGENE Regel des Mandanten.\n\nGlobale Vorgaben (`is_global`) gehoeren keinem Mandanten und lassen sich\nhier nicht entfernen; der Versuch endet in einem 404, nicht in einem\n403. Ein 404 heisst deshalb entweder „gibt es nicht\" oder „gehoert dir\nnicht\"."}},"/api/v1/permissions/overrides":{"get":{"responses":{"200":{"description":"Ausnahmen, neueste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"getApiV1PermissionsOverrides","tags":["permissions"],"parameters":[],"summary":"Persoenliche Rechte-Ausnahmen auflisten","description":"Listet die Ausnahmen, die einzelnen Mitgliedern zusaetzlich zu ihrer\nRolle gewaehrt oder entzogen wurden. Mit `userId` als Abfrageparameter\nauf ein Mitglied eingegrenzt.\n\nDie Zeilen kommen aus `SELECT upo.*` samt drei angehaengten Feldern aus\nder Nutzertabelle (`user_name`, `user_email`, `granted_by_name`). Sie\nsind roh, also snake_case; der Vertrag sagt keine Feldnamen zu.\n\nEs gibt keine Paginierung."},"post":{"responses":{"201":{"description":"Ausnahme angelegt.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."}},"operationId":"postApiV1PermissionsOverrides","tags":["permissions"],"parameters":[],"summary":"Persoenliche Rechte-Ausnahme anlegen","description":"Gewaehrt oder entzieht einem einzelnen Mitglied ein Recht, abweichend\nvon seiner Rolle. Die Ausnahme schlaegt die Rolle.\n\nDie angelegte Zeile kommt roh zurueck, ohne Feldzusage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","minLength":1,"maxLength":255},"overrideType":{"type":"string","enum":["grant","deny"]},"scope":{"type":"string","enum":["module","field","endpoint"]},"scopeKey":{"type":"string","minLength":1,"maxLength":200},"reason":{"type":"string","minLength":1,"maxLength":500},"expiresAt":{"type":"string","format":"date-time"}},"required":["userId","overrideType","scope","scopeKey","reason"]},"example":{"userId":"string","overrideType":"grant","scope":"module","scopeKey":"string","reason":"string","expiresAt":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/permissions/overrides/{id}":{"delete":{"responses":{"200":{"description":"Ausnahme entfernt.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"],"additionalProperties":false},"example":{"success":true}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Override not found` — unbekannt oder fremder Mandant."}},"operationId":"deleteApiV1PermissionsOverridesById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Persoenliche Rechte-Ausnahme entfernen","description":"Entfernt die Ausnahme. Das Mitglied faellt damit auf die Rechte seiner Rolle zurueck."}},"/api/v1/permissions/users/{userId}/ai-tools":{"get":{"responses":{"200":{"description":"Ausdruecklich gesetzte Werkzeugrechte, nach Klasse sortiert.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"toolClass":{"type":"string"},"allowed":{"type":"boolean"},"expiresAt":{"type":["string","null"]}},"required":["toolClass","allowed","expiresAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"toolClass":"string","allowed":true,"expiresAt":"string"}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"getApiV1PermissionsUsersByUserIdAi-tools","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"KI-Werkzeugrechte eines Mitglieds laden","description":"Liefert die ausdruecklich gesetzten Rechte dieses Mitglieds auf\nKI-Werkzeugklassen. Diese Route serialisiert, im Gegensatz zu den\nListen weiter oben: die Felder heissen `toolClass`, `allowed`,\n`expiresAt`.\n\nEine leere Liste heisst „nichts ausdruecklich gesetzt\", nicht „nichts\nerlaubt\" — ohne Eintrag gilt die Vorgabe der Rolle.\n\n`expiresAt` ist `null` bei unbefristeten Rechten. Ein abgelaufener\nEintrag wird hier weiterhin ausgeliefert; das Ablaufdatum wertet die\nPruefung aus, nicht diese Liste."},"put":{"responses":{"200":{"description":"Werkzeugrechte ersetzt.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"count":{"type":"number"}},"required":["success","count"],"additionalProperties":false},"example":{"success":true,"count":0}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"putApiV1PermissionsUsersByUserIdAi-tools","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"KI-Werkzeugrechte eines Mitglieds setzen (ersetzt alles)","description":"Setzt die Werkzeugrechte. VOLLSTAENDIGER ERSATZ: was im Rumpf fehlt,\nist danach nicht mehr ausdruecklich gesetzt und faellt auf die Vorgabe\nder Rolle zurueck. Ein leerer Rumpf loescht folglich alle Ausnahmen.\n\n`count` ist die Zahl der geschriebenen Eintraege.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"permissions":{"type":"array","items":{"type":"object","properties":{"toolClass":{"type":"string","enum":["PUBLIC","WRITE","DESTRUCTIVE"]},"allowed":{"type":"boolean"},"expiresAt":{"type":["string","null"],"format":"date-time"}},"required":["toolClass","allowed"]}}},"required":["permissions"]},"example":{"permissions":[{"toolClass":"PUBLIC","allowed":true,"expiresAt":"2026-01-01T12:00:00.000Z"}]}}}}}},"/api/v1/permissions/modules":{"get":{"responses":{"200":{"description":"Alle Rechte-Module in fester Reihenfolge.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"}},"required":["key","label"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"key":"string","label":"string"}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."}},"operationId":"getApiV1PermissionsModules","tags":["permissions"],"parameters":[],"summary":"Die verfuegbaren Rechte-Module auflisten","description":"Liefert die Liste der Module, auf die sich Rechte vergeben lassen — je\nEintrag ein Schluessel und ein deutsches Etikett.\n\nDie Liste ist FEST im Quelltext hinterlegt und nicht mandantenabhaengig.\nSie ist Teil des Vertrags mit der Oberflaeche: Reihenfolge und Schluessel\naendern sich nicht ohne Abstimmung. Es wird keine Datenbank befragt,\ndeshalb gibt es hier auch keinen 503."}},"/api/v1/permissions/me/modules":{"get":{"responses":{"200":{"description":"Je Modul: darf ich es sehen, und WOHER kommt diese Entscheidung. Nicht zu verwechseln mit /modules/enabled — das sagt, was der Mandant gebucht hat.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"module":{"type":"string"},"enabled":{"type":"boolean"},"source":{"type":"string"},"canWrite":{"type":"boolean"},"writeSource":{"type":"string"}},"required":["module","enabled","source","canWrite","writeSource"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"module":"string","enabled":true,"source":"string","canWrite":true,"writeSource":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1PermissionsMeModules","tags":["permissions"],"parameters":[],"summary":"Welche Module ich sehen und beschreiben darf","description":"Effektive Modul-Sichtbarkeit des eingeloggten Nutzers, mit Herkunft je\nModul. Keine Rollenpruefung: jeder Angemeldete fragt hier fuer sich\nselbst — die Navigation baut darauf auf.\n\nDie Antwort traegt IMMER alle bekannten Module, auch die verbotenen:\n`enabled` sagt, ob hineingesehen werden darf, `canWrite` getrennt davon,\nob gespeichert werden darf. Ein `enabled: true` ist also kein\n„darf alles\".\n\n`source` und `writeSource` nennen, WOHER die jeweilige Entscheidung\nkommt. Ohne sie ist ein „nein\" nicht von einem anderen „nein\" zu\nunterscheiden, und niemand weiss, an welcher Stelle man es aendern\nmuesste.\n\nNICHT zu verwechseln mit `GET /modules/enabled`: das sagt, was der\nMandant gebucht hat, nicht was dieser Nutzer darf.\n\nEs gibt weder Blaetterung noch Filter. Faellt die Aufloesung aus, kommt\n503 und keine leere Liste."}},"/api/v1/permissions/members/{userId}/modules":{"get":{"responses":{"200":{"description":"Aufgeloeste Modul-Rechte des Mitglieds.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"getApiV1PermissionsMembersByUserIdModules","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Aufgeloeste Modul-Rechte eines Mitglieds laden","description":"Liefert die FERTIG AUFGELOESTEN Rechte dieses Mitglieds: Grundrolle,\neigene Rolle und persoenliche Ausnahmen sind bereits verrechnet.\n\nDas unterscheidet die Route von `/custom-roles/{id}/module-permissions`,\ndie nur die ausdruecklich gesetzten Regeln EINER Rolle zeigt. Wer\nwissen will, was ein Mitglied wirklich darf, fragt hier."},"put":{"responses":{"200":{"description":"Rechte ersetzt.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"count":{"type":"number"}},"required":["success","count"],"additionalProperties":false},"example":{"success":true,"count":0}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"putApiV1PermissionsMembersByUserIdModules","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Modul-Rechte eines Mitglieds setzen (ersetzt alles)","description":"Setzt die persoenlichen Modul-Rechte des Mitglieds. VOLLSTAENDIGER\nERSATZ: was im Rumpf fehlt, ist danach nicht mehr gesetzt und faellt\nauf die Rolle zurueck.\n\n`count` ist die Zahl der geschriebenen Regeln.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"modules":{"type":"array","items":{"type":"object","properties":{"module":{"type":"string"},"enabled":{"type":"boolean"}},"required":["module","enabled"]}}},"required":["modules"]},"example":{"modules":[{"module":"string","enabled":true}]}}}}}},"/api/v1/permissions/members/{userId}/role":{"patch":{"responses":{"200":{"description":"Rolle zugewiesen oder entzogen.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"userId":{"type":"string"},"customRoleId":{"type":["string","null"]}},"required":["userId","customRoleId"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"userId":"string","customRoleId":"string"}}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found` — die zugewiesene Rolle gibt es nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"patchApiV1PermissionsMembersByUserIdRole","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Einem Mitglied eine eigene Rolle zuweisen oder sie entziehen","description":"Weist dem Mitglied eine selbst angelegte Rolle zu. `customRoleId: null`\nentzieht sie und setzt das Mitglied auf seine Grundrolle zurueck — das\nist der vorgesehene Weg, kein Loeschen der Rolle.\n\nDie Antwort spiegelt nur, was gesetzt wurde. Sie sagt nichts darueber,\nwelche Rechte daraus folgen; dafuer gibt es\n`GET /permissions/members/{userId}/modules`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customRoleId":{"type":["string","null"],"minLength":1}},"required":["customRoleId"]},"example":{"customRoleId":"string"}}}}}},"/api/v1/users":{"get":{"responses":{"200":{"description":"Benutzer des Mandanten, hoechstens 500.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string"}},"required":["id","name","email","role"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","name":"string","email":"string","role":"string"}]}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."}},"operationId":"getApiV1Users","tags":["users"],"parameters":[],"summary":"Benutzer des Mandanten auflisten","description":"Listet die nicht geloeschten Benutzer des Mandanten, sortiert nach Name\nbeziehungsweise E-Mail.\n\nKEINE Paginierung, Abschnitt bei 500. Wer mehr Benutzer hat, sieht die\nhinteren nicht — und die Antwort sagt das nicht.\n\n`name` und `email` sind nie `null`: der Serialisierer setzt einen leeren\nString ein, `role` faellt auf `user` zurueck. „Nicht gesetzt\" und „leer\"\nsind hier also nicht zu unterscheiden."}},"/api/v1/users/me/permissions":{"get":{"responses":{"200":{"description":"Aufgeloeste Rechte des Aufrufers plus Katalog.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"catalog":{"type":"object","properties":{"modules":{"type":"array","items":{}},"sensitive":{"type":"array","items":{}}},"required":["modules","sensitive"]}},"required":["catalog"],"additionalProperties":false},"example":{"catalog":{"modules":[],"sensitive":[]}}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."}},"operationId":"getApiV1UsersMePermissions","tags":["users"],"parameters":[],"summary":"Eigene aufgeloeste Rechte samt Katalog","description":"Liefert die FERTIG AUFGELOESTEN Rechte des angemeldeten Benutzers plus\nden Katalog aller Module und der als sensibel markierten Schluessel.\n\nDie Systemrolle wird frisch aus der Datenbank geholt und faellt nur dann\nauf die Rolle der Sitzung zurueck, wenn dort nichts steht. Eine gerade\ngeaenderte Rolle wirkt hier also sofort, ohne neue Anmeldung.\n\nDer Pfad steht bewusst VOR `/{userId}/permissions` in der Anmeldung —\nsonst wuerde `me` als Benutzer-Id gelesen."}},"/api/v1/users/{userId}/permissions":{"get":{"responses":{"200":{"description":"Aufgeloeste Rechte des angefragten Benutzers plus Katalog.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"catalog":{"type":"object","properties":{"modules":{"type":"array","items":{}},"sensitive":{"type":"array","items":{}}},"required":["modules","sensitive"]}},"required":["catalog"],"additionalProperties":false},"example":{"catalog":{"modules":[],"sensitive":[]}}}}},"400":{"description":"`userId missing`."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Rolle unter `hr_manager`."}},"operationId":"getApiV1UsersByUserIdPermissions","tags":["users"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Aufgeloeste Rechte eines anderen Benutzers","description":"Wie `/me/permissions`, aber fuer ein anderes Mitglied. Erfordert\nmindestens `hr_manager`.\n\nDie Systemrolle des Ziels wird nachgeschlagen; findet sich keine, wird\nohne Rolle aufgeloest — es wird NICHT auf die Rolle des Aufrufers\nzurueckgefallen. Das ist der Unterschied zur eigenen Sicht und wichtig,\ndamit man nicht versehentlich die eigenen Rechte gespiegelt bekommt."},"put":{"responses":{"200":{"description":"Gespeichert. Die Antwort spiegelt die geschriebenen Werte.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"role":{"type":"string"},"modules":{"type":"array","items":{"type":"string"}},"sensitive":{"type":"object","additionalProperties":{"type":"boolean"}}},"required":["role","modules","sensitive"]}},"required":["success","data"],"additionalProperties":false},"example":{"success":true,"data":{"role":"string","modules":["string"],"sensitive":{"beispiel":true}}}}}},"400":{"description":"Unbekannte Rolle, mehr als 50 Module, oder eine fehlende Benutzerkennung im Pfad."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Rolle unter `hr_manager` — oder `INSUFFICIENT_ROLE_FOR_TARGET`: die Zielrolle liegt ab `admin`, der Aufrufer darunter."}},"operationId":"putApiV1UsersByUserIdPermissions","tags":["users"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Rechte-Matrix eines Benutzers setzen","description":"Schreibt die Modul- und Sensibel-Freigaben eines Benutzers in\n`public.user_permissions` — die ausdrueckliche Ausnahme, die den\nRollen-Vorgaben vorgeht. Vollstaendige Ersetzung, kein Zusammenfuehren:\nwas nicht in `modules` steht, ist danach nicht mehr freigegeben.\n\nDAS MITGESCHICKTE `role` IST NICHT DIE SYSTEMROLLE. Es landet in der\nRechte-Zeile und bestimmt nur, welche Vorgabe greift, wenn keine\nAusnahme gesetzt ist. Die Rolle, auf die `requireMinRole` und die\nAnmelde-Schicht schauen, steht in `public.users.role` und wird\nausschliesslich von `PATCH /users/{userId}/role` geaendert. Wer hier\n`admin` eintraegt, hat damit KEINEN Administrator erzeugt.\n\nDER ZIELBENUTZER WIRD NICHT GEPRUEFT. Es gibt keinen 404: eine unbekannte\nBenutzerkennung legt eine Rechte-Zeile an, die zu niemandem gehoert. Die\nZeile haengt am Mandanten des Aufrufers, ein fremder Mandant ist also\nnicht erreichbar.\n\nErfordert `hr_manager` oder hoeher. Darueber liegt eine zweite Schranke:\neine Rolle ab `admin`-Ebene darf nur vergeben, wer selbst mindestens\n`admin` ist (403 `INSUFFICIENT_ROLE_FOR_TARGET`) — sonst koennte die\nPersonalabteilung sich selbst hochstufen.\n\nJede Aenderung wird mit altem und neuem Stand ins Rechte-Protokoll\ngeschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["user","admin"]},"modules":{"type":"array","items":{"type":"string","minLength":1,"maxLength":50},"maxItems":50},"sensitive":{"type":"object","additionalProperties":{"type":"boolean"},"default":{}}},"required":["role","modules"]},"example":{"role":"user","modules":["string"],"sensitive":{"beispiel":true}}}}}}},"/api/v1/users/{userId}/role":{"patch":{"responses":{"200":{"description":"Rolle gesetzt — oder unveraendert, dann `changed: false`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"}},"required":["id","role"]},"changed":{"type":"boolean"}},"required":["success","data","changed"],"additionalProperties":false},"example":{"success":true,"data":{"id":"string","role":"string"},"changed":true}}}},"400":{"description":"Unbekannte Rolle, oder keine Benutzerkennung im Pfad."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Rolle unter `admin` — oder `INSUFFICIENT_ROLE_FOR_TARGET` bei einer Zielrolle ab `admin`."},"404":{"description":"Diesen Benutzer gibt es im Mandanten des Aufrufers nicht."},"409":{"description":"`LAST_ADMIN_CANNOT_DEMOTE` — das waere der letzte Administrator des Mandanten gewesen."},"500":{"description":"`role_update_no_rows` — das UPDATE traf keine Zeile; die Rolle steht unveraendert."}},"operationId":"patchApiV1UsersByUserIdRole","tags":["users"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Systemrolle eines Benutzers aendern","description":"Setzt `public.users.role` — die Rolle, die die Anmelde-Schicht bei JEDEM\nAufruf liest und auf die `requireMinRole` schaut. Das ist die\nwirksame Rolle; die Rechte-Matrix unter\n`PUT /users/{userId}/permissions` ist eine andere Sache und wird hier\nnicht angefasst.\n\nVergeben werden koennen `user`, `manager`, `accountant`, `hr_manager`\nund `admin`. `api` fehlt mit Absicht: API-Schluessel sind keine\nMenschen und werden nicht ueber die Benutzerverwaltung gesetzt.\n\nDREI SCHRANKEN, JEDE MIT EIGENEM CODE:\n- Eine Rolle ab `admin`-Ebene darf nur vergeben, wer selbst mindestens\n  `admin` ist — 403 `INSUFFICIENT_ROLE_FOR_TARGET`.\n- Der Zielbenutzer muss zum Mandanten des Aufrufers gehoeren und darf\n  nicht geloescht sein — sonst 404. Ueber Mandantengrenzen hinweg\n  schreibt diese Route nicht.\n- Der LETZTE Administrator des Mandanten kann nicht herabgestuft werden,\n  auch nicht von sich selbst — 409 `LAST_ADMIN_CANNOT_DEMOTE`. Zaehlung\n  und Schreiben laufen dafuer in EINER Transaktion mit Zeilensperre, so\n  dass zwei gleichzeitige Herabstufungen den Mandanten nicht gemeinsam\n  aussperren koennen.\n\nDIE ANTWORT SAGT, OB SICH ETWAS GEAENDERT HAT: war die Rolle schon die\ngewuenschte, kommt 200 mit `changed: false`, und es wird nichts\nprotokolliert.\n\nTRIFFT DAS UPDATE KEINE ZEILE, IST DAS EIN FEHLER, KEIN ERFOLG: 500\n`role_update_no_rows`. Das deckt eine stille Fehlbindung von Mandant\noder Zeilenschutz auf, statt der Oberflaeche eine falsche Rolle zu\nbestaetigen.\n\nErfordert `admin`. Jede tatsaechliche Aenderung geht mit alter und neuer\nRolle ins Rechte-Protokoll.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["user","admin"]}},"required":["role"]},"example":{"role":"user"}}}}}},"/api/v1/users/invitations":{"get":{"responses":{"200":{"description":"Einladungen aller Zustaende, hoechstens 200, neueste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":"string"},"status":{"type":"string"},"invitedBy":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","email","role","status","invitedBy","createdAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"id":"string","email":"string","role":"string","status":"string","invitedBy":"string","createdAt":"string"}]}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Rolle unter `admin`."}},"operationId":"getApiV1UsersInvitations","tags":["users"],"parameters":[],"summary":"Einladungen des Mandanten auflisten","description":"Listet die nicht geloeschten Einladungen, neueste zuerst. Erfordert\n`admin`.\n\nEnthaelt ALLE Zustaende, nicht nur offene — `status` unterscheidet sie\n(`draft`, `invited`, `accepted`). Wer nur die offenen sucht, filtert\nselbst.\n\nKEINE Paginierung, Abschnitt bei 200.\n\nDer Token der Einladung geht NICHT mit hinaus: der Serialisierer waehlt\nsechs Felder aus, obwohl die Abfrage `SELECT *` ist. Das ist der Grund,\nwarum hier eine Feldliste zugesagt werden kann."}},"/api/v1/users/invitations/{id}":{"patch":{"responses":{"200":{"description":"Rolle der Einladung gesetzt. Die geaenderte Einladung kommt zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":"string"},"status":{"type":"string"},"invitedBy":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","email","role","status","invitedBy","createdAt"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"id":"string","email":"string","role":"string","status":"string","invitedBy":"string","createdAt":"string"}}}}},"400":{"description":"Unbekannte Rolle, oder keine Kennung im Pfad."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Rolle unter `admin` — oder `INSUFFICIENT_ROLE_FOR_TARGET` bei einer Zielrolle ab `admin`."},"404":{"description":"`not_found` — keine OFFENE Einladung unter dieser Kennung."}},"operationId":"patchApiV1UsersInvitationsById","tags":["users"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Rolle einer offenen Einladung aendern","description":"Setzt die Rolle, die der Eingeladene beim Annehmen bekommt — solange er\nnoch nicht angenommen hat. Die Rolle wird beim Annehmen aus DIESER\nZeile gelesen, nie aus der Anfrage des Eingeladenen; deshalb reicht es,\nsie hier zu aendern, und es braucht keine neue Einladung.\n\nNUR OFFENE EINLADUNGEN. Getroffen werden ausschliesslich Zeilen mit\n`status = 'invited'`. Eine bereits angenommene Einladung und ein\nAltbestand im Zustand `draft` fallen beide unter denselben 404 — die\nMeldung unterscheidet die Faelle nicht.\n\nWie beim Einladen gilt die Aufstiegs-Schranke: eine Rolle ab\n`admin`-Ebene darf nur vergeben, wer selbst mindestens `admin` ist\n(403 `INSUFFICIENT_ROLE_FOR_TARGET`).\n\nEs geht KEINE neue Mail hinaus — der Link und sein Ablauf bleiben, wie\nsie sind. Zum erneuten Versenden dient\n`POST /users/invitations/{id}/resend`.\n\nErfordert `admin`, wirkt nur im eigenen Mandanten, und wird ins\nRechte-Protokoll geschrieben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["user","admin"]}},"required":["role"]},"example":{"role":"user"}}}}}},"/api/v1/users/invitations/{id}/resend":{"post":{"responses":{"200":{"description":"Mail versendet. `recipient` nennt die tatsaechliche Adresse.","content":{"application/json":{"schema":{"type":"object","properties":{"emailSent":{"type":"boolean","const":true},"recipient":{"type":"string"},"message":{"type":"string"}},"required":["emailSent","recipient","message"],"additionalProperties":false},"example":{"emailSent":true,"recipient":"string","message":"string"}}}},"400":{"description":"`invitation id missing`."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Rolle unter `admin`."},"404":{"description":"`not_found` — keine OFFENE Einladung unter dieser Id."},"502":{"description":"Der Mailversand schlug fehl. Gleiche Rumpfform, `emailSent: false`, dazu `error`.","content":{"application/json":{"schema":{"type":"object","properties":{"emailSent":{"type":"boolean","const":false},"recipient":{"type":"string"},"message":{"type":"string"},"error":{"type":"string"}},"required":["emailSent","recipient","message","error"],"additionalProperties":false}}}}},"operationId":"postApiV1UsersInvitationsByIdResend","tags":["users"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einladung erneut versenden — mit ehrlichem Fehlercode","description":"Verschickt die Einladungsmail noch einmal. Erfordert `admin`.\n\nDIESE ROUTE BEHAUPTET KEINEN ERFOLG, DEN SIE NICHT HAT. Klappt der\nVersand nicht, kommt ein **502** statt eines 200 — mit derselben\nRumpfform, aber `emailSent: false` und einem zusaetzlichen `error`. Wer\nnur auf den Rumpf sieht, findet die Aussage in `emailSent`.\n\nDER EMPFAENGER IST IM AUFRUF UEBERSCHREIBBAR. Ein `to` im Rumpf lenkt\ndie Mail an eine ANDERE Adresse als die der Einladung — gedacht fuer\neine korrigierte Adresse oder eine verifizierte, solange der\nMailversand noch in der SES-Sandbox haengt. Der zurueckgegebene\n`recipient` sagt, wohin es wirklich ging. Die Rolle `admin` ist damit\ndie einzige Schranke davor, einen gueltigen Zugangslink an eine\nbeliebige Adresse zu schicken.\n\nEin unlesbarer Rumpf wird verschluckt und als „keine Ueberschreibung\"\nbehandelt — es gibt dafuer KEINEN 400.\n\nDer 404 verlangt eine OFFENE Einladung (`status = invited`). Eine\nbereits angenommene oder ein Entwurf faellt ebenfalls hierunter."}},"/api/v1/users/invite":{"post":{"responses":{"201":{"description":"Einladung angelegt. `emailSent` sagt, ob der Versand klappte — auch `false` kommt mit 201.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":["object","null"],"properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":"string"},"status":{"type":"string"},"invitedBy":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","email","role","status","invitedBy","createdAt"],"additionalProperties":false},"emailSent":{"type":"boolean"},"message":{"type":"string"}},"required":["success","data","emailSent","message"],"additionalProperties":false},"example":{"success":true,"data":{"id":"string","email":"string","role":"string","status":"string","invitedBy":"string","createdAt":"string"},"emailSent":true,"message":"string"}}}},"400":{"description":"Keine gueltige E-Mail-Adresse, oder unbekannte Rolle."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Rolle unter `admin` — oder `INSUFFICIENT_ROLE_FOR_TARGET` bei einer Zielrolle ab `admin`."},"409":{"description":"`USER_EXISTS` (Benutzer gibt es schon) oder `INVITE_EXISTS` (offene Einladung laeuft bereits)."}},"operationId":"postApiV1UsersInvite","tags":["users"],"parameters":[],"summary":"Benutzer einladen — Einladung anlegen und Mail versenden","description":"Legt eine Einladung fuer eine E-Mail-Adresse an und verschickt sie.\nGespeichert wird eine Zeile im Zustand `invited` mit einem\nEinmal-Token, das **14 Tage** gilt; der Ablauf wird beim Annehmen\ngeprueft (`routes/invite-accept.ts`), ein abgelaufener Link wird\nabgelehnt.\n\nES GEHT WIRKLICH EINE MAIL RAUS — ueber den Systemversand\n(`lib/mailer.ts`: AWS SES, Resend oder der eigene SMTP-Zugang des\nMandanten). Das ist NICHT das Postfach-System `email-sync`, das\nKundenmails abholt. Die Mail enthaelt den Link\n`/einladung?token=…&t=<mandant>`.\n\n`emailSent` SAGT DIE WAHRHEIT — MIT EINER AUSNAHME. Der Wert kommt vom\nVersender. Schlaegt der Versand fehl, bleibt die Einladung trotzdem\ngespeichert, der Status ist weiterhin **201**, und `emailSent` ist\n`false`; erneut versenden geht ueber\n`POST /users/invitations/{id}/resend`. Die Ausnahme: laeuft die\nUmgebung ohne Mail-Anbieter, schreibt der Versender die Mail nur auf die\nKonsole und meldet Erfolg — dann steht `emailSent: true`, obwohl\nniemand etwas bekommen hat. Das betrifft Entwicklungsumgebungen ohne\n`EMAIL_PROVIDER`.\n\nES WIRD KEIN KONTO ANGELEGT. Die Route erzeugt nur die Einladung; das\nKonto entsteht, wenn der Eingeladene den Link oeffnet. Mandant und Rolle\nwerden dann aus der gespeicherten Zeile gelesen, nie aus seiner Anfrage.\n\nZWEI KONFLIKTE, BEIDE 409 UND UNTERSCHEIDBAR: `USER_EXISTS`, wenn schon\nein Benutzer mit dieser Adresse im Mandanten ist, und `INVITE_EXISTS`,\nwenn bereits eine offene Einladung dafuer laeuft. Ein zweiter Aufruf\nerzeugt also keine zweite Einladung und kein zweites Token.\n\nErfordert `admin`. Eine Rolle ab `admin`-Ebene darf nur vorbereiten, wer\nselbst mindestens `admin` ist (403 `INSUFFICIENT_ROLE_FOR_TARGET`). Ohne\nAngabe wird `user` eingeladen. Der Vorgang geht ins Rechte-Protokoll,\neinschliesslich der Frage, ob die Mail hinausging.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":255},"role":{"type":"string","enum":["user","admin"],"default":"user"}},"required":["email"]},"example":{"email":"beispiel@example.com","role":"user"}}}}}},"/api/v1/users/{id}":{"get":{"responses":{"200":{"description":"Der Benutzer.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string"}},"required":["id","name","email","role"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"id":"string","name":"string","email":"string","role":"string"}}}}},"400":{"description":"`id fehlt`."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`not_found` — unbekannt ODER fremder Mandant."}},"operationId":"getApiV1UsersById","tags":["users"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einen Benutzer des Mandanten lesen","description":"Liest einen Benutzer. Der Mandant kommt aus dem Sitzungskontext, NIE aus\nder Adresse — eine fremde Id ueber die Mandantengrenze zu erfragen endet\ndeshalb im 404 und nicht in fremden Daten.\n\nDer 404 deckt damit zwei Faelle ab: es gibt den Benutzer nicht, ODER er\ngehoert einem anderen Mandanten. Das ist Absicht.\n\nWie in der Liste ersetzt der Serialisierer `null`: fehlender Name wird\nleerer String, fehlende Rolle wird `user`.\n\nDiese Route steht als LETZTE in der Datei. Das ist kein Zufall: `/:id`\nwuerde sonst `me/permissions`, `invitations` und `invite` verschlucken."}},"/api/v1/invite/info":{"get":{"responses":{"200":{"description":"Einladung gueltig und offen.","content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"role":{"type":"string"},"tenantName":{"type":"string"}},"required":["email","role","tenantName"],"additionalProperties":false},"example":{"email":"string","role":"string","tenantName":"string"}}}},"400":{"description":"`invalid_tenant` — der Mandanten-Slug in `t` passt nicht auf das erlaubte Muster. Zusaetzlich, wenn `token` kuerzer als 16 oder laenger als 128 Zeichen ist."},"404":{"description":"`invite_invalid` — unbekannt, schon angenommen, abgelaufen, Tabelle fehlt oder Datenbank nicht erreichbar."}},"operationId":"getApiV1InviteInfo","tags":["auth"],"parameters":[{"in":"query","name":"token","schema":{"type":"string","minLength":16,"maxLength":128},"required":true},{"in":"query","name":"t","schema":{"type":"string","minLength":1,"maxLength":64},"required":true}],"summary":"Resolve an invitation token for display.","description":"Loest einen Einladungs-Token auf, damit die Annahmeseite anzeigen kann,\nwer wofuer eingeladen wurde.\n\nDie Route ist OEFFENTLICH und gibt gegen einen gueltigen Token die\nEINGELADENE E-MAIL-ADRESSE heraus. Das ist gewollt, denn die\nAnnahmeseite muss sie anzeigen; es macht den Token aber zu einem\nGeheimnis, das eine Adresse schuetzt. Wer ihn hat, kennt sie.\n\nDER 404 DECKT FUENF FAELLE AB, UND EINER DAVON IST EIN AUSFALL.\nToken unbekannt, Einladung bereits angenommen, Token abgelaufen,\nEinladungstabelle des Mandanten existiert nicht, ODER die Datenbank ist\nnicht erreichbar. Die ersten drei zusammenzufassen ist Absicht: sonst\nliesse sich an der Antwort ablesen, welche Token es gibt. Die letzten\nbeiden fallen bloss mit hinein — es gibt keinen 503. Ein 404 beweist\ndeshalb nicht, dass die Einladung ungueltig ist.\n\n`tenantName` faellt auf den Slug aus `t` zurueck, wenn der Name nicht\ngeladen werden kann. Das Feld ist immer gefuellt, aber nicht immer ein\nAnzeigename.","security":[]}},"/api/v1/invite/accept":{"post":{"responses":{"200":{"description":"Account created — mit der Adresse und der Rolle AUS DER EINLADUNG, nicht aus der Anfrage. Kein Sitzungs-Token.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"email":{"type":"string"},"role":{"type":"string"}},"required":["success","email","role"],"additionalProperties":false},"example":{"success":true,"email":"string","role":"string"}}}},"400":{"description":"`invalid_tenant` — der Mandanten-Slug in `t` passt nicht auf das erlaubte Muster."},"404":{"description":"Invitation invalid/expired"},"409":{"description":"User already exists"},"500":{"description":"`signup_failed` — das Konto konnte nicht angelegt werden."},"503":{"description":"Auth/DB unavailable"}},"operationId":"postApiV1InviteAccept","tags":["auth"],"parameters":[],"summary":"Accept an invitation: create the user and link it to the tenant.","description":"Nimmt eine Einladung an: legt das Konto an, haengt es an den einladenden\nMandanten und verbraucht den Token. Die Route ist OEFFENTLICH — der\nEingeladene ist noch nicht angemeldet; der Token ist das einzige, was den\nBeitritt autorisiert.\n\nE-MAIL UND ROLLE KOMMEN AUS DER GESPEICHERTEN EINLADUNG, nicht aus dem\nRumpf. Aus der Anfrage stammen nur Anzeigename und Passwort. Eine hoehere\nRolle laesst sich hier also nicht erschleichen, und `t` waehlt nur das\nMandanten-Schema aus, in dem gesucht wird.\n\nDer Token ist EINMALIG: nach Erfolg steht die Einladung auf `accepted`,\nein zweiter Aufruf ergibt 404. Existiert bereits ein Konto zu dieser\nAdresse, antwortet die Route 409 und laesst die Einladung offen.\n\nDREI SCHRITTE, KEINE TRANSAKTION: Konto anlegen, Mandant und Rolle\nsetzen, Einladung entwerten. Bricht es nach dem ersten Schritt ab, steht\ndas Konto ohne Mandanten da. Scheitert schon das Anlegen, kommt 500 und\nes aendert sich nichts.\n\nDie Antwort enthaelt KEINE Sitzung — der Eingeladene meldet sich danach\nnormal an.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":16,"maxLength":128},"t":{"type":"string","minLength":1,"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":120},"password":{"type":"string","minLength":8,"maxLength":200}},"required":["token","t","name","password"]},"example":{"token":"stringxxxxxxxxxx","t":"string","name":"string","password":"stringxx"}}}},"security":[]}},"/api/v1/custom-roles/custom-roles":{"get":{"responses":{"200":{"description":"Rollen des Mandanten, aelteste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"getApiV1Custom-rolesCustom-roles","tags":["permissions"],"parameters":[],"summary":"Alle eigenen Rollen des Mandanten auflisten","description":"Listet die selbst angelegten Rollen des Mandanten, je Zeile mit\n`user_count` — der Zahl der Mitglieder, die diese Rolle tragen.\n\nDie Zeilen kommen aus `SELECT cr.*` und werden NICHT serialisiert. Sie\ntragen deshalb snake_case und die Spalten der Tabelle, nicht eine\nausgewaehlte Aussenform. Der Vertrag sagt hier keine Feldnamen zu.\n\n`user_count` faellt auf 0 zurueck, wenn die Tabelle\n`organization_members` auf diesem Mandanten schlummert. Eine 0 heisst\nalso entweder „niemand hat diese Rolle\" oder „die Zahl war nicht\nermittelbar\". Die Antwort trennt das nicht."},"post":{"responses":{"201":{"description":"Rolle angelegt.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"409":{"description":"Eine Rolle mit diesem Namen gibt es bereits."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"postApiV1Custom-rolesCustom-roles","tags":["permissions"],"parameters":[],"summary":"Eigene Rolle anlegen","description":"Legt eine Rolle an. Die angelegte Zeile kommt roh aus `RETURNING`\nzurueck, also in snake_case und ohne ausgewaehlte Aussenform. Der\nVertrag sagt hier keine Feldnamen zu.\n\nEin bereits vergebener Name fuehrt zu 409, nicht zu einer stillen\nZweitanlage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z0-9_-]+$","minLength":1,"maxLength":50},"displayName":{"type":"string","minLength":1,"maxLength":100},"baseSystemRole":{"type":"string","enum":["admin","hr_manager","accountant","manager","warehouse","sales_rep","user","viewer"],"default":"user"},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","default":"#6366f1"},"icon":{"type":"string","maxLength":50,"default":"shield"},"description":{"type":"string","maxLength":500}},"required":["name","displayName"]},"example":{"name":"00000000-0000-4000-8000-000000000000","displayName":"string","baseSystemRole":"admin","icon":"string","description":"string"}}}}}},"/api/v1/custom-roles/custom-roles/{id}":{"get":{"responses":{"200":{"description":"Rolle mit Rechte-Matrix.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"displayName":{},"baseRole":{"type":"string"},"baseSystemRole":{"type":"string"},"color":{},"icon":{},"description":{},"createdAt":{},"updatedAt":{},"permissions":{"type":"object","additionalProperties":{"type":"object","properties":{"read":{"type":"object","properties":{"value":{"type":"boolean"},"inherited":{"type":"boolean"},"fromRole":{"type":"string"}},"required":["value","inherited"],"additionalProperties":false},"write":{"type":"object","properties":{"value":{"type":"boolean"},"inherited":{"type":"boolean"},"fromRole":{"type":"string"}},"required":["value","inherited"],"additionalProperties":false},"delete":{"type":"object","properties":{"value":{"type":"boolean"},"inherited":{"type":"boolean"},"fromRole":{"type":"string"}},"required":["value","inherited"],"additionalProperties":false},"export":{"type":"object","properties":{"value":{"type":"boolean"},"inherited":{"type":"boolean"},"fromRole":{"type":"string"}},"required":["value","inherited"],"additionalProperties":false}},"required":["read","write","delete","export"],"additionalProperties":false}}},"required":["baseRole","baseSystemRole","permissions"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"baseRole":"string","baseSystemRole":"string","permissions":{"beispiel":{"read":{"value":true,"inherited":true,"fromRole":"string"},"write":{"value":true,"inherited":true,"fromRole":"string"},"delete":{"value":true,"inherited":true,"fromRole":"string"},"export":{"value":true,"inherited":true,"fromRole":"string"}}}}}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found` — unbekannt oder fremder Mandant."}},"operationId":"getApiV1Custom-rolesCustom-rolesById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eine eigene Rolle samt Rechte-Matrix laden","description":"Liefert die Stammdaten der Rolle UND ihre Modul-Rechte als Matrix, damit\ndie Detailmaske ohne zweiten Aufruf rendern kann.\n\nIn `permissions` stehen NUR die Module, fuer die diese Rolle etwas\nausdruecklich gesetzt hat. Ein Modul, das dort fehlt, ist nicht\nverboten — es erbt vom Grundrecht in `baseSystemRole`. Wer die Matrix\nals vollstaendige Rechteliste liest, haelt geerbte Rechte faelschlich\nfuer fehlende.\n\nDie Werte der Stammfelder kommen aus einer untypisierten Zeile. Die\nSchluessel stehen fest, die Typen sagt der Vertrag nicht zu."},"put":{"responses":{"200":{"description":"Rolle geaendert.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"`NO_FIELDS_TO_UPDATE` — der Rumpf enthaelt kein aenderbares Feld."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found`."}},"operationId":"putApiV1Custom-rolesCustom-rolesById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigene Rolle bearbeiten","description":"Aendert die Stammdaten einer Rolle. Es ist ein Teil-Update: nur die\ngesendeten Felder werden geschrieben.\n\nEin Rumpf OHNE ein einziges bekanntes Feld ist ein 400\n(`NO_FIELDS_TO_UPDATE`), kein stiller Erfolg. Die Antwort traegt die\ngeaenderte Zeile roh, ohne Feldzusage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"displayName":{"type":"string","minLength":1,"maxLength":100},"baseSystemRole":{"type":"string","enum":["admin","hr_manager","accountant","manager","warehouse","sales_rep","user","viewer"],"default":"user"},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","default":"#6366f1"},"icon":{"type":"string","maxLength":50,"default":"shield"},"description":{"type":"string","maxLength":500},"modulePermissions":{"type":"array","items":{"type":"object","properties":{"module":{"type":"string","minLength":1,"maxLength":50},"canRead":{"type":"boolean","default":false},"canWrite":{"type":"boolean","default":false},"canDelete":{"type":"boolean","default":false},"canExport":{"type":"boolean","default":false}},"required":["module"]}}}},"example":{"displayName":"string","baseSystemRole":"admin","icon":"string","description":"string","modulePermissions":[{"module":"string","canRead":true,"canWrite":true,"canDelete":true,"canExport":true}]}}}}},"delete":{"responses":{"200":{"description":"Rolle geloescht. `resetUserCount` nennt die zurueckgesetzten Mitglieder.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"resetUserCount":{"type":"number"}},"required":["success","resetUserCount"],"additionalProperties":false},"example":{"success":true,"resetUserCount":0}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found`."}},"operationId":"deleteApiV1Custom-rolesCustom-rolesById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Eigene Rolle loeschen","description":"Loescht die Rolle. Mitglieder, die sie tragen, werden dabei auf ihre\nGrundrolle zurueckgesetzt — `resetUserCount` sagt, wie viele das waren.\n\nDas Loeschen scheitert also NICHT daran, dass die Rolle noch benutzt\nwird. Wer eine Sperre erwartet, bekommt stattdessen stillschweigend\nzurueckgestufte Mitglieder. `resetUserCount: 0` heisst, dass niemand\nbetroffen war."}},"/api/v1/custom-roles/custom-roles/{id}/module-permissions":{"get":{"responses":{"200":{"description":"Ausdruecklich gesetzte Modul-Rechte. Leer, wenn alles geerbt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found`."}},"operationId":"getApiV1Custom-rolesCustom-rolesByIdModule-permissions","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Modul-Rechte einer Rolle laden","description":"Liefert die AUSDRUECKLICH gesetzten Modul-Rechte dieser Rolle, als rohe\nZeilen aus der Datenbank.\n\nWas hier fehlt, ist nicht verboten, sondern geerbt. Eine leere Liste\nheisst „diese Rolle setzt nichts eigenes\", nicht „diese Rolle darf\nnichts\"."},"put":{"responses":{"200":{"description":"Rechte ersetzt. `count` ist die Zahl der geschriebenen Regeln.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"count":{"type":"number"}},"required":["success","count"],"additionalProperties":false},"example":{"success":true,"count":0}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found`."}},"operationId":"putApiV1Custom-rolesCustom-rolesByIdModule-permissions","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Modul-Rechte einer Rolle setzen (ersetzt alles)","description":"Setzt die Modul-Rechte der Rolle. Das ist ein VOLLSTAENDIGER ERSATZ,\nkein Teil-Update: was im Rumpf fehlt, ist danach nicht mehr\nausdruecklich gesetzt und faellt auf das Grundrecht zurueck.\n\n`count` nennt die Zahl der geschriebenen Regeln, nicht die der\nbetroffenen Mitglieder.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"permissions":{"type":"array","items":{"type":"object","properties":{"module":{"type":"string","minLength":1,"maxLength":50},"canRead":{"type":"boolean","default":false},"canWrite":{"type":"boolean","default":false},"canDelete":{"type":"boolean","default":false},"canExport":{"type":"boolean","default":false}},"required":["module"]}}},"required":["permissions"]},"example":{"permissions":[{"module":"string","canRead":true,"canWrite":true,"canDelete":true,"canExport":true}]}}}}}},"/api/v1/custom-roles/field-visibility":{"get":{"responses":{"200":{"description":"Eigene Regeln und globale Vorgaben gemischt, unterscheidbar an `is_global`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."}},"operationId":"getApiV1Custom-rolesField-visibility","tags":["permissions"],"parameters":[],"summary":"Feld-Sichtbarkeitsregeln auflisten","description":"Listet die Regeln, ab welcher Rolle ein Feld sichtbar ist.\n\nDie Liste MISCHT ZWEI HERKUENFTE: eigene Regeln des Mandanten und\nglobale Vorgaben, die fuer alle gelten. Zu unterscheiden sind sie am\nFeld `is_global`. Wer das uebersieht, haelt eine Plattformvorgabe fuer\neine eigene Einstellung und wundert sich, dass sie sich nicht loeschen\nlaesst.\n\nDie Zeilen kommen roh aus der Abfrage, also in snake_case und samt\n`tenant_id`. Der Vertrag sagt keine Feldnamen zu."},"post":{"responses":{"201":{"description":"Regel angelegt ODER eine bestehende ueberschrieben.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."}},"operationId":"postApiV1Custom-rolesField-visibility","tags":["permissions"],"parameters":[],"summary":"Feld-Sichtbarkeitsregel anlegen oder ueberschreiben","description":"Legt eine Regel an. Gibt es fuer dieselbe Kombination aus Entitaet und\nFeld schon eine, wird deren Mindestrolle UEBERSCHRIEBEN.\n\nDer Code ist deshalb immer 201, auch wenn nichts Neues entstanden ist.\nAus der Antwort ist nicht zu erkennen, ob angelegt oder ersetzt wurde.\n\nDie Zeile kommt roh aus `RETURNING *`, ohne Feldzusage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","minLength":1,"maxLength":50},"fieldName":{"type":"string","minLength":1,"maxLength":100},"minRole":{"type":"string","minLength":1,"maxLength":50}},"required":["entity","fieldName","minRole"]},"example":{"entity":"string","fieldName":"string","minRole":"string"}}}}}},"/api/v1/custom-roles/field-visibility/{id}":{"delete":{"responses":{"200":{"description":"Regel geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"],"additionalProperties":false},"example":{"success":true}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Rule not found` — unbekannt, oder es ist eine globale Vorgabe."}},"operationId":"deleteApiV1Custom-rolesField-visibilityById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Feld-Sichtbarkeitsregel loeschen","description":"Loescht eine EIGENE Regel des Mandanten.\n\nGlobale Vorgaben (`is_global`) gehoeren keinem Mandanten und lassen sich\nhier nicht entfernen; der Versuch endet in einem 404, nicht in einem\n403. Ein 404 heisst deshalb entweder „gibt es nicht\" oder „gehoert dir\nnicht\"."}},"/api/v1/custom-roles/overrides":{"get":{"responses":{"200":{"description":"Ausnahmen, neueste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"getApiV1Custom-rolesOverrides","tags":["permissions"],"parameters":[],"summary":"Persoenliche Rechte-Ausnahmen auflisten","description":"Listet die Ausnahmen, die einzelnen Mitgliedern zusaetzlich zu ihrer\nRolle gewaehrt oder entzogen wurden. Mit `userId` als Abfrageparameter\nauf ein Mitglied eingegrenzt.\n\nDie Zeilen kommen aus `SELECT upo.*` samt drei angehaengten Feldern aus\nder Nutzertabelle (`user_name`, `user_email`, `granted_by_name`). Sie\nsind roh, also snake_case; der Vertrag sagt keine Feldnamen zu.\n\nEs gibt keine Paginierung."},"post":{"responses":{"201":{"description":"Ausnahme angelegt.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."}},"operationId":"postApiV1Custom-rolesOverrides","tags":["permissions"],"parameters":[],"summary":"Persoenliche Rechte-Ausnahme anlegen","description":"Gewaehrt oder entzieht einem einzelnen Mitglied ein Recht, abweichend\nvon seiner Rolle. Die Ausnahme schlaegt die Rolle.\n\nDie angelegte Zeile kommt roh zurueck, ohne Feldzusage.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","minLength":1,"maxLength":255},"overrideType":{"type":"string","enum":["grant","deny"]},"scope":{"type":"string","enum":["module","field","endpoint"]},"scopeKey":{"type":"string","minLength":1,"maxLength":200},"reason":{"type":"string","minLength":1,"maxLength":500},"expiresAt":{"type":"string","format":"date-time"}},"required":["userId","overrideType","scope","scopeKey","reason"]},"example":{"userId":"string","overrideType":"grant","scope":"module","scopeKey":"string","reason":"string","expiresAt":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/custom-roles/overrides/{id}":{"delete":{"responses":{"200":{"description":"Ausnahme entfernt.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"],"additionalProperties":false},"example":{"success":true}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Override not found` — unbekannt oder fremder Mandant."}},"operationId":"deleteApiV1Custom-rolesOverridesById","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Persoenliche Rechte-Ausnahme entfernen","description":"Entfernt die Ausnahme. Das Mitglied faellt damit auf die Rechte seiner Rolle zurueck."}},"/api/v1/custom-roles/users/{userId}/ai-tools":{"get":{"responses":{"200":{"description":"Ausdruecklich gesetzte Werkzeugrechte, nach Klasse sortiert.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"toolClass":{"type":"string"},"allowed":{"type":"boolean"},"expiresAt":{"type":["string","null"]}},"required":["toolClass","allowed","expiresAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"toolClass":"string","allowed":true,"expiresAt":"string"}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"getApiV1Custom-rolesUsersByUserIdAi-tools","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"KI-Werkzeugrechte eines Mitglieds laden","description":"Liefert die ausdruecklich gesetzten Rechte dieses Mitglieds auf\nKI-Werkzeugklassen. Diese Route serialisiert, im Gegensatz zu den\nListen weiter oben: die Felder heissen `toolClass`, `allowed`,\n`expiresAt`.\n\nEine leere Liste heisst „nichts ausdruecklich gesetzt\", nicht „nichts\nerlaubt\" — ohne Eintrag gilt die Vorgabe der Rolle.\n\n`expiresAt` ist `null` bei unbefristeten Rechten. Ein abgelaufener\nEintrag wird hier weiterhin ausgeliefert; das Ablaufdatum wertet die\nPruefung aus, nicht diese Liste."},"put":{"responses":{"200":{"description":"Werkzeugrechte ersetzt.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"count":{"type":"number"}},"required":["success","count"],"additionalProperties":false},"example":{"success":true,"count":0}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"putApiV1Custom-rolesUsersByUserIdAi-tools","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"KI-Werkzeugrechte eines Mitglieds setzen (ersetzt alles)","description":"Setzt die Werkzeugrechte. VOLLSTAENDIGER ERSATZ: was im Rumpf fehlt,\nist danach nicht mehr ausdruecklich gesetzt und faellt auf die Vorgabe\nder Rolle zurueck. Ein leerer Rumpf loescht folglich alle Ausnahmen.\n\n`count` ist die Zahl der geschriebenen Eintraege.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"permissions":{"type":"array","items":{"type":"object","properties":{"toolClass":{"type":"string","enum":["PUBLIC","WRITE","DESTRUCTIVE"]},"allowed":{"type":"boolean"},"expiresAt":{"type":["string","null"],"format":"date-time"}},"required":["toolClass","allowed"]}}},"required":["permissions"]},"example":{"permissions":[{"toolClass":"PUBLIC","allowed":true,"expiresAt":"2026-01-01T12:00:00.000Z"}]}}}}}},"/api/v1/custom-roles/modules":{"get":{"responses":{"200":{"description":"Alle Rechte-Module in fester Reihenfolge.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"}},"required":["key","label"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"key":"string","label":"string"}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."}},"operationId":"getApiV1Custom-rolesModules","tags":["permissions"],"parameters":[],"summary":"Die verfuegbaren Rechte-Module auflisten","description":"Liefert die Liste der Module, auf die sich Rechte vergeben lassen — je\nEintrag ein Schluessel und ein deutsches Etikett.\n\nDie Liste ist FEST im Quelltext hinterlegt und nicht mandantenabhaengig.\nSie ist Teil des Vertrags mit der Oberflaeche: Reihenfolge und Schluessel\naendern sich nicht ohne Abstimmung. Es wird keine Datenbank befragt,\ndeshalb gibt es hier auch keinen 503."}},"/api/v1/custom-roles/me/modules":{"get":{"responses":{"200":{"description":"Je Modul: darf ich es sehen, und WOHER kommt diese Entscheidung. Nicht zu verwechseln mit /modules/enabled — das sagt, was der Mandant gebucht hat.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"module":{"type":"string"},"enabled":{"type":"boolean"},"source":{"type":"string"},"canWrite":{"type":"boolean"},"writeSource":{"type":"string"}},"required":["module","enabled","source","canWrite","writeSource"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false},"example":{"data":[{"module":"string","enabled":true,"source":"string","canWrite":true,"writeSource":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Custom-rolesMeModules","tags":["permissions"],"parameters":[],"summary":"Welche Module ich sehen und beschreiben darf","description":"Effektive Modul-Sichtbarkeit des eingeloggten Nutzers, mit Herkunft je\nModul. Keine Rollenpruefung: jeder Angemeldete fragt hier fuer sich\nselbst — die Navigation baut darauf auf.\n\nDie Antwort traegt IMMER alle bekannten Module, auch die verbotenen:\n`enabled` sagt, ob hineingesehen werden darf, `canWrite` getrennt davon,\nob gespeichert werden darf. Ein `enabled: true` ist also kein\n„darf alles\".\n\n`source` und `writeSource` nennen, WOHER die jeweilige Entscheidung\nkommt. Ohne sie ist ein „nein\" nicht von einem anderen „nein\" zu\nunterscheiden, und niemand weiss, an welcher Stelle man es aendern\nmuesste.\n\nNICHT zu verwechseln mit `GET /modules/enabled`: das sagt, was der\nMandant gebucht hat, nicht was dieser Nutzer darf.\n\nEs gibt weder Blaetterung noch Filter. Faellt die Aufloesung aus, kommt\n503 und keine leere Liste."}},"/api/v1/custom-roles/members/{userId}/modules":{"get":{"responses":{"200":{"description":"Aufgeloeste Modul-Rechte des Mitglieds.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["data"],"additionalProperties":false},"example":{"data":[{}]}}}},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"getApiV1Custom-rolesMembersByUserIdModules","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Aufgeloeste Modul-Rechte eines Mitglieds laden","description":"Liefert die FERTIG AUFGELOESTEN Rechte dieses Mitglieds: Grundrolle,\neigene Rolle und persoenliche Ausnahmen sind bereits verrechnet.\n\nDas unterscheidet die Route von `/custom-roles/{id}/module-permissions`,\ndie nur die ausdruecklich gesetzten Regeln EINER Rolle zeigt. Wer\nwissen will, was ein Mitglied wirklich darf, fragt hier."},"put":{"responses":{"200":{"description":"Rechte ersetzt.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"count":{"type":"number"}},"required":["success","count"],"additionalProperties":false},"example":{"success":true,"count":0}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"putApiV1Custom-rolesMembersByUserIdModules","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Modul-Rechte eines Mitglieds setzen (ersetzt alles)","description":"Setzt die persoenlichen Modul-Rechte des Mitglieds. VOLLSTAENDIGER\nERSATZ: was im Rumpf fehlt, ist danach nicht mehr gesetzt und faellt\nauf die Rolle zurueck.\n\n`count` ist die Zahl der geschriebenen Regeln.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"modules":{"type":"array","items":{"type":"object","properties":{"module":{"type":"string"},"enabled":{"type":"boolean"}},"required":["module","enabled"]}}},"required":["modules"]},"example":{"modules":[{"module":"string","enabled":true}]}}}}}},"/api/v1/custom-roles/members/{userId}/role":{"patch":{"responses":{"200":{"description":"Rolle zugewiesen oder entzogen.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"userId":{"type":"string"},"customRoleId":{"type":["string","null"]}},"required":["userId","customRoleId"],"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{"userId":"string","customRoleId":"string"}}}}},"400":{"description":"Der Rumpf passt nicht auf das Eingabe-Schema."},"401":{"description":"Keine Sitzung, oder der Mandanten-Kontext fehlt."},"403":{"description":"Die Rolle des Aufrufers reicht fuer diese Operation nicht."},"404":{"description":"`Custom role not found` — die zugewiesene Rolle gibt es nicht."},"503":{"description":"`database_unavailable` mit `retryAfter`."}},"operationId":"patchApiV1Custom-rolesMembersByUserIdRole","tags":["permissions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"userId","required":true}],"summary":"Einem Mitglied eine eigene Rolle zuweisen oder sie entziehen","description":"Weist dem Mitglied eine selbst angelegte Rolle zu. `customRoleId: null`\nentzieht sie und setzt das Mitglied auf seine Grundrolle zurueck — das\nist der vorgesehene Weg, kein Loeschen der Rolle.\n\nDie Antwort spiegelt nur, was gesetzt wurde. Sie sagt nichts darueber,\nwelche Rechte daraus folgen; dafuer gibt es\n`GET /permissions/members/{userId}/modules`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customRoleId":{"type":["string","null"],"minLength":1}},"required":["customRoleId"]},"example":{"customRoleId":"string"}}}}}},"/api/v1/custom-modules":{"get":{"responses":{"200":{"description":"Die aktiven Module, aelteste zuerst. Auch die Antwort, wenn es die Registry noch gar nicht gibt — dann leer.","content":{"application/json":{"schema":{"type":"object","properties":{"modules":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["modules"]},"example":{"modules":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — `error: \"query_failed\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"getApiV1Custom-modules","tags":["Eigene Module"],"parameters":[],"summary":"Eigene Module auflisten","description":"Listet die aktiven eigenen Module des Mandanten — die Tabellen, die ueber die KI-Baustrecke entstanden sind.\n\nDie Abfrage ist ein `SELECT *` auf `custom_module_registry`; die Zeilen gehen UNVERAENDERT hinaus, ohne Serialisierer. Deshalb sagt der Vertrag hier KEINE Feldnamen zu: was die Tabelle traegt, traegt die Antwort. Heute sind das unter anderem `module_id`, `table_name`, `display_name`, `icon`, `nav_section`, `fields_json` und `active` — verlassen sollte sich darauf niemand, denn eine neue Spalte erscheint hier ohne Vertragsaenderung.\n\nNur `active = true`. Es gibt keine Blaetterung.\n\nFEHLT DIE REGISTRY GANZ, ist das kein Fehler, sondern der Normalzustand eines Mandanten ohne eigene Module: 200 mit leerer Liste. Andere Datenbankfehler kommen als 500 heraus, nicht als leere Liste."}},"/api/v1/custom-modules/{id}/data":{"get":{"responses":{"200":{"description":"Ausschnitt plus Gesamtzahl. Die Spalten bestimmt das Modul.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","additionalProperties":{}}},"total":{"type":"integer"}},"required":["items","total"]},"example":{"items":[{}],"total":0}}}},"400":{"description":"Der Modulname passt nicht auf `[a-z][a-z0-9_]{0,59}`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein eingetragenes eigenes Modul unter diesem Namen. Deckt drei Faelle ab, die von aussen nicht zu unterscheiden sind: es gibt das Modul nicht, seine Tabelle wurde nie angelegt, oder der Name benennt eine Tabelle, die kein eigenes Modul ist (etwa eine Kerntabelle).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error: \"query_failed\"`, mit der Datenbankmeldung im Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"getApiV1Custom-modulesByIdData","tags":["Eigene Module"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Zeilen eines eigenen Moduls lesen","description":"Liest die Zeilen der Modultabelle, neueste zuerst.\n\n`{id}` IST DER TABELLENNAME, kein Nachschlagewert — Bindestriche werden zu Unterstrichen. Der Name muss in `custom_module_registry` stehen; jeder andere Name ergibt 404, auch wenn eine Tabelle so heisst.\n\nDie Abfrage ist ein `SELECT *` auf eine Tabelle, deren Spalten der Mandant selbst bestimmt hat. Der Vertrag kann hier keine Feldnamen zusagen; welche es sind, sagt `fields_json` aus der Modulliste.\n\n`limit` ist bei 200 gedeckelt (Standard 50), `offset` bei 0 nach unten. Ein nicht-numerischer Wert ergibt keine Fehlermeldung, sondern laeuft in einen 500 — der Vertrag beschreibt das, statt es zu beschoenigen. `total` ist die Gesamtzahl der Zeilen, unabhaengig vom Ausschnitt."},"post":{"responses":{"201":{"description":"Angelegt. `item` ist die vollstaendige neue Zeile.","content":{"application/json":{"schema":{"type":"object","properties":{"item":{"type":"object","additionalProperties":{}}},"required":["item"]},"example":{"item":{}}}}},"400":{"description":"Modulname ungueltig, Rumpf kein JSON, Rumpf leer — ODER die Datenbank hat abgelehnt (unbekannte Spalte, verletzte Bedingung). Im letzten Fall steht die ROHE Datenbankmeldung in `error`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"404":{"description":"Kein eingetragenes eigenes Modul unter diesem Namen. Deckt drei Faelle ab, die von aussen nicht zu unterscheiden sind: es gibt das Modul nicht, seine Tabelle wurde nie angelegt, oder der Name benennt eine Tabelle, die kein eigenes Modul ist (etwa eine Kerntabelle).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1Custom-modulesByIdData","tags":["Eigene Module"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Zeile in einem eigenen Modul anlegen","description":"Legt eine Zeile in der Modultabelle an. Die Schluessel des Rumpfes werden zu Spaltennamen — `id`, `created_at` und `updated_at` werden vorher entfernt, die vergibt die Datenbank.\n\nES GIBT KEIN EINGABESCHEMA. Der Rumpf wird nicht gegen die Felder des Moduls geprueft; ein unbekannter Spaltenname faellt erst in der Datenbank auf und kommt als 400 mit der rohen Meldung zurueck. Wer die gueltigen Felder braucht, liest `fields_json` aus der Modulliste.\n\n`{id}` muss ein eingetragenes Modul benennen (siehe Dateikopf); jeder andere Name ergibt 404. Mindestrolle `manager`.\n\nDie Antwort gibt die angelegte Zeile per `RETURNING *` zurueck — mit allen Spalten, auch den von der Datenbank gesetzten. Feldnamen sagt der Vertrag deshalb nicht zu."}},"/api/v1/custom-modules/{id}/data/{recordId}":{"patch":{"responses":{"200":{"description":"Geaendert. `item` ist die vollstaendige Zeile danach.","content":{"application/json":{"schema":{"type":"object","properties":{"item":{"type":"object","additionalProperties":{}}},"required":["item"]},"example":{"item":{}}}}},"400":{"description":"Modulname ungueltig, Rumpf kein JSON, keine Felder — ODER die Datenbank hat abgelehnt. Dann steht die ROHE Meldung in `error`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"404":{"description":"Kein eingetragenes Modul unter diesem Namen ODER keine Zeile mit dieser Kennung — `module_not_initialized` bzw. `Record not found`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"patchApiV1Custom-modulesByIdDataByRecordId","tags":["Eigene Module"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"recordId","required":true}],"summary":"Zeile eines eigenen Moduls aendern","description":"Aendert die genannten Felder einer Zeile; `updated_at` wird mitgesetzt. `id` und `created_at` werden aus dem Rumpf entfernt und bleiben unangetastet — `updated_at` dagegen NICHT: ein `updated_at` im Rumpf wird uebernommen und danach vom Ausdruck der Abfrage ueberschrieben.\n\nEs ist ein echtes Teil-Update: nicht genannte Felder bleiben stehen. Ein Rumpf ohne Felder ergibt 400, nicht eine leere Aenderung.\n\nWie beim Anlegen gibt es KEIN Eingabeschema; unbekannte Spalten faellt erst die Datenbank auf, als 400 mit roher Meldung.\n\n`{id}` muss ein eingetragenes Modul benennen (siehe Dateikopf). Mindestrolle `manager`."},"delete":{"responses":{"200":{"description":"Die Zeile wurde geloescht. `deleted` traegt ihre Kennung zurueck.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"deleted":{"type":"string"}},"required":["success","deleted"]},"example":{"success":true,"deleted":"string"}}}},"400":{"description":"Modulname ungueltig ODER die Datenbank hat abgelehnt — im zweiten Fall mit der ROHEN Meldung in `error` (etwa eine Fremdschluessel-bindung, die die Zeile haelt).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"404":{"description":"ZWEI Faelle, am `error`-Feld zu unterscheiden: `module_not_initialized` — das Modul ist bei diesem Mandanten nicht eingerichtet. `record_not_found` — das Modul gibt es, die Zeile nicht (oder nicht mehr).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"deleteApiV1Custom-modulesByIdDataByRecordId","tags":["Eigene Module"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"recordId","required":true}],"summary":"Zeile eines eigenen Moduls loeschen","description":"Loescht die Zeile ENDGUELTIG — es gibt hier kein `deleted_at` und keinen Weg zurueck. Eigene Module fuehren keinen Soft-Delete.\n\n`{id}` muss ein eingetragenes Modul benennen (siehe Dateikopf). Mindestrolle `manager`.\n\nTRIFFT DIE KENNUNG KEINE ZEILE, KOMMT 404 (geaendert 17.08.2026). Bis dahin meldete die Route `success: true` auch dann, wenn es die Zeile nie gab — waehrend das Aendern daneben in genau demselben Fall 404 gab. Zwei Nachbarrouten, dieselbe Lage, zwei Aussagen; wer eine Loeschung protokollierte, schrieb Vorgaenge mit, die nie stattfanden."}},"/api/v1/custom-pages":{"get":{"responses":{"200":{"description":"Die aktiven Seiten mit Kennung, Kuerzel, Titel, Beschreibung und Anlagedatum. `title` und `description` sind `null`, wenn die Konfiguration sie nicht enthaelt — der Aufbau erzwingt sie nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"pages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"title":{"type":["string","null"]},"description":{"type":["string","null"]},"active":{"type":"boolean"},"created_at":{"type":"string"}},"required":["id","slug","title","description","active","created_at"]}}},"required":["pages"]},"example":{"pages":[{"id":"string","slug":"string","title":"string","description":"string","active":true,"created_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — `error: \"query_failed\"`. DIESE Route unterscheidet richtig; die drei anderen melden denselben Fall als 400.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"query_failed"},"message":{"type":"string"}},"required":["error","message"]}}}}},"operationId":"getApiV1Custom-pages","tags":["Eigene Seiten"],"parameters":[],"summary":"Eigene Seiten auflisten","description":"Die aktiven eigenen Dashboard-Seiten des Mandanten, aelteste zuerst. Sie erscheinen in der Oberflaeche unter `/c/{slug}`.\n\nDiese Liste ist eine UEBERSICHT, nicht der Inhalt: sie holt aus der Konfiguration nur `title` und `description` heraus. Der eigentliche Seitenaufbau steht in `config` und kommt erst beim Einzelabruf mit.\n\nGeloeschte Seiten (`active = false`) erscheinen nicht, und es gibt keinen Schalter, sie zu sehen. Keine Blaetterung.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten darf das."},"post":{"responses":{"201":{"description":"Angelegt ODER ueberschrieben — im zweiten Fall zugleich wieder aktiv.","content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"object","additionalProperties":{}}},"required":["page"]},"example":{"page":{}}}}},"400":{"description":"Sammelfehler mit der ROHEN Meldung in `error` — darunter auch „database unavailable\", das hier faelschlich als Client-Fehler erscheint statt als 503.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unterhalb von `admin` — `code: \"INSUFFICIENT_ROLE\"`. Es wurde nichts angelegt und nichts ueberschrieben.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"}},"required":["error","code"]}}}}},"operationId":"postApiV1Custom-pages","tags":["Eigene Seiten"],"parameters":[],"summary":"Eigene Seite anlegen oder ueberschreiben","description":"DER STATUS 201 IST NICHT WOERTLICH ZU NEHMEN: das Statement traegt `ON CONFLICT (slug) DO UPDATE`. Gibt es das Kuerzel schon, wird die Seite UEBERSCHRIEBEN und trotzdem 201 gemeldet. Die Antwort sagt nicht, welcher der beiden Faelle eintrat.\n\nSEIT 17.08.2026 schaltet ein solcher Ueberschreib-Vorgang die Seite zugleich wieder AKTIV. Das ist der einzige Weg, eine geloeschte Seite zurueckzuholen — vorher blieb sie unter demselben Kuerzel fuer immer unsichtbar, obwohl der Aufruf 201 meldete.\n\nES GIBT KEIN EINGABESCHEMA. Der Rumpf wird nicht geprueft: erwartet werden `slug` und `config`. FEHLT `config`, WIRD DER GANZE RUMPF ALS KONFIGURATION GESPEICHERT (`body.config ?? body`) — auch ein `slug`-Feld landet dann mit darin. Fehlt `slug`, lehnt die Datenbank ab und die rohe Meldung kommt als 400 zurueck. `slug` ist auf 50 Zeichen begrenzt.\n\nDie Antwort gibt die Zeile per `RETURNING *` zurueck; Feldnamen sagt der Vertrag deshalb nicht zu.\n\nNUR ADMINISTRATOREN. Eine niedrigere Rolle bekommt 403 mit `code: \"INSUFFICIENT_ROLE\"`. Bis zum 17.08.2026 durfte das jeder angemeldete Benutzer des Mandanten — auch das Ueberschreiben einer fremden Seite."}},"/api/v1/custom-pages/{slug}":{"get":{"responses":{"200":{"description":"Die Seite mit ihrem vollstaendigen Aufbau in `config`.","content":{"application/json":{"schema":{"type":"object","properties":{"page":{"type":"object","additionalProperties":{}}},"required":["page"]},"example":{"page":{}}}}},"400":{"description":"Sammelfehler mit der ROHEN Meldung in `error` — darunter auch „database unavailable\", das hier faelschlich als Client-Fehler erscheint statt als 503.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Kein Kuerzel dieser Art ODER geloescht — `Page not found`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1Custom-pagesBySlug","tags":["Eigene Seiten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Eine eigene Seite samt Aufbau","description":"Die vollstaendige Zeile inklusive `config` — dem JSON, aus dem die Oberflaeche die Seite baut.\n\nDie Abfrage ist ein `SELECT *` und reicht die Zeile OHNE Serialisierer hinaus; der Vertrag sagt deshalb keine Feldnamen zu. Heute sind es `id`, `slug`, `config`, `active`, `created_at` und `updated_at`. Was in `config` steht, bestimmt allein der Mandant — es gibt kein Schema dafuer.\n\nNur aktive Seiten. Der 404 heisst deshalb „gibt es nicht ODER wurde geloescht\" — beides ist von aussen nicht zu unterscheiden.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten darf das."},"delete":{"responses":{"200":{"description":"Der Befehl lief. Sagt NICHT, dass eine Seite getroffen wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]},"example":{"success":true}}}},"400":{"description":"Sammelfehler mit der ROHEN Meldung in `error` — darunter auch „database unavailable\", das hier faelschlich als Client-Fehler erscheint statt als 503.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unterhalb von `admin` — `code: \"INSUFFICIENT_ROLE\"`. Die Seite bleibt sichtbar.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"}},"required":["error","code"]}}}}},"operationId":"deleteApiV1Custom-pagesBySlug","tags":["Eigene Seiten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Eigene Seite ausblenden","description":"Setzt `active = false`. Die Zeile bleibt samt Konfiguration stehen — es wird nichts geloescht. Zurueck geht es ueber `POST /` mit demselben Kuerzel; das schaltet sie wieder aktiv.\n\nDIE ANTWORT SAGT NICHT, OB ES DIE SEITE GAB. Es wird nicht geprueft, ob das Kuerzel eine Zeile getroffen hat: `success: true` kommt auch bei einem erfundenen Kuerzel und bei einer bereits ausgeblendeten Seite.\n\nDiese Route legt die Tabelle NICHT an (die drei anderen tun das). Bei einem Mandanten, der noch nie eine eigene Seite hatte, faellt sie deshalb in den 400-Sammelfehler statt still durchzulaufen.\n\nNUR ADMINISTRATOREN. Eine niedrigere Rolle bekommt 403 mit `code: \"INSUFFICIENT_ROLE\"`. Bis zum 17.08.2026 durfte das jeder angemeldete Benutzer des Mandanten — auch das Ueberschreiben einer fremden Seite."}},"/api/v1/tenants/me":{"get":{"responses":{"200":{"description":"Mandant samt Branding. `fieldVisibility` erscheint NUR mit Nutzerkontext. brand_color und country sind Vorgaben (#2563eb / DE), wenn nichts gepflegt ist; nicht gepflegte Textfelder kommen als Leerstring, nicht als null.","content":{"application/json":{"schema":{"type":"object","properties":{"fieldVisibility":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},"tenant":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"tenant_number":{"type":["string","null"]},"plan":{"type":"string"},"brand_color":{"type":"string"},"brand_logo_url":{"type":["string","null"]},"imprint_vat_id":{"type":"string"},"imprint_trade_register":{"type":"string"},"imprint_ceo_name":{"type":"string"},"website":{"type":"string"},"phone":{"type":"string"},"tax_number":{"type":"string"},"industry":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"},"created_at":{"type":"string"}},"required":["id","name","slug","tenant_number","plan","brand_color","brand_logo_url","imprint_vat_id","imprint_trade_register","imprint_ceo_name","website","phone","tax_number","industry","address","city","zip","country","created_at"],"additionalProperties":false}},"required":["tenant"],"additionalProperties":false},"example":{"fieldVisibility":{"beispiel":["string"]},"tenant":{"id":"string","name":"string","slug":"string","tenant_number":"string","plan":"string","brand_color":"string","brand_logo_url":"string","imprint_vat_id":"string","imprint_trade_register":"string","imprint_ceo_name":"string","website":"string","phone":"string","tax_number":"string","industry":"string","address":"string","city":"string","zip":"string","country":"string","created_at":"string"}}}}},"401":{"description":"Unauthorized"}},"operationId":"getApiV1TenantsMe","tags":["tenants"],"parameters":[],"description":"Tenant-Daten inkl. Branding abrufen. Liefert Stammdaten, Impressumsfelder und Logo des Mandanten aus der Sitzung; das Logo faellt auf `settings.logoUrl` zurueck, wenn die Spalte leer ist. Der Plan kommt aus dem Mandantenkontext des Servers und ist nicht vom Client beeinflussbar. Mit `?include=fieldVisibility` kommt zusaetzlich die Sperrliste sensibler Felder des Nutzers.","summary":"Tenant-Daten inkl. Branding abrufen","x-nemix-summary-source":"description:first-sentence"},"put":{"responses":{"200":{"description":"Gespeichert. Kommt auch bei einem Rumpf ohne Felder.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"],"additionalProperties":false},"example":{"success":true}}}},"400":{"description":"Validierungsfehler — `name` darf nicht leer sein, wenn es mitkommt."},"401":{"description":"Keine Sitzung oder kein Mandantenkontext."},"403":{"description":"Rolle unter `admin`."},"500":{"description":"Das UPDATE scheiterte; die Meldung der Datenbank geht mit hinaus."},"503":{"description":"Keine Datenbankverbindung (`database unavailable`)."}},"operationId":"putApiV1TenantsMe","tags":["tenants"],"parameters":[],"summary":"Stammdaten des eigenen Mandanten aendern","description":"Schreibt die Firmenstammdaten des Mandanten aus der Sitzung — Name,\nBranche, Web-Adresse, Telefon, Anschrift, Ort, Postleitzahl, Land,\nSteuernummer. Ein anderer Mandant ist nicht erreichbar: die Kennung kommt\naus dem Sitzungskontext, nie aus Pfad oder Rumpf.\n\nGESCHRIEBEN WIRD NUR, WAS MITGESCHICKT WIRD. Fehlende Felder bleiben\nunberuehrt. Ein mitgeschickter LEERER String ist dagegen ein Wert und\nueberschreibt den alten — anders als bei `/me/branding` und\n`/me/imprint`, die mit COALESCE arbeiten. Wer ein Feld leeren will,\nschickt es hier ausdruecklich als `\"\"` mit.\n\nEin Rumpf ganz ohne Felder ist erlaubt und antwortet `success: true`,\nohne die Datenbank anzufassen.\n\nDie Antwort enthaelt die neuen Werte NICHT — nur `success`. Den Stand\ndanach liefert `GET /tenants/me`.\n\nFehlende Spalten werden vor dem Schreiben idempotent ergaenzt\n(`ensureTenantsTableColumns`), damit aeltere Mandanten nicht an\n„column does not exist\" scheitern.\n\nErfordert `admin`. `plan`, `slug` und `tenant_number` sind hier nicht\naenderbar.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"industry":{"type":"string"},"website":{"type":"string"},"phone":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"},"tax_number":{"type":"string"}}},"example":{"name":"string","industry":"string","website":"string","phone":"string","address":"string","city":"string","zip":"string","country":"string","tax_number":"string"}}}}}},"/api/v1/tenants/me/branding":{"put":{"responses":{"200":{"description":"Gespeichert. Kommt auch, wenn beide Felder fehlten und nichts geschah.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"],"additionalProperties":false},"example":{"success":true}}}},"400":{"description":"Keine gueltige `http(s)`-Adresse, oder `brand_color` nicht in der Form `#rrggbb`."},"401":{"description":"Keine Sitzung oder kein Mandantenkontext."},"403":{"description":"Rolle unter `admin`."},"500":{"description":"Das UPDATE scheiterte; die Meldung der Datenbank geht mit hinaus."},"503":{"description":"Keine Datenbankverbindung (`database unavailable`)."}},"operationId":"putApiV1TenantsMeBranding","tags":["tenants"],"parameters":[],"summary":"Logo-Adresse und Hausfarbe des Mandanten setzen","description":"Speichert `brand_logo_url` und `brand_color` des eigenen Mandanten. Beide\nFelder sind einzeln optional.\n\nNULL LOESCHT NICHTS. Beide Werte gehen durch `COALESCE(neu, alt)` — ein\nausgelassenes Feld UND ein ausdrueckliches `null` lassen den alten Wert\nstehen. Wer das Logo entfernen will, schickt einen LEEREN String; der\nist fuer COALESCE ein Wert und ueberschreibt.\n\nDIES IST NICHT DER WEG, EIN LOGO HOCHZULADEN. Hier wird nur eine\n`http(s)`-Adresse hinterlegt — der Validator laesst nichts anderes durch.\nEin hochgeladenes Bild legt `POST /settings/logo` als Data-URI in\n`settings.logoUrl` ab. `GET /tenants/me` liefert beide Quellen unter\n`brand_logo_url`, mit dieser Spalte zuerst.\n\n`brand_color` muss die Form `#rrggbb` haben (sechs Hex-Stellen,\nKurzform `#fff` wird abgelehnt).\n\nDie Antwort enthaelt die neuen Werte nicht — nur `success`.\n\nErfordert `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"brand_logo_url":{"anyOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},{"type":"null"}]},"brand_color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"}}},"example":{"brand_logo_url":"https://example.com"}}}}}},"/api/v1/tenants/me/imprint":{"put":{"responses":{"200":{"description":"Gespeichert. Kommt auch, wenn kein Feld mitkam und nichts geschah.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"],"additionalProperties":false},"example":{"success":true}}}},"400":{"description":"Ein Feld war kein String."},"401":{"description":"Keine Sitzung oder kein Mandantenkontext."},"403":{"description":"Rolle unter `admin`."},"500":{"description":"Das UPDATE scheiterte; die Meldung der Datenbank geht mit hinaus."},"503":{"description":"Keine Datenbankverbindung (`database unavailable`)."}},"operationId":"putApiV1TenantsMeImprint","tags":["tenants"],"parameters":[],"summary":"Impressum-Pflichtangaben des Mandanten setzen","description":"Speichert die drei Angaben, die unter Belegen und im Impressum stehen:\nUmsatzsteuer-Identifikationsnummer (`imprint_vat_id`), Handelsregister-\nEintrag (`imprint_trade_register`) und den Namen der\nGeschaeftsfuehrung (`imprint_ceo_name`). Alle drei sind einzeln optional.\n\nNULL LOESCHT NICHTS — wie bei `/me/branding` gilt `COALESCE(neu, alt)`.\nEin ausgelassenes Feld und ein ausdrueckliches `null` lassen den alten\nWert stehen; ein leerer String ueberschreibt ihn.\n\nES WIRD NICHTS GEPRUEFT. Die Umsatzsteuer-Identifikationsnummer wird\nweder auf ihr Format noch gegen ein Register geprueft — es ist ein\nfreier Text. Die getrennte Spalte `vat_id` (Einstellungen/Firma) fasst\ndiese Route nicht an.\n\nDie Antwort enthaelt die neuen Werte nicht — nur `success`. `GET\n/tenants/me` gibt die drei Felder als Leerstring zurueck, solange nichts\ngepflegt ist.\n\nErfordert `admin`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"imprint_vat_id":{"type":"string"},"imprint_trade_register":{"type":"string"},"imprint_ceo_name":{"type":"string"}}},"example":{"imprint_vat_id":"string","imprint_trade_register":"string","imprint_ceo_name":"string"}}}}}},"/api/v1/tenants/sichtbar":{"get":{"responses":{"200":{"description":"Die sichtbaren Mandanten, nach Namen sortiert.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"tenantNumber":{"type":["string","null"]}},"required":["id","slug","name","tenantNumber"]}},"total":{"type":"number"}},"required":["data","total"]},"example":{"data":[{"id":"string","slug":"string","name":"string","tenantNumber":"string"}],"total":0}}}},"401":{"description":"Nicht angemeldet"}},"operationId":"getApiV1TenantsSichtbar","tags":["tenants"],"parameters":[],"summary":"Die Mandanten, die der angemeldete Nutzer sehen darf","description":"Die Reichweite haengt an der Rolle (Festlegung vom 10.09.2026): `super_admin` sieht ALLE Mandanten des Systems; wer in `organization_users` Eigentuemer oder Admin einer Organisation ist (Global Admin), sieht alle Mandanten dieser Organisation(en); alle anderen die, auf die sie ueber `organization_tenant_access` einen ausdruecklichen Zugriff haben. Gerechnet wird das an EINER Stelle (lib/mandanten-reichweite.ts) — dieselbe, die der Wechsel unten benutzt."}},"/api/v1/tenants/{idOderSlug}/wechseln":{"post":{"responses":{"200":{"description":"Gewechselt. Nebenwirkung: der Cookie ist gesetzt.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"}},"required":["ok","tenantId","tenantSlug"]},"example":{"ok":true,"tenantId":"string","tenantSlug":"string"}}}},"400":{"description":"Kennung fehlt oder ist zu lang"},"401":{"description":"Nicht angemeldet"},"403":{"description":"Dieser Mandant gehoert nicht zur Reichweite des Aufrufers"}},"operationId":"postApiV1TenantsByIdOderSlugWechseln","tags":["tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"idOderSlug","required":true}],"summary":"Wechselt den Mandanten-Kontext, wenn der Aufrufer ihn sehen darf","description":"Setzt den httpOnly-Cookie `nemix-active-tenant-slug` (24 h) — DER entscheidet ab dann, welchen Mandanten nachfolgende Aufrufe sehen. Erlaubt ist genau, was `GET /tenants/sichtbar` auflistet: `super_admin` jeder Mandant, Global Admin seine Gruppe, sonst die zugeteilten. KEINE Genehmigung und KEIN Protokoll (Mathias, 10.09.2026: „ersteinmal ohne genehmigung und protokoll ... Das soll später erst gebaut werden, wenn ich das sage\")."}},"/api/v1/proactive-insights":{"get":{"responses":{"200":{"description":"Die Hinweise, neueste zuerst. ACHTUNG: eine leere Liste kann auch heissen, dass die Abfrage scheiterte — der Handler faengt das ab und antwortet trotzdem 200. Der Grund steht dann nur im Server-Protokoll.","content":{"application/json":{"schema":{"type":"object","properties":{"insights":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","description":"briefing | dunning | reorder_warning | payroll_reminder"},"title":{"type":"string"},"message":{"type":["string","null"]},"link":{"type":["string","null"],"description":"Ziel in der Oberflaeche, sofern hinterlegt"},"read_at":{"type":["string","null"],"description":"null = noch nicht gelesen"},"entity_type":{"type":["string","null"]},"entity_id":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","type","title","message","link","read_at","entity_type","entity_id","created_at"]}},"total":{"type":"integer","description":"Treffer OHNE Limit — die ganze Filtermenge"}},"required":["insights","total"]},"example":{"insights":[{"id":"string","type":"string","title":"string","message":"string","link":"string","read_at":"string","entity_type":"string","entity_id":"string","created_at":"string"}],"total":0}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"getApiV1Proactive-insights","tags":["proactive-insights"],"parameters":[{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"offset","schema":{"type":"number","minimum":0,"default":0}},{"in":"query","name":"unread","schema":{"type":"string","enum":["true","false","1","0","yes","no","on","off"]}}],"summary":"Listet proaktive KI-Hinweise fuer diesen Mandanten","description":"Listet proaktive KI-Hinweise für den Tenant (Briefing, Mahnungen, Lagerwarnung, Lohnlauf)"}},"/api/v1/proactive-insights/{id}/dismiss":{"post":{"responses":{"200":{"description":"Als gelesen markiert.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"id":{"type":"string","description":"Die Id aus dem Pfad, unveraendert zurueckgespiegelt"}},"required":["success","id"]},"example":{"success":true,"id":"string"}}}},"400":{"description":"Kein Mandantenkontext"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Nicht vorhanden ODER bereits gelesen"},"500":{"description":"Interner Fehler — die Abfrage selbst schlug fehl"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"postApiV1Proactive-insightsByIdDismiss","tags":["proactive-insights"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt einen Hinweis auf gelesen. Der Aufruf kennt nur diese eine Richtung — wieder auf ungelesen laesst er sich hier nicht setzen. Er wirkt genau EINMAL: geaendert wird nur, was noch ungelesen ist; ein zweiter Aufruf ergibt 404, nicht 200. „Nicht vorhanden\" und „schon gelesen\" sind dabei NICHT unterscheidbar. Ein echter Datenbankfehler kommt als 500, damit er nicht als 404 durchgeht.","summary":"Setzt einen Hinweis auf gelesen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/customer-portal/provision":{"post":{"responses":{"200":{"description":"Es gab bereits ein Portal — es kommt unveraendert zurueck, `provisioned: false`. Denselben Code (und `status: \"pending_db\"`, `id: \"\"`) liefert der Fall ohne Datenbankverbindung: dann ist nur der Vorschlag fuer Name und Slug berechnet, angelegt wurde nichts.","content":{"application/json":{"schema":{"type":"object","properties":{"portal":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"},"wasCreated":{"type":"boolean"}},"required":["id","name","slug","status","createdAt","wasCreated"]},"provisioned":{"type":"boolean"}},"required":["portal","provisioned"]},"example":{"portal":{"id":"string","name":"string","slug":"string","status":"string","createdAt":"string","wasCreated":true},"provisioned":true}}}},"201":{"description":"Portal neu angelegt, `provisioned: true`. Der Slug wird bei Kollision durchnummeriert; ein gleichzeitiger zweiter Aufruf laeuft in `ON CONFLICT DO NOTHING` und bekommt darum 200 statt 201.","content":{"application/json":{"schema":{"type":"object","properties":{"portal":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"},"wasCreated":{"type":"boolean"}},"required":["id","name","slug","status","createdAt","wasCreated"]},"provisioned":{"type":"boolean"}},"required":["portal","provisioned"]},"example":{"portal":{"id":"string","name":"string","slug":"string","status":"string","createdAt":"string","wasCreated":true},"provisioned":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"402":{"description":"Plan-Gate: `free` ohne aktiven Trial. Meldung `customer_portal_requires_pro_plan`."},"404":{"description":"Der Mandant zur Sitzung steht nicht in `public.tenants`."},"503":{"description":"Keine Datenbankverbindung beim Lesen des Mandanten-Plans."}},"operationId":"postApiV1Customer-portalProvision","tags":["Customer-Portal"],"parameters":[],"summary":"Default-Portal fuer Tenant anlegen (idempotent)","description":"Lazy-Trigger fuer Frontend-EmptyState-CTAs. Legt ein Default-Portal an wenn keins existiert; gibt sonst das bestehende Portal zurueck. Plan-Gate: erfordert Pro-Plan oder aktiven Trial."}},"/api/v1/customer-portal/portals":{"get":{"responses":{"200":{"description":"Alle nicht geloeschten Portale samt Einstellungen","content":{"application/json":{"schema":{"type":"object","properties":{"portals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"customDomain":{"type":["string","null"]},"branding":{"type":"object","additionalProperties":{}},"accentColor":{"type":["string","null"]},"faviconUrl":{"type":["string","null"]},"metaTitle":{"type":["string","null"]},"metaDescription":{"type":["string","null"]},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"settings":{"type":"object","properties":{"allowForms":{"type":"boolean"},"allowAppointments":{"type":"boolean"},"allowDocuments":{"type":"boolean"},"allowMessages":{"type":"boolean"},"requireEmailVerification":{"type":"boolean"},"magicLinkExpiresMinutes":{"type":"number"},"sessionExpiresDays":{"type":"number"}},"required":["allowForms","allowAppointments","allowDocuments","allowMessages","requireEmailVerification","magicLinkExpiresMinutes","sessionExpiresDays"],"additionalProperties":false}},"required":["id","name","slug","customDomain","branding","accentColor","faviconUrl","metaTitle","metaDescription","status","createdBy","createdAt","updatedAt","settings"],"additionalProperties":false}}},"required":["portals"],"additionalProperties":false},"example":{"portals":[{"id":"string","name":"string","slug":"string","customDomain":"string","branding":{},"accentColor":"string","faviconUrl":"string","metaTitle":"string","metaDescription":"string","status":"string","createdBy":"string","createdAt":"string","updatedAt":"string","settings":{"allowForms":true,"allowAppointments":true,"allowDocuments":true,"allowMessages":true,"requireEmailVerification":true,"magicLinkExpiresMinutes":0,"sessionExpiresDays":0}}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"402":{"description":"Tarif schliesst Kundenportale nicht ein"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Customer-portalPortals","tags":["Customer-Portal"],"parameters":[],"summary":"Customer-Portals des Tenants listen","description":"Liefert alle Portale des Mandanten ausser den auf `deleted` gesetzten, neueste zuerst. Archivierte Portale stehen also MIT in der Liste; wer nur die benutzbaren will, filtert selbst auf `status`. Die Einstellungen sind je Portal eingebettet — fehlt die Einstellzeile, stehen dort Vorgabewerte statt null, aus der Antwort ist beides nicht zu unterscheiden. Kein Blaettern, kein Filter. Vor dem Lesen prueft die Route den Tarif: ein Mandant im Free-Tarif ohne laufende Testphase bekommt 402 `customer_portal_requires_pro_plan`."},"post":{"responses":{"201":{"description":"Das angelegte Portal samt seiner Einstellungen","content":{"application/json":{"schema":{"type":"object","properties":{"portal":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"customDomain":{"type":["string","null"]},"branding":{"type":"object","additionalProperties":{}},"accentColor":{"type":["string","null"]},"faviconUrl":{"type":["string","null"]},"metaTitle":{"type":["string","null"]},"metaDescription":{"type":["string","null"]},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"settings":{"type":"object","properties":{"allowForms":{"type":"boolean"},"allowAppointments":{"type":"boolean"},"allowDocuments":{"type":"boolean"},"allowMessages":{"type":"boolean"},"requireEmailVerification":{"type":"boolean"},"magicLinkExpiresMinutes":{"type":"number"},"sessionExpiresDays":{"type":"number"}},"required":["allowForms","allowAppointments","allowDocuments","allowMessages","requireEmailVerification","magicLinkExpiresMinutes","sessionExpiresDays"],"additionalProperties":false}},"required":["id","name","slug","customDomain","branding","accentColor","faviconUrl","metaTitle","metaDescription","status","createdBy","createdAt","updatedAt","settings"],"additionalProperties":false}},"required":["portal"],"additionalProperties":false},"example":{"portal":{"id":"string","name":"string","slug":"string","customDomain":"string","branding":{},"accentColor":"string","faviconUrl":"string","metaTitle":"string","metaDescription":"string","status":"string","createdBy":"string","createdAt":"string","updatedAt":"string","settings":{"allowForms":true,"allowAppointments":true,"allowDocuments":true,"allowMessages":true,"requireEmailVerification":true,"magicLinkExpiresMinutes":0,"sessionExpiresDays":0}}}}}},"400":{"description":"Validierungsfehler (etwa `invalid_slug`)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"402":{"description":"Tarif schliesst Kundenportale nicht ein"},"409":{"description":"`slug_already_in_use`"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1Customer-portalPortals","tags":["Customer-Portal"],"parameters":[],"summary":"Customer-Portal anlegen","description":"Legt Portal UND Einstellzeile in einem Aufruf an (201). Nicht gesetzte Einstellungen bekommen die Vorgaben: alles erlaubt, Bestaetigung verlangt, Magic-Link 30 Minuten, Sitzung 30 Tage. Der `slug` wird vorab auf Eindeutigkeit im Mandanten geprueft — ein belegter Wert ergibt 409 `slug_already_in_use`. Das mitgeschickte `branding` laeuft durch dieselbe Bereinigung wie beim Lesen: unbekannte Schluessel fallen weg, fehlende bekommen Vorgabewerte. Eine `custom_domain` wird hier ungeprueft uebernommen; verifiziert wird sie erst ueber /portals/{id}/custom-domain. Erfordert die Rolle Admin, und der Tarif muss Kundenportale einschliessen (sonst 402).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"custom_domain":{"type":"string","maxLength":255},"branding":{"type":"object","properties":{"logo_url":{"type":"string","format":"uri","maxLength":500},"logoUrl":{"type":"string","format":"uri","maxLength":500},"primary_color":{"type":"string","maxLength":9},"primaryColor":{"type":"string","maxLength":9},"secondary_color":{"type":"string","maxLength":9},"secondaryColor":{"type":"string","maxLength":9},"font_family":{"type":"string","maxLength":200},"fontFamily":{"type":"string","maxLength":200},"welcome_text":{"type":"string","maxLength":500},"welcomeText":{"type":"string","maxLength":500},"footer_text":{"type":"string","maxLength":500},"footerText":{"type":"string","maxLength":500},"custom_css":{"type":"string","maxLength":10000},"customCss":{"type":"string","maxLength":10000}},"additionalProperties":true},"settings":{"type":"object","properties":{"allow_forms":{"type":"boolean"},"allow_appointments":{"type":"boolean"},"allow_documents":{"type":"boolean"},"allow_messages":{"type":"boolean"},"require_email_verification":{"type":"boolean"},"magic_link_expires_minutes":{"type":"integer","minimum":5,"maximum":120},"session_expires_days":{"type":"integer","minimum":1,"maximum":90}}}},"required":["name","slug"]},"example":{"name":"string","slug":"00000000-0000-4000-8000-000000000000","custom_domain":"string","branding":{"logo_url":"https://example.com","logoUrl":"https://example.com","primary_color":"string","primaryColor":"string","secondary_color":"string","secondaryColor":"string","font_family":"string","fontFamily":"string","welcome_text":"string","welcomeText":"string","footer_text":"string","footerText":"string","custom_css":"string","customCss":"string"},"settings":{"allow_forms":true,"allow_appointments":true,"allow_documents":true,"allow_messages":true,"require_email_verification":true,"magic_link_expires_minutes":5,"session_expires_days":1}}}}}}},"/api/v1/customer-portal/portals/{id}":{"get":{"responses":{"200":{"description":"Portal mit aufgeloesten Einstellungen.","content":{"application/json":{"schema":{"type":"object","properties":{"portal":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"customDomain":{"type":["string","null"]},"branding":{"type":"object","additionalProperties":{}},"accentColor":{"type":["string","null"]},"faviconUrl":{"type":["string","null"]},"metaTitle":{"type":["string","null"]},"metaDescription":{"type":["string","null"]},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"settings":{"type":"object","properties":{"allowForms":{"type":"boolean"},"allowAppointments":{"type":"boolean"},"allowDocuments":{"type":"boolean"},"allowMessages":{"type":"boolean"},"requireEmailVerification":{"type":"boolean"},"magicLinkExpiresMinutes":{"type":"number"},"sessionExpiresDays":{"type":"number"}},"required":["allowForms","allowAppointments","allowDocuments","allowMessages","requireEmailVerification","magicLinkExpiresMinutes","sessionExpiresDays"],"additionalProperties":false}},"required":["id","name","slug","customDomain","branding","accentColor","faviconUrl","metaTitle","metaDescription","status","createdBy","createdAt","updatedAt","settings"],"additionalProperties":false}},"required":["portal"],"additionalProperties":false},"example":{"portal":{"id":"string","name":"string","slug":"string","customDomain":"string","branding":{},"accentColor":"string","faviconUrl":"string","metaTitle":"string","metaDescription":"string","status":"string","createdBy":"string","createdAt":"string","updatedAt":"string","settings":{"allowForms":true,"allowAppointments":true,"allowDocuments":true,"allowMessages":true,"requireEmailVerification":true,"magicLinkExpiresMinutes":0,"sessionExpiresDays":0}}}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Der Tarif des Mandanten enthaelt das Kundenportal nicht."},"404":{"description":"`portal_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"getApiV1Customer-portalPortalsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Portal in der Verwaltungssicht laden","description":"Liefert ein Portal samt seiner Einstellungen fuer die Verwaltungsmaske.\n\nDie Werte unter `settings` tragen VORGABEN, wenn der Mandant nie etwas\neingestellt hat. Ein `allowForms: true` heisst also nicht zwingend „wurde\nso gewaehlt\"; die Antwort trennt Vorgabe und Einstellung nicht.\n\nAuch archivierte Portale kommen hier zurueck, erkennbar an `status`."},"patch":{"responses":{"200":{"description":"Das aktualisierte Portal, gleiche Form wie `GET /portals/:id`.","content":{"application/json":{"schema":{"type":"object","properties":{"portal":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"customDomain":{"type":["string","null"]},"branding":{"type":"object","additionalProperties":{}},"accentColor":{"type":["string","null"]},"faviconUrl":{"type":["string","null"]},"metaTitle":{"type":["string","null"]},"metaDescription":{"type":["string","null"]},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"settings":{"type":"object","properties":{"allowForms":{"type":"boolean"},"allowAppointments":{"type":"boolean"},"allowDocuments":{"type":"boolean"},"allowMessages":{"type":"boolean"},"requireEmailVerification":{"type":"boolean"},"magicLinkExpiresMinutes":{"type":"number"},"sessionExpiresDays":{"type":"number"}},"required":["allowForms","allowAppointments","allowDocuments","allowMessages","requireEmailVerification","magicLinkExpiresMinutes","sessionExpiresDays"],"additionalProperties":false}},"required":["id","name","slug","customDomain","branding","accentColor","faviconUrl","metaTitle","metaDescription","status","createdBy","createdAt","updatedAt","settings"],"additionalProperties":false}},"required":["portal"],"additionalProperties":false},"example":{"portal":{"id":"string","name":"string","slug":"string","customDomain":"string","branding":{},"accentColor":"string","faviconUrl":"string","metaTitle":"string","metaDescription":"string","status":"string","createdBy":"string","createdAt":"string","updatedAt":"string","settings":{"allowForms":true,"allowAppointments":true,"allowDocuments":true,"allowMessages":true,"requireEmailVerification":true,"magicLinkExpiresMinutes":0,"sessionExpiresDays":0}}}}}},"400":{"description":"Rumpf verletzt das Schema (z. B. unbekannter `status`)."},"401":{"description":"`tenant_context_required` — keine Sitzung oder kein Mandanten-Kontext."},"402":{"description":"`customer_portal_requires_pro_plan` — Tarif `free` ohne laufende Testphase. Die Tarifsperre antwortet 402, NICHT 403."},"403":{"description":"Rolle unter `manager`."},"404":{"description":"`portal_not_found` oder `tenant_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"patchApiV1Customer-portalPortalsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Portal-Stammdaten, Branding und Einstellungen aendern","description":"Teil-Update aus der Verwaltungssicht des Mandanten. Geschrieben wird nur,\nwas im Rumpf steht: `name`, `custom_domain`, `status`, `branding` und die\nsieben Werte unter `settings`. Die Einstellzeile wird angelegt, falls es\nnoch keine gibt.\n\nACHTUNG bei `branding`: der Block wird NICHT mit dem Bestand\nzusammengefuehrt, sondern ersetzt ihn. Felder, die im Rumpf fehlen,\nfallen auf die Vorgabewerte zurueck — ein Aufruf mit nur\n`{ \"primary_color\": \"#ff0000\" }` setzt Logo, Schrift, Begruessungs- und\nFusstext sowie eigenes CSS also auf die Vorgabe zurueck. Wer einzelne\nBranding-Felder aendern will, nimmt `PUT /portals/:id/branding`; nur die\nRoute fuehrt zusammen.\n\n`status: \"archived\"` bewirkt dasselbe wie `DELETE /portals/:id`: die\noeffentlichen Kundenrouten filtern auf `active` und antworten dann 404,\ndie Daten bleiben liegen.\n\nErfordert eine angemeldete Mitarbeiter-Sitzung plus Mandanten-Kontext und\nmindestens die Rolle `manager`. Dies ist die Mandanten-, nicht die\nKundenseite des Portals.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"custom_domain":{"type":["string","null"],"maxLength":255},"branding":{"type":"object","properties":{"logo_url":{"type":"string","format":"uri","maxLength":500},"logoUrl":{"type":"string","format":"uri","maxLength":500},"primary_color":{"type":"string","maxLength":9},"primaryColor":{"type":"string","maxLength":9},"secondary_color":{"type":"string","maxLength":9},"secondaryColor":{"type":"string","maxLength":9},"font_family":{"type":"string","maxLength":200},"fontFamily":{"type":"string","maxLength":200},"welcome_text":{"type":"string","maxLength":500},"welcomeText":{"type":"string","maxLength":500},"footer_text":{"type":"string","maxLength":500},"footerText":{"type":"string","maxLength":500},"custom_css":{"type":"string","maxLength":10000},"customCss":{"type":"string","maxLength":10000}},"additionalProperties":true},"settings":{"type":"object","properties":{"allow_forms":{"type":"boolean"},"allow_appointments":{"type":"boolean"},"allow_documents":{"type":"boolean"},"allow_messages":{"type":"boolean"},"require_email_verification":{"type":"boolean"},"magic_link_expires_minutes":{"type":"integer","minimum":5,"maximum":120},"session_expires_days":{"type":"integer","minimum":1,"maximum":90}}},"status":{"type":"string","enum":["active","archived"]}}},"example":{"name":"string","custom_domain":"string","branding":{"logo_url":"https://example.com","logoUrl":"https://example.com","primary_color":"string","primaryColor":"string","secondary_color":"string","secondaryColor":"string","font_family":"string","fontFamily":"string","welcome_text":"string","welcomeText":"string","footer_text":"string","footerText":"string","custom_css":"string","customCss":"string"},"settings":{"allow_forms":true,"allow_appointments":true,"allow_documents":true,"allow_messages":true,"require_email_verification":true,"magic_link_expires_minutes":5,"session_expires_days":1},"status":"active"}}}}},"delete":{"responses":{"200":{"description":"Portal archiviert. Wiederholbar.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"archived":{"type":"boolean","const":true}},"required":["ok","archived"],"additionalProperties":false},"example":{"ok":true,"archived":true}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Rolle unter `admin`, oder der Tarif enthaelt das Kundenportal nicht."},"404":{"description":"`portal_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"deleteApiV1Customer-portalPortalsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Portal archivieren","description":"Archiviert das Portal. Es wird NICHT geloescht, sondern auf\n`status = archived` gesetzt. Mitglieder, Formulare, Dokumente und\nEinreichungen bleiben unangetastet in der Datenbank.\n\nWas das fuer die Kundenseite bedeutet: die oeffentlichen Routen filtern\nauf `status = active`, ein archiviertes Portal antwortet dort also mit\n404. Die Anmeldung ist damit zu, die Daten sind es nicht.\n\nDer Aufruf ist wiederholbar und antwortet erneut mit 200.\nErfordert Rolle `admin`."}},"/api/v1/customer-portal/portals/{id}/branding":{"put":{"responses":{"200":{"description":"Das Portal nach dem Schreiben, frisch gelesen","content":{"application/json":{"schema":{"type":"object","properties":{"portal":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"customDomain":{"type":["string","null"]},"branding":{"type":"object","additionalProperties":{}},"accentColor":{"type":["string","null"]},"faviconUrl":{"type":["string","null"]},"metaTitle":{"type":["string","null"]},"metaDescription":{"type":["string","null"]},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"settings":{"type":"object","properties":{"allowForms":{"type":"boolean"},"allowAppointments":{"type":"boolean"},"allowDocuments":{"type":"boolean"},"allowMessages":{"type":"boolean"},"requireEmailVerification":{"type":"boolean"},"magicLinkExpiresMinutes":{"type":"number"},"sessionExpiresDays":{"type":"number"}},"required":["allowForms","allowAppointments","allowDocuments","allowMessages","requireEmailVerification","magicLinkExpiresMinutes","sessionExpiresDays"],"additionalProperties":false}},"required":["id","name","slug","customDomain","branding","accentColor","faviconUrl","metaTitle","metaDescription","status","createdBy","createdAt","updatedAt","settings"],"additionalProperties":false}},"required":["portal"],"additionalProperties":false},"example":{"portal":{"id":"string","name":"string","slug":"string","customDomain":"string","branding":{},"accentColor":"string","faviconUrl":"string","metaTitle":"string","metaDescription":"string","status":"string","createdBy":"string","createdAt":"string","updatedAt":"string","settings":{"allowForms":true,"allowAppointments":true,"allowDocuments":true,"allowMessages":true,"requireEmailVerification":true,"magicLinkExpiresMinutes":0,"sessionExpiresDays":0}}}}}},"400":{"description":"Validierungsfehler (etwa eine Farbe, die kein Hex-Wert ist)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"402":{"description":"Tarif schliesst Kundenportale nicht ein"},"404":{"description":"`portal_not_found`"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"putApiV1Customer-portalPortalsByIdBranding","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Branding eines Portals aktualisieren (Editor-Endpoint)","description":"Fuehrt die gesendeten Branding-Felder mit dem Bestand ZUSAMMEN — anders als PATCH /portals/{id}, das den ganzen Block ersetzt. Nicht gesendete Felder behalten also ihren Wert. Die vier eigenen Spalten (`accentColor`, `faviconUrl`, `metaTitle`, `metaDescription`) und `customDomain` werden nur geschrieben, wenn sie im Rumpf stehen; alles Uebrige landet zusammengefuehrt und bereinigt im JSONB. Farben muessen als sechsstelliger Hex-Wert kommen. Eine hier gesetzte `customDomain` wird NICHT geprueft — die TXT-Verifizierung laeuft ueber /portals/{id}/custom-domain. Der Vorgang wird ins Portal-Protokoll geschrieben; scheitert das, faellt nur ein Protokolleintrag aus. Unbekanntes Portal → 404, erforderlich ist mindestens die Rolle Manager.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"logoUrl":{"type":"string","format":"uri","maxLength":500},"primaryColor":{"type":"string","pattern":"^#[0-9a-f]{6}$"},"secondaryColor":{"type":"string","pattern":"^#[0-9a-f]{6}$"},"accentColor":{"type":"string","pattern":"^#[0-9a-f]{6}$"},"customCss":{"type":"string","maxLength":50000},"customDomain":{"type":"string","pattern":"^[a-z0-9.-]+\\.[a-z]{2,}$","maxLength":255},"faviconUrl":{"type":"string","format":"uri","maxLength":500},"metaTitle":{"type":"string","maxLength":200},"metaDescription":{"type":"string","maxLength":500},"welcomeText":{"type":"string","maxLength":500},"footerText":{"type":"string","maxLength":500},"fontFamily":{"type":"string","maxLength":200}},"additionalProperties":false},"example":{"logoUrl":"https://example.com","customCss":"string","faviconUrl":"https://example.com","metaTitle":"string","metaDescription":"string","welcomeText":"string","footerText":"string","fontFamily":"string"}}}}}},"/api/v1/customer-portal/portals/{id}/members":{"get":{"responses":{"200":{"description":"Mitglieder des Portals, hoechstens 500, neueste zuerst.","content":{"application/json":{"schema":{"type":"object","properties":{"portalId":{"type":"string"},"members":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"contactId":{"type":["string","null"]},"email":{"type":"string"},"displayName":{"type":["string","null"]},"lastLoginAt":{"type":["string","null"]},"sessionActive":{"type":"boolean"},"status":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","contactId","email","displayName","lastLoginAt","sessionActive","status","createdAt"],"additionalProperties":false}}},"required":["portalId","members"],"additionalProperties":false},"example":{"portalId":"string","members":[{"id":"string","contactId":"string","email":"string","displayName":"string","lastLoginAt":"string","sessionActive":true,"status":"string","createdAt":"string"}]}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"403":{"description":"Der Tarif des Mandanten enthaelt das Kundenportal nicht."},"404":{"description":"`portal_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"getApiV1Customer-portalPortalsByIdMembers","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mitglieder eines Portals auflisten","description":"Listet die Portal-Mitglieder samt Anmeldezustand.\n\n`sessionActive` ist KEIN gespeichertes Feld, sondern zur Abrufzeit\nberechnet: es vergleicht `session_expires_at` mit dem Jetzt. Der Wert\naltert also im Bildschirm, ohne dass sich in der Datenbank etwas aendert.\n\nEs gibt KEINE Paginierung. Die Liste schneidet bei 500 Mitgliedern ab,\nneueste zuerst; wer mehr hat, sieht die aeltesten nicht.\n\nDie Adressen der Mitglieder gehen hier vollstaendig heraus. Das ist die\nVerwaltungssicht des Mandanten auf seine eigenen Kunden, nicht die\nKundensicht."}},"/api/v1/customer-portal/portals/{id}/members/resend-magic-link":{"post":{"responses":{"200":{"description":"Link ausgestellt und Mitglied angelegt bzw. aktualisiert. Enthaelt die vollstaendige Login-URL. Sagt nichts ueber den Mailversand aus.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"magicLinkUrl":{"type":"string"}},"required":["ok","magicLinkUrl"],"additionalProperties":false},"example":{"ok":true,"magicLinkUrl":"string"}}}},"400":{"description":"`email` fehlt oder ist keine gueltige Adresse."},"401":{"description":"`tenant_context_required` — keine Sitzung oder kein Mandanten-Kontext."},"402":{"description":"`customer_portal_requires_pro_plan` — Tarif `free` ohne laufende Testphase. Die Tarifsperre antwortet 402, NICHT 403."},"403":{"description":"Rolle unter `manager`."},"404":{"description":"`portal_not_found` oder `tenant_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"postApiV1Customer-portalPortalsByIdMembersResend-magic-link","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Magic-Link fuer einen Portal-Zugang neu ausstellen","description":"Stellt einen frischen Magic-Link fuer eine E-Mail-Adresse aus und schickt\nihn per Mail an diese Adresse. Gespeichert wird nur der Hash des Tokens.\nDie Gueltigkeit kommt aus `magicLinkExpiresMinutes` des Portals (Vorgabe\n30 Minuten).\n\n„Neu ausstellen\" ist woertlich zu nehmen: die Adresse muss KEIN\nbestehendes Mitglied sein. Kennt das Portal sie nicht, legt der Aufruf das\nMitglied an. Der Aufruf verschafft also jeder beliebigen Adresse Zugang\nzum Portal des Mandanten.\n\nDie fertige Login-URL steht im Klartext im Antwortkoerper\n(`magicLinkUrl`) — mitsamt gueltigem Token. Wer die Antwort sieht, kann\nsich im Portal als dieser Kunde anmelden, ohne die Mail zu haben. Deshalb\nist die Route auf Rolle `manager` begrenzt.\n\n`ok: true` heisst NICHT, dass die Mail draussen ist: scheitert der Versand,\nwird er nur protokolliert, und die Antwort bleibt 200. Der Link im Rumpf\nist dann trotzdem gueltig.\n\nDie Route selbst gehoert zur Mandantenseite: sie verlangt eine angemeldete\nMitarbeiter-Sitzung (Cookie oder API-Key) plus Mandanten-Kontext. Der\nausgestellte Link fuehrt dagegen auf die oeffentliche Kundenseite unter\n`/p/:slug/verify`, die ohne Nemix-Anmeldung allein mit dem Token arbeitet.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":254}},"required":["email"]},"example":{"email":"beispiel@example.com"}}}}}},"/api/v1/customer-portal/portals/{id}/analytics":{"get":{"responses":{"200":{"description":"Aggregierte Kennzahlen des Portals","content":{"application/json":{"schema":{"type":"object","properties":{"portalId":{"type":"string","description":"Kennung des ausgewerteten Portals"},"period":{"type":"string","enum":["7d","30d","90d"],"description":"Ausgewerteter Zeitraum"},"generatedAt":{"type":"string","format":"date-time","description":"Zeitpunkt der Berechnung — bei einem Treffer aus dem Zwischenspeicher bis zu fuenf Minuten alt"},"counters":{"type":"object","properties":{"submissionsToday":{"type":"integer","minimum":0,"description":"Einsendungen seit Tagesbeginn"},"submissionsThisWeek":{"type":"integer","minimum":0,"description":"Einsendungen seit Wochenbeginn"},"appointmentsUpcoming":{"type":"integer","minimum":0,"description":"Kuenftige Termine ohne cancelled und no_show"},"membersTotal":{"type":"integer","minimum":0,"description":"Mitglieder im Status active"},"membersActive30d":{"type":"integer","minimum":0,"description":"Davon mit Anmeldung in den letzten 30 Tagen"},"unreadMessagesTotal":{"type":"integer","minimum":0,"description":"Eingehende Nachrichten ohne read_at"}},"required":["submissionsToday","submissionsThisWeek","appointmentsUpcoming","membersTotal","membersActive30d","unreadMessagesTotal"],"description":"Kennzahlen zum Stichtag, unabhaengig vom gewaehlten Zeitraum"},"submissionsByDay":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","description":"Tag (YYYY-MM-DD)"},"count":{"type":"integer","minimum":0}},"required":["date","count"]},"description":"Ein Eintrag je Tag des Zeitraums — Tage ohne Einsendung stehen mit 0 drin"},"appointmentsByDay":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","description":"Tag (YYYY-MM-DD)"},"count":{"type":"integer","minimum":0}},"required":["date","count"]},"description":"Ein Eintrag je Tag des Zeitraums, ohne cancelled und no_show"},"appointmentsByWeek":{"type":"array","items":{"type":"object","properties":{"weekStart":{"type":"string","description":"Erster Tag der Woche (YYYY-MM-DD)"},"count":{"type":"integer","minimum":0}},"required":["weekStart","count"]},"description":"Nur Wochen mit Terminen — anders als die Tagesreihen ohne Luecken-Auffuellung"},"topForms":{"type":"array","items":{"type":"object","properties":{"formId":{"type":"string","description":"Kennung des Formulars"},"title":{"type":"string","description":"Formulartitel; \"(unbenannt)\" wenn keiner hinterlegt ist"},"count":{"type":"integer","minimum":0,"description":"Einsendungen im Zeitraum"}},"required":["formId","title","count"]},"description":"Die drei meistgenutzten Formulare des Zeitraums"}},"required":["portalId","period","generatedAt","counters","submissionsByDay","appointmentsByDay","appointmentsByWeek","topForms"]},"example":{"portalId":"string","period":"7d","generatedAt":"2026-01-01T12:00:00.000Z","counters":{"submissionsToday":0,"submissionsThisWeek":0,"appointmentsUpcoming":0,"membersTotal":0,"membersActive30d":0,"unreadMessagesTotal":0},"submissionsByDay":[{"date":"string","count":0}],"appointmentsByDay":[{"date":"string","count":0}],"appointmentsByWeek":[{"weekStart":"string","count":0}],"topForms":[{"formId":"string","title":"string","count":0}]}}}},"400":{"description":"Ungültige Portal-Kennung oder unbekannter Zeitraum"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Portal nicht gefunden"},"503":{"description":"Keine Datenbankverbindung"}},"operationId":"getApiV1Customer-portalPortalsByIdAnalytics","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Aggregierte Analytics für ein Customer-Portal (5min Cache)","description":"Zaehlt Einsendungen, Termine, Mitglieder und ungelesene Nachrichten des Portals zusammen. `period` (7d, 30d, 90d — Voreinstellung 30d) steuert nur die Reihen und die Top-Formulare; die Zaehler unter `counters` beziehen sich immer auf heute, diese Woche und alle kuenftigen Termine.  Termine im Status `cancelled` oder `no_show` bleiben ueberall aussen vor. Die Tagesreihen sind luckenlos: Tage ohne Vorgang stehen mit 0 drin.  Fehlende Tabellen legt die Route beim Aufruf selbst an. Das Ergebnis liegt fuenf Minuten im Zwischenspeicher — `generatedAt` kann also aelter sein als der Aufruf. Ein Portal, das dem Mandanten nicht gehoert, antwortet 404."}},"/api/v1/customer-portal/portals/{portalId}/custom-fields":{"get":{"responses":{"200":{"description":"Die eigenen Felder dieses Portals, in Anzeigereihenfolge.","content":{"application/json":{"schema":{"type":"object","properties":{"fields":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"fieldKey":{"type":"string","description":"Maschinenschluessel. Unveraenderlich — kein PATCH fasst ihn an."},"fieldLabel":{"type":"string"},"fieldType":{"type":"string","enum":["text","number","email","phone","date","select","multiselect","textarea","boolean","file"]},"required":{"type":"boolean"},"defaultValue":{"type":["string","null"]},"options":{"type":["array","null"],"items":{"type":"object","properties":{"value":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":200}},"required":["value","label"]},"description":"Nur bei `select`/`multiselect` gefuellt, sonst null."},"placeholder":{"type":["string","null"]},"helpText":{"type":["string","null"]},"displayOrder":{"type":"integer"},"visibleOnDashboard":{"type":"boolean"},"editableByCustomer":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","portalId","fieldKey","fieldLabel","fieldType","required","defaultValue","options","placeholder","helpText","displayOrder","visibleOnDashboard","editableByCustomer","createdAt","updatedAt"]}}},"required":["fields"]},"example":{"fields":[{"id":"string","portalId":"string","fieldKey":"string","fieldLabel":"string","fieldType":"text","required":true,"defaultValue":"string","options":[{"value":"string","label":"string"}],"placeholder":"string","helpText":"string","displayOrder":0,"visibleOnDashboard":true,"editableByCustomer":true,"createdAt":"string","updatedAt":"string"}]}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`portal_not_found` — unbekannt oder geloescht."},"503":{"description":"Datenbank nicht erreichbar."}},"operationId":"getApiV1Customer-portalPortalsByPortalIdCustom-fields","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true}],"summary":"Custom-Felder eines Portals listen","description":"Liefert die eigenen Eingabefelder eines Portals, sortiert nach `displayOrder` und bei Gleichstand nach Anlagedatum. Entfernte Felder bleiben aussen vor. Gibt es das Portal nicht oder ist es geloescht, kommt 404 — eine leere Liste heisst also wirklich, dass keine Felder angelegt sind. `options` ist nur bei den Feldarten `select` und `multiselect` gefuellt, sonst null. Keine Rollenpruefung: jeder angemeldete Benutzer des Mandanten. Keine Tarifsperre — der 402 liegt auf den Portal-Routen selbst."},"post":{"responses":{"201":{"description":"Feld angelegt.","content":{"application/json":{"schema":{"type":"object","properties":{"field":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"fieldKey":{"type":"string","description":"Maschinenschluessel. Unveraenderlich — kein PATCH fasst ihn an."},"fieldLabel":{"type":"string"},"fieldType":{"type":"string","enum":["text","number","email","phone","date","select","multiselect","textarea","boolean","file"]},"required":{"type":"boolean"},"defaultValue":{"type":["string","null"]},"options":{"type":["array","null"],"items":{"type":"object","properties":{"value":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":200}},"required":["value","label"]},"description":"Nur bei `select`/`multiselect` gefuellt, sonst null."},"placeholder":{"type":["string","null"]},"helpText":{"type":["string","null"]},"displayOrder":{"type":"integer"},"visibleOnDashboard":{"type":"boolean"},"editableByCustomer":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","portalId","fieldKey","fieldLabel","fieldType","required","defaultValue","options","placeholder","helpText","displayOrder","visibleOnDashboard","editableByCustomer","createdAt","updatedAt"]}},"required":["field"]},"example":{"field":{"id":"string","portalId":"string","fieldKey":"string","fieldLabel":"string","fieldType":"text","required":true,"defaultValue":"string","options":[{"value":"string","label":"string"}],"placeholder":"string","helpText":"string","displayOrder":0,"visibleOnDashboard":true,"editableByCustomer":true,"createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"`select_field_requires_options`, oder der Rumpf haelt das Schema nicht ein (ungueltiger `field_key`, unbekannte Feldart)."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`portal_not_found` — unbekannt oder geloescht."},"409":{"description":"`field_key_already_in_use` — im selben Portal traegt ein noch vorhandenes Feld diesen Schluessel."},"503":{"description":"`database_unavailable`."}},"operationId":"postApiV1Customer-portalPortalsByPortalIdCustom-fields","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true}],"summary":"Eigenes Feld im Portal anlegen","description":"Legt ein zusaetzliches Eingabefeld an, das den Mitgliedern dieses\nPortals angezeigt wird. `field_key` ist der Maschinenschluessel, unter\ndem die Werte spaeter abgelegt werden; er muss innerhalb des Portals\neindeutig sein und ist danach nicht mehr aenderbar.\n\nOhne `display_order` haengt das Feld ans Ende: die Route liest den\nhoechsten vergebenen Wert der noch vorhandenen Felder und zaehlt eins\nhoch. Entfernte Felder zaehlen dabei nicht mit, ihre Nummern werden\nalso wiederverwendet.\n\nFeldarten `select` und `multiselect` verlangen mindestens einen Eintrag\nin `options` — sonst 400. Alle anderen Arten ignorieren `options`.\n\nEs wird nichts geschrieben, wenn das Portal nicht existiert oder\ngeloescht ist (404), und nichts ueberschrieben, wenn der Schluessel\nschon vergeben ist (409). Der Schluesselvergleich beruecksichtigt nur\nnicht entfernte Felder: ein per DELETE entferntes Feld gibt seinen\nSchluessel wieder frei.\n\nKeine Tarifsperre. Der 402 aus `/customer-portal/portals` liegt auf den\nPortal-Routen selbst, nicht auf den Feldern eines bestehenden Portals.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"field_key":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,62}$"},"field_label":{"type":"string","minLength":1,"maxLength":200},"field_type":{"type":"string","enum":["text","number","email","phone","date","select","multiselect","textarea","boolean","file"]},"required":{"type":"boolean","default":false},"default_value":{"type":["string","null"],"maxLength":1000},"options":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":200}},"required":["value","label"]},"maxItems":100},"placeholder":{"type":["string","null"],"maxLength":200},"help_text":{"type":["string","null"],"maxLength":500},"display_order":{"type":"integer","minimum":0,"maximum":9999},"visible_on_dashboard":{"type":"boolean","default":true},"editable_by_customer":{"type":"boolean","default":true}},"required":["field_key","field_label","field_type"]}}}}}},"/api/v1/customer-portal/portals/{portalId}/custom-fields/reorder":{"put":{"responses":{"200":{"description":"Alle Felder aus `items` geschrieben. `updated` ist deren Anzahl.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"updated":{"type":"integer","description":"Zahl der uebergebenen Felder, nicht der tatsaechlich geaenderten."}},"required":["ok","updated"],"additionalProperties":false},"example":{"ok":true,"updated":0}}}},"400":{"description":"Der Rumpf haelt das Schema nicht ein (`items` leer oder fehlerhaft)."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`portal_not_found`, oder `field_not_found:<id>` — dann sind die Felder davor bereits geschrieben."},"503":{"description":"`database_unavailable`."}},"operationId":"putApiV1Customer-portalPortalsByPortalIdCustom-fieldsReorder","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true}],"summary":"Reihenfolge der eigenen Portal-Felder setzen","description":"Setzt `display_order` fuer die uebergebenen Felder. Es zaehlt nur, was\nin `items` steht: nicht genannte Felder behalten ihre bisherige Nummer,\nund die Route prueft nicht, ob die Nummern danach eindeutig sind. Wer\nzwei Feldern dieselbe Nummer gibt, bekommt sie auch so gespeichert; die\nListe sortiert dann nach Anlagezeitpunkt weiter.\n\nNICHT ATOMAR. Die Felder werden nacheinander einzeln geschrieben. Faellt\neines aus der Reihe — unbekannt, schon entfernt, oder zu einem anderen\nPortal gehoerend —, bricht die Route mit 404 ab, aber die davor bereits\ngeschriebenen Nummern BLEIBEN STEHEN. Eine 404-Antwort bedeutet hier\nalso einen halb ausgefuehrten Aufruf, nicht einen wirkungslosen.\n\nDie Fehlermeldung nennt die Kennung, an der es gescheitert ist\n(`field_not_found:<id>`); alles davor in `items` ist geschrieben.\n\nKeine Tarifsperre. Der 402 aus `/customer-portal/portals` liegt auf den\nPortal-Routen selbst, nicht auf den Feldern eines bestehenden Portals.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100},"display_order":{"type":"integer","minimum":0,"maximum":9999}},"required":["id","display_order"]},"minItems":1,"maxItems":500}},"required":["items"]},"example":{"items":[{"id":"string","display_order":0}]}}}}}},"/api/v1/customer-portal/portals/{portalId}/custom-fields/{fieldId}":{"patch":{"responses":{"200":{"description":"Das Feld nach der Aenderung. Auch bei leerem Rumpf (dann unveraendert).","content":{"application/json":{"schema":{"type":"object","properties":{"field":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"fieldKey":{"type":"string","description":"Maschinenschluessel. Unveraenderlich — kein PATCH fasst ihn an."},"fieldLabel":{"type":"string"},"fieldType":{"type":"string","enum":["text","number","email","phone","date","select","multiselect","textarea","boolean","file"]},"required":{"type":"boolean"},"defaultValue":{"type":["string","null"]},"options":{"type":["array","null"],"items":{"type":"object","properties":{"value":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":200}},"required":["value","label"]},"description":"Nur bei `select`/`multiselect` gefuellt, sonst null."},"placeholder":{"type":["string","null"]},"helpText":{"type":["string","null"]},"displayOrder":{"type":"integer"},"visibleOnDashboard":{"type":"boolean"},"editableByCustomer":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","portalId","fieldKey","fieldLabel","fieldType","required","defaultValue","options","placeholder","helpText","displayOrder","visibleOnDashboard","editableByCustomer","createdAt","updatedAt"]}},"required":["field"]},"example":{"field":{"id":"string","portalId":"string","fieldKey":"string","fieldLabel":"string","fieldType":"text","required":true,"defaultValue":"string","options":[{"value":"string","label":"string"}],"placeholder":"string","helpText":"string","displayOrder":0,"visibleOnDashboard":true,"editableByCustomer":true,"createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"`select_field_requires_options`, oder der Rumpf haelt das Schema nicht ein."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`field_not_found` — unbekannt, schon entfernt, oder fremdes Portal."},"503":{"description":"`database_unavailable`."}},"operationId":"patchApiV1Customer-portalPortalsByPortalIdCustom-fieldsByFieldId","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true},{"schema":{"type":"string"},"in":"path","name":"fieldId","required":true}],"summary":"Eigenes Portal-Feld aendern","description":"Aendert Beschriftung, Feldart, Pflichtkennzeichen, Vorbelegung,\nAuswahlwerte, Platzhalter, Hilfetext, Position und die beiden\nSichtbarkeits-Schalter. NICHT aenderbar ist `field_key` — er steht\nnicht im Schema, bereits erfasste Werte haengen an ihm.\n\nTeil-Aenderung: nur die uebergebenen Felder werden geschrieben, alles\nandere bleibt. Ein leerer Rumpf `{}` ist kein Fehler — die Route\nantwortet 200 mit dem unveraenderten Feld, ohne die Datenbank\nanzufassen.\n\nWechselt die Feldart auf `select`/`multiselect`, muessen Auswahlwerte\nvorhanden sein: entweder im selben Aufruf oder bereits gespeichert.\nSonst 400. `options: null` loescht die Auswahlwerte und ist damit bei\ndiesen beiden Arten ebenfalls ein 400.\n\nDas Portal wird NICHT gesondert geprueft. Die Abfrage bindet `portal_id`\naus dem Pfad mit ein, deshalb deckt der eine 404 drei Faelle ab: Feld\nunbekannt, Feld schon entfernt, oder Feld gehoert zu einem anderen\nPortal. Eine unbekannte Portal-Kennung ergibt hier also\n`field_not_found` und nicht `portal_not_found` wie bei GET und POST.\n\nEntfernte Felder (`deleted_at`) sind nicht erreichbar; ueber diese Route\nlaesst sich ein Feld nicht zurueckholen.\n\nKeine Tarifsperre. Der 402 aus `/customer-portal/portals` liegt auf den\nPortal-Routen selbst, nicht auf den Feldern eines bestehenden Portals.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"field_label":{"type":"string","minLength":1,"maxLength":200},"field_type":{"type":"string","enum":["text","number","email","phone","date","select","multiselect","textarea","boolean","file"]},"required":{"type":"boolean"},"default_value":{"type":["string","null"],"maxLength":1000},"options":{"type":["array","null"],"items":{"type":"object","properties":{"value":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":200}},"required":["value","label"]},"maxItems":100},"placeholder":{"type":["string","null"],"maxLength":200},"help_text":{"type":["string","null"],"maxLength":500},"display_order":{"type":"integer","minimum":0,"maximum":9999},"visible_on_dashboard":{"type":"boolean"},"editable_by_customer":{"type":"boolean"}}},"example":{"field_label":"string","field_type":"text","required":true,"default_value":"string","options":[{"value":"string","label":"string"}],"placeholder":"string","help_text":"string","display_order":0,"visible_on_dashboard":true,"editable_by_customer":true}}}}},"delete":{"responses":{"200":{"description":"Feld entfernt. Erfasste Werte bleiben bestehen.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deleted":{"type":"boolean","const":true}},"required":["ok","deleted"],"additionalProperties":false},"example":{"ok":true,"deleted":true}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`field_not_found` — unbekannt, schon entfernt, oder fremdes Portal."},"503":{"description":"`database_unavailable`."}},"operationId":"deleteApiV1Customer-portalPortalsByPortalIdCustom-fieldsByFieldId","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true},{"schema":{"type":"string"},"in":"path","name":"fieldId","required":true}],"summary":"Eigenes Portal-Feld entfernen","description":"Entfernt das Feld aus dem Portal. Es wird NICHT geloescht, sondern mit\n`deleted_at` gestempelt. Bereits erfasste Werte der Mitglieder bleiben in\nder Wertetabelle stehen und werden von dieser Route nicht angefasst.\n\nNICHT WIEDERHOLBAR: die Bedingung lautet `deleted_at IS NULL`, ein\nzweiter Aufruf endet im 404.\n\nDer 404 deckt drei Faelle ab: Feld unbekannt, Feld schon entfernt, ODER\ndas Feld gehoert einem anderen Portal — die Abfrage bindet `portal_id`\naus dem Pfad mit ein."}},"/api/v1/customer-portal/forms":{"get":{"responses":{"200":{"description":"Die Formulare dieses Portals","content":{"application/json":{"schema":{"type":"object","properties":{"forms":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"fields":{"type":"array","items":{"type":"object","additionalProperties":{}}},"autoCreateTicket":{"type":"boolean"},"autoRouteViaAi":{"type":"boolean"},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"validationRules":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["id","portalId","title","description","fields","autoCreateTicket","autoRouteViaAi","status","createdBy","createdAt","updatedAt","validationRules"],"additionalProperties":false}}},"required":["forms"],"additionalProperties":false},"example":{"forms":[{"id":"string","portalId":"string","title":"string","description":"string","fields":[{}],"autoCreateTicket":true,"autoRouteViaAi":true,"status":"string","createdBy":"string","createdAt":"string","updatedAt":"string","validationRules":[{}]}]}}}},"400":{"description":"Ungueltige Query-Parameter (`portal_id` fehlt)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Portal nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Customer-portalForms","tags":["Customer-Portal"],"parameters":[{"in":"query","name":"portal_id","schema":{"type":"string","minLength":1,"maxLength":100},"required":true},{"in":"query","name":"include_deleted","schema":{"type":"string","enum":["true","false","1","0","yes","no","on","off"]},"required":false}],"summary":"Forms eines Portals listen","description":"Liefert die Formulare EINES Portals; `portal_id` ist Pflicht, und das Portal muss es geben (sonst 404). Auf `deleted` gesetzte Formulare bleiben aussen vor, es sei denn `include_deleted=true` — physisch entfernt wird hier nie eines, damit alte Einreichungen ihre Formular-Referenz behalten. Sortiert nach Anlagedatum, neueste zuerst; ohne Blaetterung."},"post":{"responses":{"201":{"description":"Das angelegte Formular","content":{"application/json":{"schema":{"type":"object","properties":{"form":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"fields":{"type":"array","items":{"type":"object","additionalProperties":{}}},"autoCreateTicket":{"type":"boolean"},"autoRouteViaAi":{"type":"boolean"},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"validationRules":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["id","portalId","title","description","fields","autoCreateTicket","autoRouteViaAi","status","createdBy","createdAt","updatedAt","validationRules"],"additionalProperties":false}},"required":["form"],"additionalProperties":false},"example":{"form":{"id":"string","portalId":"string","title":"string","description":"string","fields":[{}],"autoCreateTicket":true,"autoRouteViaAi":true,"status":"string","createdBy":"string","createdAt":"string","updatedAt":"string","validationRules":[{}]}}}}},"400":{"description":"Validierungsfehler oder unbrauchbare Feldliste"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Portal nicht gefunden"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1Customer-portalForms","tags":["Customer-Portal"],"parameters":[],"summary":"Form anlegen","description":"Legt ein Formular in einem bestehenden Portal an (201); ein unbekanntes Portal ergibt 404. Vor dem Schreiben prueft die Route die Feldliste: doppelte Feldnamen, ein `select` ohne Auswahlmoeglichkeiten und ein unbrauchbarer Pruefausdruck werden mit 400 abgewiesen und es wird NICHTS gespeichert. Verlangt zwischen 1 und 50 Felder. `fields` und `validation_rules` landen als JSONB; beide Automatik-Schalter stehen ohne Angabe auf false. Der anlegende Benutzer wird als `createdBy` vermerkt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"portal_id":{"type":"string","minLength":1,"maxLength":100},"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000},"fields":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,62}$","minLength":1,"maxLength":63},"type":{"type":"string","enum":["text","email","select","checkbox","file","date","textarea"]},"label":{"type":"string","minLength":1,"maxLength":200},"required":{"type":"boolean","default":false},"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":200}},"validation":{"type":"string","maxLength":500},"placeholder":{"type":"string","maxLength":200}},"required":["name","type","label"]},"minItems":1,"maxItems":50},"auto_create_ticket":{"type":"boolean","default":false},"auto_route_via_ai":{"type":"boolean","default":false},"validation_rules":{"type":"array","items":{"type":"object","properties":{"fieldName":{"type":"string","minLength":1,"maxLength":63},"type":{"type":"string","enum":["email","phone","minLength","regex"]},"value":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"}]}},"required":["fieldName","type"]},"maxItems":100}},"required":["portal_id","title","fields"]}}}}}},"/api/v1/customer-portal/forms/{id}":{"get":{"responses":{"200":{"description":"Formular in der Verwaltungssicht, unabhaengig vom Status.","content":{"application/json":{"schema":{"type":"object","properties":{"form":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"fields":{"type":"array","items":{"type":"object","additionalProperties":{}}},"autoCreateTicket":{"type":"boolean"},"autoRouteViaAi":{"type":"boolean"},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"validationRules":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["id","portalId","title","description","fields","autoCreateTicket","autoRouteViaAi","status","createdBy","createdAt","updatedAt","validationRules"],"additionalProperties":false}},"required":["form"],"additionalProperties":false},"example":{"form":{"id":"string","portalId":"string","title":"string","description":"string","fields":[{}],"autoCreateTicket":true,"autoRouteViaAi":true,"status":"string","createdBy":"string","createdAt":"string","updatedAt":"string","validationRules":[{}]}}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`form_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"getApiV1Customer-portalFormsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Formular in der Verwaltungssicht laden","description":"Liefert ein Formular samt Feldern und Validierungsregeln fuer die\nVerwaltungsmaske des Mandanten.\n\nAnders als die Kundensicht `GET /p/{slug}/forms/{form_id}` filtert diese\nRoute NICHT auf `status = active`: ein archiviertes oder geloeschtes\nFormular kommt hier ebenfalls zurueck, erkennbar an `status`."},"patch":{"responses":{"200":{"description":"Das Formular nach der Aenderung. Auch bei leerem Rumpf (dann unveraendert).","content":{"application/json":{"schema":{"type":"object","properties":{"form":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"fields":{"type":"array","items":{"type":"object","additionalProperties":{}}},"autoCreateTicket":{"type":"boolean"},"autoRouteViaAi":{"type":"boolean"},"status":{"type":"string"},"createdBy":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"validationRules":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["id","portalId","title","description","fields","autoCreateTicket","autoRouteViaAi","status","createdBy","createdAt","updatedAt","validationRules"],"additionalProperties":false}},"required":["form"],"additionalProperties":false},"example":{"form":{"id":"string","portalId":"string","title":"string","description":"string","fields":[{}],"autoCreateTicket":true,"autoRouteViaAi":true,"status":"string","createdBy":"string","createdAt":"string","updatedAt":"string","validationRules":[{}]}}}}},"400":{"description":"`field_name_duplicate:<name>`, `select_field_requires_options:<name>`, `field_validation_invalid_regex:<name>`, oder der Rumpf haelt das Schema nicht ein."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`form_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"patchApiV1Customer-portalFormsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Formular aendern","description":"Aendert Titel, Beschreibung, Felder, Validierungsregeln, Status und die\nbeiden Automatik-Schalter (Ticket anlegen, per KI zuordnen). Nur die\nuebergebenen Felder werden geschrieben.\n\nFELDER WERDEN ERSETZT, NICHT ZUSAMMENGEFUEHRT. Wer `fields` schickt,\nschickt die vollstaendige Liste — was fehlt, ist danach weg. Dasselbe\ngilt fuer `validation_rules`. Ein leerer Rumpf `{}` ist kein Fehler: die\nRoute antwortet 200 mit dem unveraenderten Formular.\n\nBEREITS ABGEGEBENE EINREICHUNGEN WERDEN NICHT MITGEZOGEN. Sie behalten\ndie Werte, die zum Zeitpunkt der Abgabe galten. Wird ein Feld\numbenannt oder entfernt, zeigen alte Einreichungen weiter den alten\nSchluessel — die Anzeige kann ihn dann keiner Beschriftung mehr\nzuordnen. Das ist gewollt und die Grundlage der Nachvollziehbarkeit.\n\nDie Feldliste wird geprueft: doppelte Feldnamen, `select` ohne\nAuswahlwerte und ein nicht uebersetzbarer Regex sind je 400, und die\nMeldung nennt den betroffenen Feldnamen.\n\nDer Status kennt hier nur `active` und `archived`. Ein per\n`DELETE /forms/{id}` auf `deleted` gesetztes Formular ist ueber diese\nRoute trotzdem erreichbar und laesst sich mit `status: \"active\"` wieder\nin Betrieb nehmen — das Aus-dem-Portal-Nehmen ist also umkehrbar.\n\nKeine Tarifsperre. Der 402 aus `/customer-portal/portals` liegt auf den\nPortal-Routen selbst.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000},"fields":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,62}$","minLength":1,"maxLength":63},"type":{"type":"string","enum":["text","email","select","checkbox","file","date","textarea"]},"label":{"type":"string","minLength":1,"maxLength":200},"required":{"type":"boolean","default":false},"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":200}},"validation":{"type":"string","maxLength":500},"placeholder":{"type":"string","maxLength":200}},"required":["name","type","label"]},"minItems":1,"maxItems":50},"auto_create_ticket":{"type":"boolean"},"auto_route_via_ai":{"type":"boolean"},"status":{"type":"string","enum":["active","archived"]},"validation_rules":{"type":"array","items":{"type":"object","properties":{"fieldName":{"type":"string","minLength":1,"maxLength":63},"type":{"type":"string","enum":["email","phone","minLength","regex"]},"value":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"}]}},"required":["fieldName","type"]},"maxItems":100}}},"example":{"title":"string","description":"string","auto_create_ticket":true,"auto_route_via_ai":true,"status":"active","validation_rules":[{"fieldName":"string","type":"email","value":"string"}]}}}}},"delete":{"responses":{"200":{"description":"Formular aus dem Portal genommen. Wiederholbar.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deleted":{"type":"boolean","const":true}},"required":["ok","deleted"],"additionalProperties":false},"example":{"ok":true,"deleted":true}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`form_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"deleteApiV1Customer-portalFormsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Formular aus dem Portal nehmen","description":"Nimmt das Formular aus dem Portal. Es wird NICHT geloescht, sondern auf\n`status = deleted` gesetzt — bereits abgegebene Einreichungen bleiben\ndadurch zuordenbar.\n\nDer Aufruf ist WIEDERHOLBAR: es gibt keine Bedingung auf den bisherigen\nStatus, ein zweiter Aufruf antwortet erneut mit 200. Die Loeschrouten fuer\ngeteilte Dokumente und Eigene Felder verhalten sich umgekehrt und\nantworten beim zweiten Mal 404.\n\n`deleted: true` heisst hier also Statuswechsel. Bei\n`DELETE /availability-overrides/{id}` steht dieselbe Antwort fuer eine\nechte Loeschung."}},"/api/v1/customer-portal/submissions":{"get":{"responses":{"200":{"description":"Die Einreichungen der aktuellen Seite","content":{"application/json":{"schema":{"type":"object","properties":{"submissions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"formId":{"type":"string"},"memberId":{"type":"string"},"submittedAt":{"type":"string"},"data":{"type":"object","additionalProperties":{},"description":"Die abgegebenen Werte, Schluessel sind die Feldnamen von damals."},"status":{"type":"string","description":"new, in_review, processed oder spam."},"assignedToUserId":{"type":["string","null"]},"ticketId":{"type":["string","null"]},"aiCategory":{"type":["string","null"]},"aiConfidence":{"type":["number","null"]},"notes":{"type":["string","null"]}},"required":["id","portalId","formId","memberId","submittedAt","data","status","assignedToUserId","ticketId","aiCategory","aiConfidence","notes"],"additionalProperties":false}},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["submissions","limit","offset"],"additionalProperties":false},"example":{"submissions":[{"id":"string","portalId":"string","formId":"string","memberId":"string","submittedAt":"string","data":{},"status":"string","assignedToUserId":"string","ticketId":"string","aiCategory":"string","aiConfidence":0,"notes":"string"}],"limit":0,"offset":0}}}},"400":{"description":"Ungueltige Query-Parameter (`portal_id` fehlt)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Customer-portalSubmissions","tags":["Customer-Portal"],"parameters":[{"in":"query","name":"portal_id","schema":{"type":"string","minLength":1,"maxLength":100},"required":true},{"in":"query","name":"form_id","schema":{"type":"string","minLength":1,"maxLength":100},"required":false},{"in":"query","name":"status","schema":{"type":"string","enum":["new","in_review","processed","spam"]},"required":false},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"required":false},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0},"required":false}],"summary":"Submissions eines Portals listen","description":"Die MANDANTEN-Sicht auf die Einreichungen eines Portals: jeder berechtigte Benutzer des Mandanten sieht alle, nicht nur die eigenen. `portal_id` ist Pflicht; zusaetzlich laesst sich auf `form_id` und `status` einschraenken. Sortiert nach Abgabezeitpunkt, neueste zuerst, geblaettert ueber `limit` (1-500, Standard 100) und `offset`. Die Antwort spiegelt `limit` und `offset` zurueck, nennt aber KEINE Gesamtzahl. Anders als beim Formular-Listing wird nicht geprueft, ob es das Portal gibt — eine unbekannte `portal_id` ergibt schlicht eine leere Liste."}},"/api/v1/customer-portal/submissions/{id}/route":{"post":{"responses":{"200":{"description":"Die neue Einordnung. Auch der Rueckfall nach einem KI-Fehler kommt hier an — dann steht `general` mit `confidence: 0` darin.","content":{"application/json":{"schema":{"type":"object","properties":{"submission_id":{"type":"string"},"category":{"type":"string","enum":["support","billing","sales","complaint","technical","general"]},"confidence":{"type":"number"},"assigned_to_user_id":{"type":["string","null"]},"reasoning":{"type":"string"},"routed_at":{"type":"string"}},"required":["submission_id","category","confidence","assigned_to_user_id","reasoning","routed_at"],"additionalProperties":false},"example":{"submission_id":"string","category":"support","confidence":0,"assigned_to_user_id":"string","reasoning":"string","routed_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"`submission_not_found`"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1Customer-portalSubmissionsByIdRoute","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Submission per AI re-routen","description":"Laesst eine bereits abgegebene Einreichung erneut von der KI einordnen und schreibt Kategorie, Sicherheit, zustaendigen Mitarbeiter und Begruendung in die Zeile zurueck — der bisherige Stand dieser vier Felder geht dabei verloren. Ob das Formular `auto_route_via_ai` gesetzt hat, spielt keine Rolle: wer diese Route aufruft, hat die Einordnung ausdruecklich verlangt. Ein Mitarbeiter wird nur ab einer Sicherheit von 0,6 vorgeschlagen, darunter bleibt `assigned_to_user_id` null. Scheitert die KI, faellt sie auf Kategorie `general` mit Sicherheit 0 zurueck, statt den Aufruf scheitern zu lassen — die Antwort bleibt 200. Die abgegebenen Werte selbst bleiben unangetastet. Unbekannte Einreichung → 404."}},"/api/v1/customer-portal/submissions/{id}":{"patch":{"responses":{"200":{"description":"Die Einreichung nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"submission":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"formId":{"type":"string"},"memberId":{"type":"string"},"submittedAt":{"type":"string"},"data":{"type":"object","additionalProperties":{},"description":"Die abgegebenen Werte, Schluessel sind die Feldnamen von damals."},"status":{"type":"string","description":"new, in_review, processed oder spam."},"assignedToUserId":{"type":["string","null"]},"ticketId":{"type":["string","null"]},"aiCategory":{"type":["string","null"]},"aiConfidence":{"type":["number","null"]},"notes":{"type":["string","null"]}},"required":["id","portalId","formId","memberId","submittedAt","data","status","assignedToUserId","ticketId","aiCategory","aiConfidence","notes"],"additionalProperties":false}},"required":["submission"],"additionalProperties":false},"example":{"submission":{"id":"string","portalId":"string","formId":"string","memberId":"string","submittedAt":"string","data":{},"status":"string","assignedToUserId":"string","ticketId":"string","aiCategory":"string","aiConfidence":0,"notes":"string"}}}}},"400":{"description":"`no_fields_to_update` bei leerem Rumpf, oder der Rumpf haelt das Schema nicht ein (unbekannter Status)."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`submission_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"patchApiV1Customer-portalSubmissionsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Einreichung bearbeiten","description":"Setzt Bearbeitungsstatus, zustaendigen Mitarbeiter und interne Notiz\neiner Formular-Einreichung. Nur die uebergebenen Felder werden\ngeschrieben; ein leerer Rumpf ist ein Fehler (400 `no_fields_to_update`).\n\nDIE ABGEGEBENEN WERTE SIND NICHT AENDERBAR. `data` steht nicht im\nSchema — was das Mitglied eingetragen hat, bleibt so, wie es abgegeben\nwurde. Diese Route aendert nur die Verwaltungsspur daneben.\n\nEs wird nichts geloescht. Auch `status: \"spam\"` setzt nur den Status —\ndie Zeile bleibt vollstaendig stehen, erscheint weiter in\n`GET /submissions` (die Liste filtert nur, wenn ein `status` mitgegeben\nwird) und laesst sich mit einem zweiten Aufruf zurueckholen. Eine Route,\ndie eine Einreichung wirklich entfernt, gibt es nicht.\n\n`assigned_to_user_id` wird NICHT geprueft: weder auf Existenz noch\ndarauf, ob der Benutzer zum Mandanten gehoert. `null` nimmt die\nZuweisung zurueck. Eine hier von Hand gesetzte Zuweisung ueberschreibt\ndie maschinelle Zuordnung aus `POST /submissions/{id}/route`; die Felder\n`aiCategory` und `aiConfidence` bleiben dabei stehen und koennen der\nneuen Zuweisung widersprechen.\n\nEs wird niemand benachrichtigt und kein Ticket angelegt.\n\nDas Portal wird NICHT geprueft: die Abfrage bindet nur die Kennung der\nEinreichung. Innerhalb desselben Mandanten ist damit auch eine\nEinreichung eines anderen Portals erreichbar.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten sieht und\nbearbeitet alle Einreichungen. Die auf das eigene Mitglied begrenzte\nSicht gibt es nur auf der Kundenstrecke.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["new","in_review","processed","spam"]},"assigned_to_user_id":{"type":["string","null"],"maxLength":100},"notes":{"type":["string","null"],"maxLength":5000}}},"example":{"status":"new","assigned_to_user_id":"string","notes":"string"}}}}}},"/api/v1/customer-portal/appointment-types":{"get":{"responses":{"200":{"description":"Die Terminarten des Portals. Leer, wenn es keine gibt.","content":{"application/json":{"schema":{"type":"object","properties":{"types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"durationMinutes":{"type":"integer"},"color":{"type":"string"},"assignedUserId":{"type":["string","null"]},"bufferMinutes":{"type":"integer"},"advanceNoticeHours":{"type":"integer"},"maxPerDay":{"type":["integer","null"]},"status":{"type":"string","description":"`active` oder `archived`."},"createdAt":{"type":"string"}},"required":["id","portalId","title","description","durationMinutes","color","assignedUserId","bufferMinutes","advanceNoticeHours","maxPerDay","status","createdAt"]}}},"required":["types"]},"example":{"types":[{"id":"string","portalId":"string","title":"string","description":"string","durationMinutes":0,"color":"string","assignedUserId":"string","bufferMinutes":0,"advanceNoticeHours":0,"maxPerDay":0,"status":"string","createdAt":"string"}]}}}},"400":{"description":"`portal_id` fehlt oder ist zu lang."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`portal_not_found` — unbekannt oder geloescht."},"503":{"description":"`database_unavailable`."}},"operationId":"getApiV1Customer-portalAppointment-types","tags":["Customer-Portal"],"parameters":[{"in":"query","name":"portal_id","schema":{"type":"string","minLength":1,"maxLength":100},"required":true},{"in":"query","name":"include_archived","schema":{"type":"string","enum":["true","false","1","0","yes","no","on","off"]},"required":false}],"summary":"Termin-Types eines Portals listen","description":"Liefert die buchbaren Terminarten eines Portals, neueste zuerst\n(`created_at` absteigend). `portal_id` ist PFLICHT; ohne den Parameter\nkommt 400.\n\nDas Portal WIRD hier geprueft: eine unbekannte oder geloeschte\nPortal-Kennung ergibt 404 `portal_not_found`. Die Nachbarrouten\n`GET /appointments` und `GET /availability-overrides` pruefen das nicht\nund antworten im selben Fall mit einer leeren Liste.\n\nOhne `include_archived` erscheinen nur Typen mit `status = active`.\nACHTUNG bei diesem Parameter: jeder nicht-leere Wert schaltet die\narchivierten Typen ZU — auch `include_archived=false` und\n`include_archived=0`. Zum Ausblenden den Parameter weglassen.\n\nDie Liste ist nicht blaetterbar und nicht begrenzt.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."},"post":{"responses":{"201":{"description":"Die angelegte Terminart.","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"durationMinutes":{"type":"integer"},"color":{"type":"string"},"assignedUserId":{"type":["string","null"]},"bufferMinutes":{"type":"integer"},"advanceNoticeHours":{"type":"integer"},"maxPerDay":{"type":["integer","null"]},"status":{"type":"string","description":"`active` oder `archived`."},"createdAt":{"type":"string"}},"required":["id","portalId","title","description","durationMinutes","color","assignedUserId","bufferMinutes","advanceNoticeHours","maxPerDay","status","createdAt"]}},"required":["type"]},"example":{"type":{"id":"string","portalId":"string","title":"string","description":"string","durationMinutes":0,"color":"string","assignedUserId":"string","bufferMinutes":0,"advanceNoticeHours":0,"maxPerDay":0,"status":"string","createdAt":"string"}}}}},"400":{"description":"`invalid_color` (kein `#rgb`/`#rrggbb`), fehlender Titel oder ein Zahlenwert ausserhalb seiner Grenzen."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`portal_not_found` — unbekannt oder geloescht."},"503":{"description":"`database_unavailable`."}},"operationId":"postApiV1Customer-portalAppointment-types","tags":["Customer-Portal"],"parameters":[],"summary":"Termin-Type anlegen","description":"Legt eine buchbare Terminart in einem bestehenden Portal an. Das Portal\nwird geprueft (404 `portal_not_found`), Pflicht sind nur `portal_id` und\n`title`.\n\nVoreinstellungen ohne Angabe: 30 Minuten Dauer, Farbe `#3b82f6`, 5\nMinuten Puffer, 24 Stunden Vorlaufzeit. `max_per_day` bleibt leer, es\ngibt dann keine Tagesobergrenze.\n\nDer Status ist beim Anlegen nicht setzbar: die Terminart entsteht immer\nals `active`. Archivieren geht erst danach ueber `DELETE /{id}` oder\n`PATCH /{id}` mit `status`.\n\nEs wird nichts zusammengefasst und auf nichts geprueft: gleiche Titel im\nselben Portal sind erlaubt, jeder Aufruf legt eine weitere Terminart an.\n`assigned_user_id` ist eine freie Zeichenkette und wird NICHT gegen die\nMitarbeiter des Mandanten abgeglichen.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"portal_id":{"type":"string","minLength":1,"maxLength":100},"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000},"duration_minutes":{"type":"integer","minimum":5,"maximum":720,"default":30},"color":{"type":"string","pattern":"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$","default":"#3b82f6"},"assigned_user_id":{"type":["string","null"],"maxLength":100},"buffer_minutes":{"type":"integer","minimum":0,"maximum":240,"default":5},"advance_notice_hours":{"type":"integer","minimum":0,"maximum":720,"default":24},"max_per_day":{"type":["integer","null"],"minimum":1,"maximum":100}},"required":["portal_id","title"]},"example":{"portal_id":"string","title":"string","description":"string","duration_minutes":5,"assigned_user_id":"string","buffer_minutes":0,"advance_notice_hours":0,"max_per_day":1}}}}}},"/api/v1/customer-portal/appointment-types/{id}":{"patch":{"responses":{"200":{"description":"Der Termintyp nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"durationMinutes":{"type":"integer"},"color":{"type":"string"},"assignedUserId":{"type":["string","null"]},"bufferMinutes":{"type":"integer"},"advanceNoticeHours":{"type":"integer"},"maxPerDay":{"type":["integer","null"]},"status":{"type":"string","description":"`active` oder `archived`."},"createdAt":{"type":"string"}},"required":["id","portalId","title","description","durationMinutes","color","assignedUserId","bufferMinutes","advanceNoticeHours","maxPerDay","status","createdAt"]}},"required":["type"]},"example":{"type":{"id":"string","portalId":"string","title":"string","description":"string","durationMinutes":0,"color":"string","assignedUserId":"string","bufferMinutes":0,"advanceNoticeHours":0,"maxPerDay":0,"status":"string","createdAt":"string"}}}}},"400":{"description":"`no_fields_to_update` bei leerem Rumpf, oder der Rumpf haelt das Schema nicht ein."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`type_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"patchApiV1Customer-portalAppointment-typesById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Termintyp aendern","description":"Aendert Bezeichnung, Beschreibung, Dauer, Farbe, zustaendigen\nMitarbeiter, Puffer, Vorlaufzeit, Tagesobergrenze und Status. Nur die\nuebergebenen Felder werden geschrieben.\n\nEin leerer Rumpf `{}` ist hier ein FEHLER (400 `no_fields_to_update`).\nDie Feld-Route `PATCH /portals/{portalId}/custom-fields/{fieldId}`\nantwortet im selben Fall 200 mit dem unveraenderten Satz — die beiden\nverhalten sich unterschiedlich.\n\nUeber `status` laeuft auch die Ruecknahme einer Archivierung:\n`archived` archiviert wie `DELETE /{id}`, `active` holt den Typ zurueck.\nDas Archivieren ist damit umkehrbar.\n\nBereits gebuchte Termine bleiben unberuehrt. Eine geaenderte Dauer gilt\nnur fuer kuenftige Buchungen — die Dauer wird beim Buchen in den Termin\nkopiert.\n\nDas Portal wird NICHT geprueft: die Abfrage bindet nur die Kennung des\nTermintyps, nicht das Portal. Innerhalb desselben Mandanten laesst sich\nso ein Typ aendern, der zu einem anderen Portal gehoert. Die\nMandantentrennung bleibt gewahrt (die Tabelle liegt im Mandanten-Schema).\n\nKeine Tarifsperre. Der 402 aus `/customer-portal/portals` liegt auf den\nPortal-Routen selbst.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000},"duration_minutes":{"type":"integer","minimum":5,"maximum":720},"color":{"type":"string","pattern":"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"},"assigned_user_id":{"type":["string","null"],"maxLength":100},"buffer_minutes":{"type":"integer","minimum":0,"maximum":240},"advance_notice_hours":{"type":"integer","minimum":0,"maximum":720},"max_per_day":{"type":["integer","null"],"minimum":1,"maximum":100},"status":{"type":"string","enum":["active","archived"]}}},"example":{"title":"string","description":"string","duration_minutes":5,"assigned_user_id":"string","buffer_minutes":0,"advance_notice_hours":0,"max_per_day":1,"status":"active"}}}}},"delete":{"responses":{"200":{"description":"Termintyp archiviert. Wiederholbar.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"archived":{"type":"boolean","const":true}},"required":["ok","archived"],"additionalProperties":false},"example":{"ok":true,"archived":true}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`type_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"deleteApiV1Customer-portalAppointment-typesById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Termintyp archivieren","description":"Archiviert den Termintyp. Er wird NICHT geloescht, sondern auf\n`status = archived` gesetzt — bereits gebuchte Termine bleiben dadurch\nauswertbar.\n\nDer Aufruf ist wiederholbar: es gibt keine Bedingung auf den bisherigen\nStatus, ein zweiter Aufruf antwortet erneut mit 200.\n\nDie Antwort heisst `archived`, nicht `deleted`. Die Loeschrouten fuer\nDokumente und Eigene Felder daneben antworten `deleted` und meinen\ndasselbe: einen Statuswechsel."}},"/api/v1/customer-portal/appointments":{"get":{"responses":{"200":{"description":"Eine Seite Termine. Leer, wenn es keine gibt ODER das Portal unbekannt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"appointments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"typeId":{"type":"string"},"memberId":{"type":"string"},"assignedUserId":{"type":["string","null"]},"scheduledAt":{"type":"string"},"durationMinutes":{"type":"integer"},"status":{"type":"string","description":"pending, confirmed, cancelled, done oder no_show."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","portalId","typeId","memberId","assignedUserId","scheduledAt","durationMinutes","status","notes","createdAt","updatedAt"]}},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["appointments","limit","offset"]},"example":{"appointments":[{"id":"string","portalId":"string","typeId":"string","memberId":"string","assignedUserId":"string","scheduledAt":"string","durationMinutes":0,"status":"string","notes":"string","createdAt":"string","updatedAt":"string"}],"limit":0,"offset":0}}}},"400":{"description":"`portal_id` fehlt, `limit` ueber 500, `offset` negativ, unbekannter Status oder `from`/`to` kein ISO-Zeitpunkt."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"503":{"description":"`database_unavailable`."}},"operationId":"getApiV1Customer-portalAppointments","tags":["Customer-Portal"],"parameters":[{"in":"query","name":"portal_id","schema":{"type":"string","minLength":1,"maxLength":100},"required":true},{"in":"query","name":"from","schema":{"type":"string","format":"date-time"},"required":false},{"in":"query","name":"to","schema":{"type":"string","format":"date-time"},"required":false},{"in":"query","name":"status","schema":{"type":"string","enum":["pending","confirmed","cancelled","done","no_show"]},"required":false},{"in":"query","name":"assigned_user_id","schema":{"type":"string","maxLength":100},"required":false},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"required":false},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0},"required":false}],"summary":"Termine eines Portals listen","description":"Blaettert ueber die gebuchten Termine eines Portals, nach Zeitpunkt\naufsteigend. `portal_id` ist PFLICHT; `limit` steht ohne Angabe auf 100\nund ist bei 500 gedeckelt, `offset` auf 0.\n\n`from` ist EINSCHLIESSEND, `to` AUSSCHLIESSEND (`>=` und `<`) — ein\nTagesbereich laesst sich also lueckenlos aneinanderreihen. Beide sind\nvollstaendige ISO-Zeitpunkte, kein reines Datum.\n\nOhne `status`-Filter sind ALLE Termine dabei, auch abgesagte\n(`cancelled`) und vergangene. Es gibt keinen Papierkorb und keine\nstille Ausblendung.\n\nDas Portal wird NICHT geprueft. Eine unbekannte oder geloeschte\nPortal-Kennung ergibt eine LEERE Liste mit Status 200, keinen 404 — die\nGegenroute `GET /appointment-types` prueft dagegen. Eine leere Liste\nbeweist hier also nicht, dass das Portal existiert.\n\nDie Antwort spiegelt `limit` und `offset` zurueck, nennt aber KEINE\nGesamtzahl. Ob eine weitere Seite folgt, zeigt nur eine volle Seite.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."},"post":{"responses":{"201":{"description":"Der angelegte Termin.","content":{"application/json":{"schema":{"type":"object","properties":{"appointment":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"typeId":{"type":"string"},"memberId":{"type":"string"},"assignedUserId":{"type":["string","null"]},"scheduledAt":{"type":"string"},"durationMinutes":{"type":"integer"},"status":{"type":"string","description":"pending, confirmed, cancelled, done oder no_show."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","portalId","typeId","memberId","assignedUserId","scheduledAt","durationMinutes","status","notes","createdAt","updatedAt"]}},"required":["appointment"]},"example":{"appointment":{"id":"string","portalId":"string","typeId":"string","memberId":"string","assignedUserId":"string","scheduledAt":"string","durationMinutes":0,"status":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"Pflichtfeld fehlt oder `start_at` ist kein ISO-Zeitpunkt."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`portal_not_found`, `appointment_type_not_found` (unbekannt, fremdes Portal oder archiviert) oder `member_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"postApiV1Customer-portalAppointments","tags":["Customer-Portal"],"parameters":[],"summary":"Termin im Auftrag des Customers buchen (Admin/AI)","description":"Setzt einen Termin fuer ein Portal-Mitglied — der Weg fuer die\nMandanten-Sicht und fuer den KI-Assistenten.\n\nES WIRD KEINE VERFUEGBARKEIT GEPRUEFT. Weder Buchungszeiten noch\nVorlaufzeit, Puffer, Tagesobergrenze oder eine Ueberschneidung mit einem\nanderen Termin halten den Aufruf auf; auch ein Zeitpunkt in der\nVergangenheit wird angenommen. Das ist Absicht: die Mandanten-Sicht darf\nbewusst in Pausen und ausserhalb der Slots legen. Die Pruefungen sitzen\nausschliesslich auf der Kunden-Buchungsstrecke\n(`/p/{slug}/appointments/book`).\n\nGeprueft wird nur, dass es die Beteiligten gibt: das Portal\n(404 `portal_not_found`), die Terminart — sie muss zu DIESEM Portal\ngehoeren und `active` sein (404 `appointment_type_not_found`) — und das\nMitglied in diesem Portal (404 `member_not_found`).\n\nDer Termin entsteht immer als `confirmed`, nie als `pending`; ein\nanderer Status ist beim Anlegen nicht uebergebbar. Die Dauer wird aus der\nTerminart KOPIERT und bleibt danach fest — eine spaetere Aenderung der\nTerminart wirkt nicht zurueck.\n\n`assigned_user_id` weggelassen bedeutet: der zustaendige Mitarbeiter der\nTerminart wird uebernommen. Ausdrueckliches `null` laesst den Termin\nunzugewiesen — das ist ein Unterschied.\n\nEs wird nichts verschickt: weder eine Bestaetigung an das Mitglied noch\neine Nachricht an den zugewiesenen Mitarbeiter. Ein Storno-Token wird\ndabei nicht angelegt.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"portal_id":{"type":"string","minLength":1,"maxLength":100},"member_id":{"type":"string","minLength":1,"maxLength":100},"type_id":{"type":"string","minLength":1,"maxLength":100},"start_at":{"type":"string","format":"date-time"},"assigned_user_id":{"type":["string","null"],"maxLength":100},"notes":{"type":"string","maxLength":2000}},"required":["portal_id","member_id","type_id","start_at"]},"example":{"portal_id":"string","member_id":"string","type_id":"string","start_at":"2026-01-01T12:00:00.000Z","assigned_user_id":"string","notes":"string"}}}}}},"/api/v1/customer-portal/appointments/{id}":{"patch":{"responses":{"200":{"description":"Der Termin nach der Aenderung.","content":{"application/json":{"schema":{"type":"object","properties":{"appointment":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"typeId":{"type":"string"},"memberId":{"type":"string"},"assignedUserId":{"type":["string","null"]},"scheduledAt":{"type":"string"},"durationMinutes":{"type":"integer"},"status":{"type":"string","description":"pending, confirmed, cancelled, done oder no_show."},"notes":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","portalId","typeId","memberId","assignedUserId","scheduledAt","durationMinutes","status","notes","createdAt","updatedAt"]}},"required":["appointment"]},"example":{"appointment":{"id":"string","portalId":"string","typeId":"string","memberId":"string","assignedUserId":"string","scheduledAt":"string","durationMinutes":0,"status":"string","notes":"string","createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"`no_fields_to_update` bei leerem Rumpf, oder der Rumpf haelt das Schema nicht ein (unbekannter Status, `scheduled_at` kein ISO-Zeitpunkt)."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`appointment_not_found`."},"503":{"description":"`database_unavailable`."}},"operationId":"patchApiV1Customer-portalAppointmentsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Termin aendern (Mandanten-Sicht)","description":"Aendert Status, interne Notiz, zustaendigen Mitarbeiter oder den\nZeitpunkt eines gebuchten Termins. Nur die uebergebenen Felder werden\ngeschrieben; ein leerer Rumpf ist ein Fehler (400 `no_fields_to_update`).\n\nABSAGEN LOESCHT NICHT. `status: \"cancelled\"` setzt nur den Status; die\nZeile bleibt vollstaendig stehen und laesst sich mit einem zweiten\nAufruf wieder auf `confirmed` setzen. Es gibt keine Route, die einen\nTermin wirklich entfernt.\n\nBeim Verlegen (`scheduled_at`) prueft die Route NICHTS: keine\nVerfuegbarkeit, keine Vorlaufzeit, keine Tagesobergrenze, keine\nUeberschneidung mit anderen Terminen. Das ist beabsichtigt — die\nMandanten-Sicht darf bewusst auch in Pausen und ausserhalb der\nBuchungszeiten legen. Die Pruefungen sitzen ausschliesslich auf der\nKunden-Buchungsstrecke (`/p/{slug}/appointments/book`).\n\nDie Dauer bleibt, wie sie beim Buchen war; sie wird hier nicht neu aus\ndem Termintyp gelesen.\n\nEs wird keine Benachrichtigung ausgeloest — weder an das Mitglied noch\nan den zugewiesenen Mitarbeiter.\n\nDas Portal wird NICHT geprueft: die Abfrage bindet nur die Termin-\nKennung. Innerhalb desselben Mandanten ist damit auch ein Termin eines\nanderen Portals erreichbar.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["pending","confirmed","cancelled","done","no_show"]},"notes":{"type":["string","null"],"maxLength":5000},"assigned_user_id":{"type":["string","null"],"maxLength":100},"scheduled_at":{"type":"string","format":"date-time"}}},"example":{"status":"pending","notes":"string","assigned_user_id":"string","scheduled_at":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/customer-portal/availability-overrides":{"get":{"responses":{"200":{"description":"Alle Regeln des Portals. Leer, wenn es keine gibt ODER das Portal unbekannt ist.","content":{"application/json":{"schema":{"type":"object","properties":{"overrides":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"userId":{"type":["string","null"],"description":"null: die Regel gilt fuer alle Mitarbeiter."},"weekday":{"type":["integer","null"],"description":"0-6 fuer eine woechentliche Regel; null, wenn `date` gesetzt ist."},"date":{"type":["string","null"],"description":"YYYY-MM-DD fuer einen einzelnen Tag."},"startTime":{"type":["string","null"]},"endTime":{"type":["string","null"]},"isAvailable":{"type":"boolean","description":"false sperrt den Zeitraum, true gibt ihn zusaetzlich frei."},"createdAt":{"type":"string"}},"required":["id","portalId","userId","weekday","date","startTime","endTime","isAvailable","createdAt"]}}},"required":["overrides"]},"example":{"overrides":[{"id":"string","portalId":"string","userId":"string","weekday":0,"date":"string","startTime":"string","endTime":"string","isAvailable":true,"createdAt":"string"}]}}}},"400":{"description":"`portal_id` fehlt oder ist zu lang."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"503":{"description":"`database_unavailable`."}},"operationId":"getApiV1Customer-portalAvailability-overrides","tags":["Customer-Portal"],"parameters":[{"in":"query","name":"portal_id","schema":{"type":"string","minLength":1,"maxLength":100},"required":true}],"summary":"Abweichende Verfuegbarkeiten eines Portals listen","description":"Listet die Ausnahmen von den regulaeren Buchungszeiten eines Portals —\ngesperrte oder zusaetzlich freigegebene Zeitraeume, entweder\nwoechentlich (`weekday`) oder fuer einen einzelnen Tag (`date`).\n\n`portal_id` ist PFLICHT; ohne den Parameter kommt 400. Die Liste ist\nnicht blaetterbar und nicht begrenzt: sie enthaelt immer alle Regeln des\nPortals, sortiert nach Datum, dann Wochentag, dann Startzeit.\n\nDas Portal wird NICHT geprueft. Eine unbekannte oder geloeschte\nPortal-Kennung ergibt eine LEERE Liste mit Status 200, keinen 404 — die\nGegenroute `POST` prueft dagegen und antwortet dort `portal_not_found`.\nEine leere Liste beweist hier also nicht, dass das Portal existiert.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten."},"post":{"responses":{"201":{"description":"Regel angelegt.","content":{"application/json":{"schema":{"type":"object","properties":{"override":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"userId":{"type":["string","null"],"description":"null: die Regel gilt fuer alle Mitarbeiter."},"weekday":{"type":["integer","null"],"description":"0-6 fuer eine woechentliche Regel; null, wenn `date` gesetzt ist."},"date":{"type":["string","null"],"description":"YYYY-MM-DD fuer einen einzelnen Tag."},"startTime":{"type":["string","null"]},"endTime":{"type":["string","null"]},"isAvailable":{"type":"boolean","description":"false sperrt den Zeitraum, true gibt ihn zusaetzlich frei."},"createdAt":{"type":"string"}},"required":["id","portalId","userId","weekday","date","startTime","endTime","isAvailable","createdAt"]}},"required":["override"]},"example":{"override":{"id":"string","portalId":"string","userId":"string","weekday":0,"date":"string","startTime":"string","endTime":"string","isAvailable":true,"createdAt":"string"}}}}},"400":{"description":"`either_weekday_or_date_required`, `weekday_and_date_mutually_exclusive`, `invalid_date_format` oder `invalid_time_format`."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`portal_not_found` — unbekannt oder geloescht."},"503":{"description":"`database_unavailable`."}},"operationId":"postApiV1Customer-portalAvailability-overrides","tags":["Customer-Portal"],"parameters":[],"summary":"Abweichende Verfuegbarkeit anlegen","description":"Legt eine Ausnahme von den regulaeren Buchungszeiten an. Entweder\nwoechentlich wiederkehrend (`weekday`, 0-6) ODER fuer einen einzelnen\nTag (`date`, YYYY-MM-DD) — genau eines von beiden, nie keines und nie\nbeide. Verstoesse dagegen sind 400.\n\n`is_available` steht ohne Angabe auf `false`, die Regel SPERRT also in\nder Voreinstellung. `true` gibt den Zeitraum zusaetzlich frei.\n\n`user_id` leer bedeutet: gilt fuer alle Mitarbeiter des Portals.\n\nEs wird nichts zusammengefasst und nichts ersetzt: jeder Aufruf legt\neine weitere Zeile an, auch wenn schon eine deckungsgleiche existiert.\nUeberschneidende oder widerspruechliche Regeln werden nicht erkannt.\nRuecknahme nur ueber `DELETE /availability-overrides/{id}` — und das\nloescht endgueltig, ohne Papierkorb.\n\nBereits gebuchte Termine bleiben stehen. Eine nachtraeglich gesetzte\nSperre sagt keinen bestehenden Termin ab, sie verhindert nur kuenftige\nBuchungen in diesem Zeitraum.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"portal_id":{"type":"string","minLength":1,"maxLength":100},"user_id":{"type":["string","null"],"maxLength":100},"weekday":{"type":["integer","null"],"minimum":0,"maximum":6},"date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"start_time":{"type":["string","null"],"pattern":"^\\d{2}:\\d{2}(:\\d{2})?$"},"end_time":{"type":["string","null"],"pattern":"^\\d{2}:\\d{2}(:\\d{2})?$"},"is_available":{"type":"boolean","default":false}},"required":["portal_id"]},"example":{"portal_id":"string","user_id":"string","weekday":0,"date":"2026-01-01","start_time":null,"end_time":null,"is_available":true}}}}}},"/api/v1/customer-portal/availability-overrides/{id}":{"delete":{"responses":{"200":{"description":"Zeile endgueltig geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deleted":{"type":"boolean","const":true}},"required":["ok","deleted"],"additionalProperties":false},"example":{"ok":true,"deleted":true}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`override_not_found` — unbekannt oder bereits geloescht."},"503":{"description":"`database_unavailable`."}},"operationId":"deleteApiV1Customer-portalAvailability-overridesById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Abweichende Verfuegbarkeit loeschen","description":"DIESE ROUTE LOESCHT WIRKLICH. Sie ist die einzige im Kundenportal, die\nein `DELETE FROM` ausfuehrt; alle anderen Loeschrouten setzen nur einen\nStatus. Die Zeile ist danach weg und nicht wiederherstellbar.\n\nDie Antwort `{ ok: true, deleted: true }` ist dieselbe wie bei\n`DELETE /forms/{id}` — dort bedeutet sie einen Statuswechsel, hier eine\nechte Loeschung. Am Antwortkoerper ist das nicht zu unterscheiden.\n\nNicht wiederholbar: ein zweiter Aufruf trifft nichts mehr und endet im 404."}},"/api/v1/customer-portal/admin-documents":{"get":{"responses":{"200":{"description":"Dokumente der Seite","content":{"application/json":{"schema":{"type":"object","properties":{"documents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Dokuments"},"portalId":{"type":"string","description":"Portal, zu dem die Datei gehoert"},"memberId":{"type":"string","description":"Portal-Mitglied, fuer das die Datei bestimmt ist"},"fileName":{"type":"string","description":"Bereinigter Dateiname"},"fileUrl":{"type":["string","null"],"description":"Adresse aus der Ablage; null wenn keine geliefert wurde. KEIN Download-Link — dafuer `/{id}/download`"},"mimeType":{"type":["string","null"],"description":"Inhaltstyp; `application/octet-stream` wenn der Upload keinen nannte"},"sizeBytes":{"type":["number","null"],"description":"Groesse in Byte; null wenn nicht erfasst"},"uploadedAt":{"type":"string","description":"Zeitpunkt des Hochladens"},"sharedWithUserIds":{"type":"array","items":{"type":"string"},"description":"Mandanten-Anwender, mit denen die Datei zusaetzlich geteilt ist; leere Liste wenn keine"},"isPublic":{"type":"boolean","description":"Ob die Datei im Portal offen sichtbar ist"},"dmsDocumentId":{"type":["string","null"],"description":"Verknuepftes DMS-Dokument; null wenn keins verknuepft ist"},"status":{"type":"string","description":"`active`, `quarantined` oder `deleted`"},"notes":{"type":["string","null"],"description":"Bemerkung; null wenn keine erfasst ist"}},"required":["id","portalId","memberId","fileName","fileUrl","mimeType","sizeBytes","uploadedAt","sharedWithUserIds","isPublic","dmsDocumentId","status","notes"]},"description":"Die Dokumente der Seite, neueste zuerst"},"limit":{"type":"integer","minimum":1,"maximum":500,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"}},"required":["documents","limit","offset"]},"example":{"documents":[{"id":"string","portalId":"string","memberId":"string","fileName":"string","fileUrl":"string","mimeType":"string","sizeBytes":0,"uploadedAt":"string","sharedWithUserIds":["string"],"isPublic":true,"dmsDocumentId":"string","status":"string","notes":"string"}],"limit":1,"offset":0}}}},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext"},"503":{"description":"`database_unavailable`"}},"operationId":"getApiV1Customer-portalAdmin-documents","tags":["Customer-Portal"],"parameters":[{"in":"query","name":"portal_id","schema":{"type":"string","minLength":1,"maxLength":100}},{"in":"query","name":"member_id","schema":{"type":"string","minLength":1,"maxLength":100}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","deleted","quarantined"]}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"summary":"Alle Customer-Portal-Uploads listen (mandantenweit)","description":"Liest `customer_portal_documents` des Mandanten ueber ALLE Portale hinweg, neueste zuerst. `portal_id`, `member_id` und `status` filtern exakt; `limit` (1-500, Vorgabe 100) und `offset` blaettern.  OHNE `status` sind herausgenommene Dateien (`deleted`) ausgeblendet, `quarantined` dagegen sichtbar. Wer die herausgenommenen sehen will, setzt `status=deleted` ausdruecklich.  Es kommt KEINE Gesamtzahl — nur die Seite selbst und die uebergebenen Seitenangaben. Und `fileUrl` ist kein Download-Link; den erzeugt `GET /{id}/download`. Fehlende Tabellen legt die Route beim Aufruf selbst an."},"post":{"responses":{"201":{"description":"Die angelegte Zeile mit vergebener Kennung","content":{"application/json":{"schema":{"type":"object","properties":{"document":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Dokuments"},"portalId":{"type":"string","description":"Portal, zu dem die Datei gehoert"},"memberId":{"type":"string","description":"Portal-Mitglied, fuer das die Datei bestimmt ist"},"fileName":{"type":"string","description":"Bereinigter Dateiname"},"fileUrl":{"type":["string","null"],"description":"Adresse aus der Ablage; null wenn keine geliefert wurde. KEIN Download-Link — dafuer `/{id}/download`"},"mimeType":{"type":["string","null"],"description":"Inhaltstyp; `application/octet-stream` wenn der Upload keinen nannte"},"sizeBytes":{"type":["number","null"],"description":"Groesse in Byte; null wenn nicht erfasst"},"uploadedAt":{"type":"string","description":"Zeitpunkt des Hochladens"},"sharedWithUserIds":{"type":"array","items":{"type":"string"},"description":"Mandanten-Anwender, mit denen die Datei zusaetzlich geteilt ist; leere Liste wenn keine"},"isPublic":{"type":"boolean","description":"Ob die Datei im Portal offen sichtbar ist"},"dmsDocumentId":{"type":["string","null"],"description":"Verknuepftes DMS-Dokument; null wenn keins verknuepft ist"},"status":{"type":"string","description":"`active`, `quarantined` oder `deleted`"},"notes":{"type":["string","null"],"description":"Bemerkung; null wenn keine erfasst ist"}},"required":["id","portalId","memberId","fileName","fileUrl","mimeType","sizeBytes","uploadedAt","sharedWithUserIds","isPublic","dmsDocumentId","status","notes"]}},"required":["document"]},"example":{"document":{"id":"string","portalId":"string","memberId":"string","fileName":"string","fileUrl":"string","mimeType":"string","sizeBytes":0,"uploadedAt":"string","sharedWithUserIds":["string"],"isPublic":true,"dmsDocumentId":"string","status":"string","notes":"string"}}}}},"400":{"description":"multipart fehlt, `file`/`portal_id`/`member_id` fehlt oder Datei leer"},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext"},"413":{"description":"`file_too_large_max_25mb`"},"415":{"description":"`file_type_not_allowed` — ausfuehrbare Endung"},"500":{"description":"`document_insert_failed` — Datei liegt in der Ablage, die Zeile fehlt"},"502":{"description":"`storage_upload_failed` — nichts wurde gespeichert"},"503":{"description":"`database_unavailable`"}},"operationId":"postApiV1Customer-portalAdmin-documents","tags":["Customer-Portal"],"parameters":[],"summary":"Mandant lädt Datei für einen Portal-Member hoch (multipart, Feld \"file\")","description":"Nimmt EINE Datei als multipart entgegen: `file` sowie `portal_id` und `member_id` sind Pflicht, `notes` ist freiwillig und wird auf 1000 Zeichen gekuerzt. Der Dateiname wird bereinigt, die Datei in die Ablage geschrieben und danach eine Zeile mit `status = active` angelegt.  Grenzen: hoechstens 25 MB, eine leere Datei wird abgelehnt, und ausfuehrbare Endungen (exe, bat, sh, js, jar, ps1, dll und weitere) ergeben 415. Scheitert der Ablage-Upload, kommt 502 und es entsteht KEINE Zeile — anders als auf der Kunden-Seite gibt es hier keinen Datensatz ohne Datei.  Ob es Portal und Mitglied ueberhaupt gibt, wird NICHT geprueft. Antwortet mit 201."}},"/api/v1/customer-portal/admin-documents/{id}/download":{"get":{"responses":{"200":{"description":"Signierte Adresse, Dateiname und Restlaufzeit","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"Signierte Adresse zum Herunterladen — zeitlich begrenzt gueltig"},"fileName":{"type":"string","description":"Dateiname zum Speichern"},"expiresInSeconds":{"type":"integer","description":"Gueltigkeitsdauer der Adresse in Sekunden (24 Stunden)"}},"required":["url","fileName","expiresInSeconds"]},"example":{"url":"string","fileName":"string","expiresInSeconds":0}}}},"400":{"description":"`invalid_id` — leer oder laenger als 100 Zeichen"},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext"},"404":{"description":"`document_not_found`"},"410":{"description":"`document_deleted` — aus dem Portal genommen"},"502":{"description":"`presign_failed`"},"503":{"description":"`database_unavailable`"}},"operationId":"getApiV1Customer-portalAdmin-documentsByIdDownload","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Signed Download-URL erzeugen (TTL 24h)","description":"Erzeugt eine signierte Adresse zur Datei, 24 Stunden gueltig. Die Datei selbst kommt NICHT zurueck — nur die Adresse, der Dateiname und die Restlaufzeit. Wer die Adresse hat, kommt bis zum Ablauf ohne Anmeldung an die Datei.  Ein herausgenommenes Dokument (`status = deleted`) ergibt 410, nicht 404: die Zeile gibt es noch, sie ist nur nicht mehr abrufbar. Unbekannte Kennung ergibt 404, ein Fehler beim Signieren 502."}},"/api/v1/customer-portal/admin-documents/{id}/share":{"post":{"responses":{"200":{"description":"Das Dokument mit der neuen Freigabeliste","content":{"application/json":{"schema":{"type":"object","properties":{"document":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Dokuments"},"portalId":{"type":"string","description":"Portal, zu dem die Datei gehoert"},"memberId":{"type":"string","description":"Portal-Mitglied, fuer das die Datei bestimmt ist"},"fileName":{"type":"string","description":"Bereinigter Dateiname"},"fileUrl":{"type":["string","null"],"description":"Adresse aus der Ablage; null wenn keine geliefert wurde. KEIN Download-Link — dafuer `/{id}/download`"},"mimeType":{"type":["string","null"],"description":"Inhaltstyp; `application/octet-stream` wenn der Upload keinen nannte"},"sizeBytes":{"type":["number","null"],"description":"Groesse in Byte; null wenn nicht erfasst"},"uploadedAt":{"type":"string","description":"Zeitpunkt des Hochladens"},"sharedWithUserIds":{"type":"array","items":{"type":"string"},"description":"Mandanten-Anwender, mit denen die Datei zusaetzlich geteilt ist; leere Liste wenn keine"},"isPublic":{"type":"boolean","description":"Ob die Datei im Portal offen sichtbar ist"},"dmsDocumentId":{"type":["string","null"],"description":"Verknuepftes DMS-Dokument; null wenn keins verknuepft ist"},"status":{"type":"string","description":"`active`, `quarantined` oder `deleted`"},"notes":{"type":["string","null"],"description":"Bemerkung; null wenn keine erfasst ist"}},"required":["id","portalId","memberId","fileName","fileUrl","mimeType","sizeBytes","uploadedAt","sharedWithUserIds","isPublic","dmsDocumentId","status","notes"]}},"required":["document"]},"example":{"document":{"id":"string","portalId":"string","memberId":"string","fileName":"string","fileUrl":"string","mimeType":"string","sizeBytes":0,"uploadedAt":"string","sharedWithUserIds":["string"],"isPublic":true,"dmsDocumentId":"string","status":"string","notes":"string"}}}}},"400":{"description":"`invalid_id` — leer oder laenger als 100 Zeichen"},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext"},"404":{"description":"`document_not_found` — unbekannt ODER bereits herausgenommen"},"503":{"description":"`database_unavailable`"}},"operationId":"postApiV1Customer-portalAdmin-documentsByIdShare","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Datei mit weiteren Tenant-Usern teilen","description":"ERSETZT die Liste der Mandanten-Anwender, mit denen die Datei geteilt ist — sie wird nicht ergaenzt. Wer in `user_ids` fehlt, verliert die Freigabe; eine leere Liste hebt jede Freigabe auf. Doppelte und leere Eintraege werden verworfen, hoechstens 50 sind erlaubt.  Ob es die genannten Anwender gibt, wird NICHT geprueft. Ein herausgenommenes Dokument (`status = deleted`) laesst sich nicht teilen und ergibt 404, ebenso eine unbekannte Kennung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"user_ids":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50}},"required":["user_ids"]},"example":{"user_ids":["string"]}}}}}},"/api/v1/customer-portal/admin-documents/{id}":{"delete":{"responses":{"200":{"description":"Aus dem Portal genommen. `id` nennt den betroffenen Datensatz.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deleted":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","deleted","id"],"additionalProperties":false},"example":{"ok":true,"deleted":true,"id":"string"}}}},"400":{"description":"`invalid_id` — leer oder laenger als 100 Zeichen."},"401":{"description":"Keine Sitzung oder kein Mandanten-Kontext."},"404":{"description":"`document_not_found` — unbekannt ODER bereits herausgenommen."},"503":{"description":"`database_unavailable`."}},"operationId":"deleteApiV1Customer-portalAdmin-documentsById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Geteiltes Dokument aus dem Portal nehmen","description":"Nimmt das Dokument aus dem Portal. Es wird NICHT geloescht, sondern auf\n`status = deleted` gesetzt; die Datei bleibt in der Ablage.\n\nDIESER AUFRUF IST NICHT WIEDERHOLBAR. Die Bedingung lautet\n`status != deleted`, ein zweiter Aufruf trifft also keine Zeile mehr und\nendet im 404. Das unterscheidet die Route von den Loeschrouten fuer\nFormulare, Portale und Termintypen, die beliebig oft 200 antworten."}},"/api/v1/customer-portal/messages":{"get":{"responses":{"200":{"description":"Der Verlauf. `hasMore` ist nur eine Vermutung — es steht auf true, sobald die Seite genau voll ist, auch wenn danach nichts mehr kommt.","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"memberId":{"type":"string"},"threadId":{"type":["string","null"]},"direction":{"type":"string","enum":["in","out"]},"senderUserId":{"type":["string","null"]},"body":{"type":"string"},"attachments":{"type":"array","items":{}},"aiCategory":{"type":["string","null"]},"aiConfidence":{"type":["number","null"]},"readAt":{"type":["string","null"]},"deliveryStatus":{"type":"string"},"emailMessageId":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","portalId","memberId","threadId","direction","senderUserId","body","attachments","aiCategory","aiConfidence","readAt","deliveryStatus","emailMessageId","createdAt"],"additionalProperties":false}},"hasMore":{"type":"boolean"}},"required":["messages","hasMore"],"additionalProperties":false},"example":{"messages":[{"id":"string","portalId":"string","memberId":"string","threadId":"string","direction":"in","senderUserId":"string","body":"string","attachments":[],"aiCategory":"string","aiConfidence":0,"readAt":"string","deliveryStatus":"string","emailMessageId":"string","createdAt":"string"}],"hasMore":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Customer-portalMessages","tags":["Customer-Portal"],"parameters":[],"summary":"Messages eines Members (Mandanten-Side)","description":"Liest den Nachrichtenverlauf EINES Portal-Mitglieds. `portal_id` und `member_id` sind beide PFLICHT — es gibt keine portalweite Gesamtliste. Sortiert nach Anlagezeit absteigend, neueste zuerst; `limit` liegt zwischen 1 und 200 (Vorgabe 50), und `before` (ISO-Zeitpunkt) blaettert weiter zurueck. Ein- und ausgehende Nachrichten kommen gemeinsam, unterschieden durch `direction`. Gehoert das Portal nicht zu diesem Mandanten oder ist es nicht aktiv, kommt 404. Der Aufruf markiert NICHTS als gelesen."}},"/api/v1/customer-portal/messages/unread-count":{"get":{"responses":{"200":{"description":"Die Zahl der ungelesenen eingehenden Nachrichten.","content":{"application/json":{"schema":{"type":"object","properties":{"unread":{"type":"number"}},"required":["unread"],"additionalProperties":false},"example":{"unread":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Customer-portalMessagesUnread-count","tags":["Customer-Portal"],"parameters":[],"summary":"Anzahl ungelesener IN-Messages des Portals","description":"Zaehlt die eingehenden Nachrichten (`direction = in`) OHNE `read_at` ueber das GANZE Portal, nicht je Mitglied. `portal_id` ist Pflicht. Anders als die Verlaufsliste prueft dieser Aufruf NICHT, ob das Portal zu diesem Mandanten gehoert — er zaehlt in dessen Schema, eine fremde Kennung ergibt daher 0 und kein 404. Ausgehende Nachrichten zaehlen nie mit."}},"/api/v1/customer-portal/messages/send":{"post":{"responses":{"201":{"description":"Die Nachricht ist gespeichert. `delivery_status` steht hier immer auf `sent` und sagt NICHTS ueber den Empfang. Ob die Ersatz-Mail rausging, steht in `email_sent`; scheiterte sie, nennt `email_error` den Grund — die Nachricht bleibt trotzdem gespeichert und der Aufruf trotzdem 201.","content":{"application/json":{"schema":{"type":"object","properties":{"message_id":{"type":"string"},"delivery_status":{"type":"string"},"email_sent":{"type":"boolean"},"email_error":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["message_id","delivery_status","email_sent","email_error","created_at"],"additionalProperties":false},"example":{"message_id":"string","delivery_status":"string","email_sent":true,"email_error":"string","created_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Customer-portalMessagesSend","tags":["Customer-Portal"],"parameters":[],"summary":"Nachricht an Portal-Customer senden","description":"Legt eine ausgehende Nachricht (`direction = out`) an und stoesst die Zustellung an. Geprueft wird zuerst, ob das Portal zu diesem Mandanten gehoert und aktiv ist, dann ob das Mitglied dazu aktiv ist — beides sonst 404. Anschliessend geht ein WebSocket-Ereignis raus (Fehler dort bleiben folgenlos), und wenn das Mitglied offline ist oder `force_email` gesetzt wurde, zusaetzlich eine Ersatz-Mail mit Antwort-Kennung. Hoechstens 20 Anhaenge, hoechstens 20 000 Zeichen Text. Die Antwort ist snake_case, anders als die Verlaufsliste."}},"/api/v1/customer-portal/messages/{id}/read":{"post":{"responses":{"200":{"description":"Quittung mit dem gueltigen Lesezeitpunkt — beim zweiten Aufruf der aus dem ersten.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"read_at":{"type":"string"}},"required":["ok","read_at"],"additionalProperties":false},"example":{"ok":true,"read_at":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Customer-portalMessagesByIdRead","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"IN-Message als gelesen markieren (Mandant)","description":"Setzt `read_at` auf jetzt — aber nur, wenn es noch leer ist: ein zweiter Aufruf laesst den urspruenglichen Zeitpunkt stehen und antwortet mit ihm. Wirkt ausschliesslich auf EINGEHENDE Nachrichten; eine ausgehende oder unbekannte Kennung ergibt 404. Danach geht eine Lesebestaetigung per WebSocket an das Portal, deren Scheitern folgenlos bleibt."}},"/api/v1/customer-portal/portals/{portalId}/reminders":{"get":{"responses":{"200":{"description":"Die Reminder des Portals, in camelCase.","content":{"application/json":{"schema":{"type":"object","properties":{"reminders":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"customerId":{"type":["string","null"]},"reminderType":{"type":"string"},"triggerKind":{"type":"string"},"scheduleCron":{"type":["string","null"]},"triggerEvent":{"type":["string","null"]},"templateId":{"type":"string"},"templateVars":{},"active":{"type":"boolean"},"lastSentAt":{"type":["string","null"]},"nextDueAt":{"type":["string","null"]},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"createdBy":{"type":["string","null"]}},"required":["id","portalId","customerId","reminderType","triggerKind","scheduleCron","triggerEvent","templateId","active","lastSentAt","nextDueAt","deletedAt","createdAt","createdBy"],"additionalProperties":false}}},"required":["reminders"],"additionalProperties":false},"example":{"reminders":[{"id":"string","portalId":"string","customerId":"string","reminderType":"string","triggerKind":"string","scheduleCron":"string","triggerEvent":"string","templateId":"string","active":true,"lastSentAt":"string","nextDueAt":"string","deletedAt":"string","createdAt":"string","createdBy":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Customer-portalPortalsByPortalIdReminders","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true}],"summary":"Reminders fuer ein Portal listen","description":"Liest `customer_portal_reminders` zu diesem Portal ohne die weich geloeschten Zeilen, neueste zuerst. Die Menge ist fest auf 200 Zeilen begrenzt; Filter, Blaetterung und Gesamtzahl gibt es nicht — bei mehr als 200 Reminders ist die Liste abgeschnitten, ohne dass die Antwort es sagt. Inaktive Reminder (`active: false`) sind enthalten."},"post":{"responses":{"201":{"description":"Der angelegte Reminder, im `reminder`-Umschlag.","content":{"application/json":{"schema":{"type":"object","properties":{"reminder":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"customerId":{"type":["string","null"]},"reminderType":{"type":"string"},"triggerKind":{"type":"string"},"scheduleCron":{"type":["string","null"]},"triggerEvent":{"type":["string","null"]},"templateId":{"type":"string"},"templateVars":{},"active":{"type":"boolean"},"lastSentAt":{"type":["string","null"]},"nextDueAt":{"type":["string","null"]},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"createdBy":{"type":["string","null"]}},"required":["id","portalId","customerId","reminderType","triggerKind","scheduleCron","triggerEvent","templateId","active","lastSentAt","nextDueAt","deletedAt","createdAt","createdBy"],"additionalProperties":false}},"required":["reminder"],"additionalProperties":false},"example":{"reminder":{"id":"string","portalId":"string","customerId":"string","reminderType":"string","triggerKind":"string","scheduleCron":"string","triggerEvent":"string","templateId":"string","active":true,"lastSentAt":"string","nextDueAt":"string","deletedAt":"string","createdAt":"string","createdBy":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Customer-portalPortalsByPortalIdReminders","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true}],"summary":"Neuen Reminder anlegen","description":"Legt eine Zeile in `customer_portal_reminders` an. `trigger_kind` entscheidet, was zusaetzlich verlangt wird: bei `cron` ein `schedule_cron`, bei `event` ein `trigger_event` — fehlt es, lehnt die Pruefung den Rumpf ab. Bei `cron` errechnet der Aufruf `next_due_at` aus dem Ausdruck, sofern nicht ausdruecklich mitgegeben. `customer_id` steuert die Empfaenger: leer heisst alle aktiven Mitglieder des Portals, gesetzt heisst nur dieses eine. Verschickt wird hier NICHTS — das tut der Cron-Worker oder `/send-now`. Ob `template_id` ueberhaupt existiert, wird erst beim Versand geprueft.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customer_id":{"type":["string","null"],"format":"uuid"},"reminder_type":{"type":"string","enum":["document_upload","appointment_confirm","payment_due","custom"]},"trigger_kind":{"type":"string","enum":["cron","manual","event"]},"schedule_cron":{"type":["string","null"],"minLength":1,"maxLength":120},"trigger_event":{"type":["string","null"],"minLength":1,"maxLength":120},"template_id":{"type":"string","minLength":1,"maxLength":120},"template_vars":{"type":"object","additionalProperties":{},"default":{}},"active":{"type":"boolean","default":true},"next_due_at":{"type":"string","format":"date-time"}},"required":["reminder_type","trigger_kind","template_id"]},"example":{"customer_id":"00000000-0000-4000-8000-000000000000","reminder_type":"document_upload","trigger_kind":"cron","schedule_cron":"string","trigger_event":"string","template_id":"string","template_vars":{},"active":true,"next_due_at":"2026-01-01T12:00:00.000Z"}}}}}},"/api/v1/customer-portal/portals/{portalId}/reminders/{id}":{"put":{"responses":{"200":{"description":"Der geaenderte Reminder, vollstaendig — nicht nur die geaenderten Felder.","content":{"application/json":{"schema":{"type":"object","properties":{"reminder":{"type":"object","properties":{"id":{"type":"string"},"portalId":{"type":"string"},"customerId":{"type":["string","null"]},"reminderType":{"type":"string"},"triggerKind":{"type":"string"},"scheduleCron":{"type":["string","null"]},"triggerEvent":{"type":["string","null"]},"templateId":{"type":"string"},"templateVars":{},"active":{"type":"boolean"},"lastSentAt":{"type":["string","null"]},"nextDueAt":{"type":["string","null"]},"deletedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"createdBy":{"type":["string","null"]}},"required":["id","portalId","customerId","reminderType","triggerKind","scheduleCron","triggerEvent","templateId","active","lastSentAt","nextDueAt","deletedAt","createdAt","createdBy"],"additionalProperties":false}},"required":["reminder"],"additionalProperties":false},"example":{"reminder":{"id":"string","portalId":"string","customerId":"string","reminderType":"string","triggerKind":"string","scheduleCron":"string","triggerEvent":"string","templateId":"string","active":true,"lastSentAt":"string","nextDueAt":"string","deletedAt":"string","createdAt":"string","createdBy":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putApiV1Customer-portalPortalsByPortalIdRemindersById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Reminder aktualisieren","description":"Schreibt nur die mitgeschickten Felder; nicht genannte bleiben unberuehrt. Enthaelt der Rumpf keines, antwortet der Aufruf 400 `no_fields_to_update`. Wird `schedule_cron` geaendert, ohne dass `next_due_at` ausdruecklich mitkommt, rechnet der Aufruf die naechste Faelligkeit neu. `trigger_kind` laesst sich hier NICHT aendern — das Feld wird zwar angenommen, aber nicht geschrieben. Ein weich geloeschter oder unbekannter Reminder ergibt 404.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"customer_id":{"type":["string","null"],"format":"uuid"},"reminder_type":{"type":"string","enum":["document_upload","appointment_confirm","payment_due","custom"]},"trigger_kind":{"type":"string","enum":["cron","manual","event"]},"schedule_cron":{"type":["string","null"],"minLength":1,"maxLength":120},"trigger_event":{"type":["string","null"],"minLength":1,"maxLength":120},"template_id":{"type":"string","minLength":1,"maxLength":120},"template_vars":{"type":"object","additionalProperties":{},"default":{}},"active":{"type":"boolean","default":true},"next_due_at":{"type":"string","format":"date-time"}}},"example":{"customer_id":"00000000-0000-4000-8000-000000000000","reminder_type":"document_upload","trigger_kind":"cron","schedule_cron":"string","trigger_event":"string","template_id":"string","template_vars":{},"active":true,"next_due_at":"2026-01-01T12:00:00.000Z"}}}}},"delete":{"responses":{"200":{"description":"Quittung mit der getroffenen Kennung, kein Datensatz.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","id"],"additionalProperties":false},"example":{"ok":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"deleteApiV1Customer-portalPortalsByPortalIdRemindersById","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Reminder soft-loeschen","description":"Setzt `deleted_at` und zugleich `active = false`; die Zeile bleibt in der Datenbank und verschwindet aus Liste, Aenderung und Versand. Einen Endpunkt zum Zurueckholen gibt es nicht. Ein zweiter Loeschversuch trifft nichts mehr und ergibt 404 — so bleibt der Zeitpunkt der ersten Loeschung erhalten."}},"/api/v1/customer-portal/portals/{portalId}/reminders/{id}/send-now":{"post":{"responses":{"200":{"description":"Versandbericht. `ok: true` heisst NUR, dass der Versuch lief — die Wahrheit steht in `sent` und `errors`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"sent":{"type":"number"},"errors":{"type":"array","items":{"type":"object","properties":{"memberId":{"type":"string"},"error":{"type":"string"}},"required":["memberId","error"],"additionalProperties":false}}},"required":["ok","sent","errors"],"additionalProperties":false},"example":{"ok":true,"sent":0,"errors":[{"memberId":"string","error":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1Customer-portalPortalsByPortalIdRemindersByIdSend-now","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"portalId","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Reminder sofort versenden (Test/Manual)","description":"Verschickt den Reminder sofort an die im Datensatz hinterlegten Empfaenger, unabhaengig von `trigger_kind` und `next_due_at`. Danach wird `last_sent_at` gesetzt; bei einem Cron-Reminder wird ausserdem die naechste Faelligkeit neu gerechnet — ein Testversand verschiebt also den regulaeren Lauf. Der Aufruf antwortet auch dann 200 `ok`, wenn NIEMAND erreicht wurde: `sent` ist dann 0, und `errors` nennt je Empfaenger den Grund. Ein unbekanntes Template steht dort mit der Kennung `<resolver>`. Ein weich geloeschter oder unbekannter Reminder ergibt 404."}},"/api/v1/customer-portal/portals/{id}/custom-domain":{"get":{"responses":{"200":{"description":"Stand der eigenen Domain samt TXT-Anleitung","content":{"application/json":{"schema":{"type":"object","properties":{"portalId":{"type":"string"},"customDomain":{"type":["string","null"]},"status":{"type":["string","null"]},"verifiedAt":{"type":["string","null"]},"lastCheckAt":{"type":["string","null"]},"lastError":{"type":["string","null"]},"instruction":{"type":["object","null"],"properties":{"recordName":{"type":"string"},"recordValue":{"type":"string"}},"required":["recordName","recordValue"],"additionalProperties":false}},"required":["portalId","customDomain","status","verifiedAt","lastCheckAt","lastError","instruction"],"additionalProperties":false},"example":{"portalId":"string","customDomain":"string","status":"string","verifiedAt":"string","lastCheckAt":"string","lastError":"string","instruction":{"recordName":"string","recordValue":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"`portal_not_found`"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"getApiV1Customer-portalPortalsByIdCustom-domain","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Custom-Domain-Status + TXT-Anleitung lesen","description":"Liest den Stand der eigenen Domain dieses Portals: eingetragener Name, Status (`pending`, `verified` oder `failed`), Zeitpunkt der letzten Pruefung und deren Fehlergrund. Solange Domain und Token gesetzt sind, nennt `instruction` den TXT-Eintrag, den der Mandant in seinem DNS anlegen muss: Name `_nemix-verify.<domain>`, Wert `nemix-verify=<token>`. Sonst ist `instruction` null. Fehlende Spalten zieht die Route beim Aufruf selbst nach. Unbekanntes Portal → 404."},"post":{"responses":{"200":{"description":"Domain eingetragen, Token erzeugt, Status pending","content":{"application/json":{"schema":{"type":"object","properties":{"portalId":{"type":"string"},"customDomain":{"type":["string","null"]},"status":{"type":["string","null"]},"verifiedAt":{"type":["string","null"]},"lastCheckAt":{"type":["string","null"]},"lastError":{"type":["string","null"]},"instruction":{"type":["object","null"],"properties":{"recordName":{"type":"string"},"recordValue":{"type":"string"}},"required":["recordName","recordValue"],"additionalProperties":false}},"required":["portalId","customDomain","status","verifiedAt","lastCheckAt","lastError","instruction"],"additionalProperties":false},"example":{"portalId":"string","customDomain":"string","status":"string","verifiedAt":"string","lastCheckAt":"string","lastError":"string","instruction":{"recordName":"string","recordValue":"string"}}}}},"400":{"description":"`invalid_domain` oder `portal_id_required`"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"`portal_not_found`"},"409":{"description":"`domain_already_verified_for_other_tenant`"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1Customer-portalPortalsByIdCustom-domain","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Custom-Domain setzen — generiert Token + Status pending","description":"Traegt die Domain klein geschrieben ein, erzeugt einen frischen 64-stelligen Pruef-Token und setzt den Status auf `pending`; Verifizierungs-Zeitpunkt, letzte Pruefung und Fehlergrund werden dabei geleert. Auch dieselbe Domain erneut zu setzen erzeugt einen NEUEN Token und erzwingt damit einen sauberen Durchlauf. Vorher sucht die Route in allen Mandanten-Schemas nach einem Portal, das diese Domain bereits verifiziert hat, und lehnt dann mit 409 ab; scheitert diese Suche an der Datenbank, laesst sie den Vorgang durch. Die Antwort ist 200 mit dem neuen Stand — nicht 201.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","pattern":"^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,}$","maxLength":253}},"required":["domain"]}}}}}},"/api/v1/customer-portal/portals/{id}/custom-domain/verify":{"post":{"responses":{"200":{"description":"Ergebnis der Pruefung. Auch ein Fehlschlag kommt hier an — `status` und `lastError` sagen, was war.","content":{"application/json":{"schema":{"type":"object","properties":{"portalId":{"type":"string"},"customDomain":{"type":["string","null"]},"status":{"type":["string","null"]},"verifiedAt":{"type":["string","null"]},"lastCheckAt":{"type":["string","null"]},"lastError":{"type":["string","null"]},"instruction":{"type":["object","null"],"properties":{"recordName":{"type":"string"},"recordValue":{"type":"string"}},"required":["recordName","recordValue"],"additionalProperties":false}},"required":["portalId","customDomain","status","verifiedAt","lastCheckAt","lastError","instruction"],"additionalProperties":false},"example":{"portalId":"string","customDomain":"string","status":"string","verifiedAt":"string","lastCheckAt":"string","lastError":"string","instruction":{"recordName":"string","recordValue":"string"}}}}},"400":{"description":"`domain_not_configured` oder `portal_id_required`"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"`portal_not_found`"},"503":{"description":"Datenbank nicht erreichbar"}},"operationId":"postApiV1Customer-portalPortalsByIdCustom-domainVerify","tags":["Customer-Portal"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"TXT-Record pruefen → Status verified|failed","description":"Schlaegt `_nemix-verify.<domain>` als TXT-Eintrag nach (Abbruch nach 5 Sekunden) und vergleicht ihn mit dem gespeicherten Token. Trifft er zu, wird der Status `verified` und der Verifizierungs-Zeitpunkt gesetzt; sonst `failed`, und `lastError` nennt den Grund (kein Eintrag, Token passt nicht, oder die Meldung des DNS-Fehlers). Ein Fehlschlag ist KEIN Fehlerstatus: die Antwort bleibt 200 und der Grund steht im Rumpf. Ein frueher erreichtes `verifiedAt` bleibt dabei stehen, obwohl der Status auf `failed` wechselt. Ohne eingetragene Domain oder Token endet der Aufruf mit 400 `domain_not_configured`."}},"/api/v1/call-transcripts/transcribe":{"post":{"responses":{"400":{"description":"Kein Feld `file` im Upload","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung oder Klartext"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Upload nicht lesbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung oder Klartext"}},"required":["error"]}}}},"501":{"description":"Der einzige Ausgang bei gueltigem Upload — es gibt keinen Erfolgsfall","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_implemented"},"message":{"type":"string","description":"Klartext samt Vorgangsnummer, unter der die Umsetzung gefuehrt wird"}},"required":["error","message"]}}}}},"operationId":"postApiV1Call-transcriptsTranscribe","tags":["CRM","Calls"],"parameters":[],"summary":"Transcribe audio file (stub — Whisper integration pending)","description":"NOCH NICHT GEBAUT. Der Endpunkt nimmt zwar einen multipart-Upload im Feld `file` entgegen und lehnt einen fehlenden mit 400 ab, verarbeitet die Datei aber nicht: es gibt KEINEN Erfolgsfall. Jeder gueltige Aufruf endet mit 501 und der Kennung `not_implemented` — absichtlich, damit niemand ein erfundenes Transkript bekommt. Die Datei wird nicht gespeichert. Wer eine Zusammenfassung braucht, uebergibt einen fertigen Text an `POST /call-transcripts/summarize`."}},"/api/v1/call-transcripts/summarize":{"post":{"responses":{"200":{"description":"Zusammenfassung; `activityId` nur wenn eine Aktivitaet geschrieben wurde","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"string","description":"Die Zusammenfassung des Gespraechs"},"actionItems":{"type":"array","items":{"type":"string"},"description":"Abgeleitete Aufgaben"},"sentiment":{"type":"string","enum":["positive","neutral","negative"],"description":"Grundstimmung des Gespraechs"},"painPoints":{"type":"array","items":{"type":"string"},"description":"Genannte Probleme des Gespraechspartners"},"activityId":{"type":["string","null"],"description":"Kennung der angelegten CRM-Aktivitaet; null wenn keine geschrieben wurde — auch wenn das Schreiben fehlschlug"}},"required":["summary","actionItems","sentiment","painPoints","activityId"]},"example":{"summary":"string","actionItems":["string"],"sentiment":"positive","painPoints":["string"],"activityId":"string"}}}},"400":{"description":"Eingabe abgelehnt (z. B. Text zu kurz)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Die Zusammenfassung konnte nicht erzeugt werden","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Fehlerkennung oder Klartext"}},"required":["error"]}}}}},"operationId":"postApiV1Call-transcriptsSummarize","tags":["CRM","Calls"],"parameters":[],"summary":"Summarize a call transcript and persist as CRM activity","description":"Fasst einen FERTIGEN Gespraechstext zusammen (mindestens 100 Zeichen) und liefert Zusammenfassung, Aufgaben, Grundstimmung und Problempunkte. Wird zusaetzlich eine `customerId` mitgegeben und `saveAsActivity` nicht auf false gesetzt, entsteht daraus eine CRM-Aktivitaet vom Typ `call` am Kunden, bereits als erledigt gekennzeichnet; die noetige Tabelle wird dabei angelegt. OHNE `customerId` wird NICHTS gespeichert, auch wenn `saveAsActivity` true ist. Das Speichern ist nachrangig: scheitert es, kommt trotzdem 200 und `activityId` bleibt null. Es wird nicht geprueft, ob es den Kunden gibt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"transcript":{"type":"string","minLength":100},"language":{"type":"string","enum":["de","en"],"default":"de"},"customerId":{"type":"string","format":"uuid"},"saveAsActivity":{"type":"boolean","default":true}},"required":["transcript"]},"example":{"transcript":"stringxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","language":"de","customerId":"00000000-0000-4000-8000-000000000000","saveAsActivity":true}}}}}},"/api/v1/connectors":{"get":{"responses":{"200":{"description":"Alle eingebauten Konnektoren mit ihren Eckdaten.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"authType":{"type":"string"},"entities":{"type":"array","items":{"type":"string"}},"available":{"type":"boolean"},"driver":{"type":"string"}},"required":["slug","name","authType","entities","available"]}}},"required":["data"]},"example":{"data":[{"slug":"string","name":"string","authType":"string","entities":["string"],"available":true,"driver":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1Connectors","tags":["Konnektoren"],"parameters":[],"summary":"Verfuegbare Konnektoren auflisten","description":"Der KATALOG der eingebauten Konnektoren — eine feste Liste aus dem Quelltext, keine Datenbankabfrage und nichts Mandantenbezogenes. Jeder Mandant bekommt dieselbe Antwort.\n\nSie sagt NICHT, ob ein Konnektor eingerichtet ist; `available` meint „im Produkt vorhanden\", nicht „bei diesem Mandanten verbunden\". Den Zustand liefert `GET /{slug}/status`.\n\n`entities` nennt die Datenarten, die der Konnektor kennt — die Namen, die in `/{slug}/schema/{entity}` einzusetzen sind.\n\nAntwortet immer mit 200; es gibt keinen Fehlerfall.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten darf das. Gegatet sind nur `configure` und `sync` (`manager`)."}},"/api/v1/connectors/{slug}/status":{"get":{"responses":{"200":{"description":"Der Zustand — oder, bei `unavailable: true`, das ausdrueckliche „nicht ermittelbar\".","content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string"},"configured":{"type":["boolean","null"],"description":"Ob der Mandant den Konnektor eingerichtet hat. `null` = nicht ermittelbar (siehe `unavailable`), NICHT „nein\"."},"status":{"type":"string","description":"`\"unknown\"`, wenn der Zustand nicht gelesen werden konnte."},"lastSyncAt":{"type":["string","null"]},"lastError":{"type":["string","null"]},"unavailable":{"type":"boolean","const":true,"description":"Nur bei einem Datenbankfehler gesetzt. Ohne dieses Feld sind die uebrigen Werte gemessen."}},"required":["slug","configured","status","lastSyncAt","lastError"]},"example":{"slug":"string","configured":true,"status":"string","lastSyncAt":"string","lastError":"string","unavailable":true}}}},"401":{"description":"Kein Mandantenkontext (`tenant_required`, text/plain)."},"404":{"description":"Diesen Konnektor gibt es nicht (`connector_not_found`)."}},"operationId":"getApiV1ConnectorsBySlugStatus","tags":["Konnektoren"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Zustand eines Konnektors beim Mandanten","description":"Sagt, ob dieser Mandant den Konnektor eingerichtet hat, wann zuletzt abgeglichen wurde und ob dabei ein Fehler auftrat.\n\nEIN DATENBANKFEHLER KOMMT WEITERHIN ALS **200** ZURUECK, sagt es aber jetzt (geaendert 17.08.2026): `status: \"unknown\"`, `configured: null` und `unavailable: true`. Bis dahin antwortete er mit `configured: false, status: \"not_configured\"` — derselben Antwort wie fuer einen Mandanten, der den Konnektor nie eingerichtet hat. Die Route behauptete damit aktiv einen Geschaeftszustand, statt ihr Nichtwissen zuzugeben; wer darauf eine Einrichtungsaufforderung baut, zeigte sie auch bei einer Stoerung.\n\nDer Statuscode bleibt bewusst 200: eine einzelne Kachel ohne Zustand ist kein Seitenfehler. `unavailable` ist das Unterscheidungsmerkmal.\n\nEin UNBEKANNTER Konnektor ergibt dagegen einen echten 404, und ein fehlender Mandantenkontext einen 401 — beides als `text/plain`.\n\n`configured_at` wird aus der Datenbank gelesen, aber NICHT zurueckgegeben; wann eingerichtet wurde, sagt diese Route nicht.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten darf das. Gegatet sind nur `configure` und `sync` (`manager`)."}},"/api/v1/connectors/{slug}/configure":{"post":{"responses":{"201":{"description":"Zugangsdaten hinterlegt.","content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string"},"status":{"type":"string","const":"configured"}},"required":["slug","status"],"additionalProperties":false},"example":{"slug":"string","status":"configured"}}}},"400":{"description":"`credentials` war kein Objekt."},"401":{"description":"Kein Mandantenkontext (`tenant_required`)."},"403":{"description":"Rolle unter `manager`."},"404":{"description":"Diesen Konnektor gibt es nicht (`connector_not_found`)."},"503":{"description":"`database_unavailable` — nichts wurde hinterlegt. Oder, nur in Produktion, `field_encryption_unavailable`: der Schluessel fehlt, und Zugangsdaten werden dann bewusst NICHT im Klartext abgelegt."}},"operationId":"postApiV1ConnectorsBySlugConfigure","tags":["Konnektoren"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Zugangsdaten eines Konnektors hinterlegen","description":"Speichert die Zugangsdaten des Mandanten fuer einen Konnektor\n(Lexware, sevDesk, DATEV) in `public.tenant_integrations` und setzt\ndessen Zustand auf `configured`. Erst danach kann\n`POST /connectors/{slug}/sync` etwas holen.\n\nDIE ZUGANGSDATEN WERDEN VERSCHLUESSELT (seit 30.08.2026). Sie liegen\nAES-256-GCM-verschluesselt in `credentials_enc`, mit einem je Mandant\nabgeleiteten Schluessel und `connector:<slug>` als zusaetzlichem\nAuthentifizierungsdatum — eine in eine andere Konnektor-Zeile kopierte\nZeile ist damit unlesbar, nicht verwertbar.\n\nFEHLT `FIELD_ENCRYPTION_MASTER_KEY`, antwortet die Route in Produktion\nmit 503 `field_encryption_unavailable` und speichert NICHTS. Ausserhalb\nder Produktion wird gewarnt und im Klartext gespeichert, damit eine\nEntwicklungsumgebung ohne Schluessel benutzbar bleibt.\n\nALTBESTAND aus der Zeit davor bleibt lesbar und wandert beim naechsten\n`/configure` oder `/sync` von selbst herueber; es gibt keinen\nMigrationslauf, den jemand vergessen koennte.\n\nES WIRD NICHT GEPRUEFT, OB DIE DATEN STIMMEN. Es findet kein Testaufruf\nbeim fremden System statt; `status: configured` heisst „hinterlegt\",\nnicht „verbunden\". Ob sie tragen, zeigt sich erst beim ersten Abgleich.\nAuch der Inhalt ist frei: der Validator nimmt jedes Objekt an und kennt\nkeine Pflichtfelder je Konnektor.\n\nERNEUTES AUFRUFEN ERSETZT. Der Datensatz haengt an Mandant plus\nKonnektor; ein zweiter Aufruf ueberschreibt die alten Zugangsdaten\nvollstaendig und loescht einen vermerkten letzten Fehler. Ein leerer\nRumpf ersetzt sie durch ein leeres Objekt — das ist der einzige Weg,\nsie hier zu entfernen.\n\nErfordert `manager` oder hoeher. Der Mandant kommt aus der Sitzung.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"credentials":{"type":"object","additionalProperties":{},"default":{}}}},"example":{"credentials":{}}}}}}},"/api/v1/connectors/{slug}/sync":{"post":{"responses":{"202":{"description":"Durchgelaufen. `rows` geholt, `written` geschrieben, `failed` Datensaetze abgelehnt — die Arbeit ist bereits getan.","content":{"application/json":{"schema":{"type":"object","properties":{"runId":{"type":["string","null"]},"slug":{"type":"string"},"entity":{"type":"string"},"status":{"type":"string","const":"succeeded"},"rows":{"type":"integer"},"written":{"type":"integer"},"failed":{"type":"integer"},"targetTable":{"type":["string","null"]},"skipped":{"type":"string","description":"Nur gesetzt, wenn NICHTS geschrieben wurde — z. B. `no_schema_mapping`."},"errors":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"code":{"type":"string"},"message":{"type":"string"}},"required":["index","code","message"]},"description":"Nur gesetzt, wenn einzelne Datensaetze scheiterten."}},"required":["runId","slug","entity","status","rows","written","failed","targetTable"]},"example":{"runId":"string","slug":"string","entity":"string","status":"succeeded","rows":0,"written":0,"failed":0,"targetTable":"string","skipped":"string","errors":[{"index":0,"code":"string","message":"string"}]}}}},"400":{"description":"Kein `entity` — oder `entity_not_supported:<name>`: der Konnektor kennt diese Datenart nicht."},"401":{"description":"Kein Mandantenkontext (`tenant_required`)."},"403":{"description":"Rolle unter `manager`."},"404":{"description":"Diesen Konnektor gibt es nicht (`connector_not_found`)."},"502":{"description":"Abruf oder Abgleich gescheitert — `status: \"failed\"` und `error`. Der Lauf ist als `failed` vermerkt, der Konnektor steht auf `error`.","content":{"application/json":{"schema":{"type":"object","properties":{"runId":{"type":["string","null"]},"slug":{"type":"string"},"entity":{"type":"string"},"status":{"type":"string","const":"failed"},"error":{"type":"string"}},"required":["runId","slug","entity","status","error"]}}}},"503":{"description":"`credentials_unreadable` — die hinterlegten Zugangsdaten liessen sich nicht entschluesseln. Entweder fehlt `FIELD_ENCRYPTION_MASTER_KEY`, oder die Zeile gehoert zu einem anderen Mandanten bzw. Konnektor (das zusaetzliche Authentifizierungsdatum `connector:<slug>` schlaegt dann fehl). Der Chiffretext wird in diesem Fall NICHT an das Fremdsystem weitergereicht — er wuerde dort als API-Schluessel verschickt."}},"operationId":"postApiV1ConnectorsBySlugSync","tags":["Konnektoren"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Eine Datenart aus einem Fremdsystem uebernehmen","description":"Holt eine Datenart (`entity`) beim Konnektor ab, bildet sie ueber die\nhinterlegte Feldzuordnung auf die Nemix-Spalten ab und schreibt sie in\ndie Zieltabelle des Mandanten. Geschrieben wird aktualisierend ueber die\nFremdkennung beziehungsweise die Belegnummer — ein zweiter Lauf legt\ndieselben Datensaetze nicht noch einmal an.\n\nDER 202 IST EIN ETIKETT, KEINE WARTESCHLANGE. Der Aufruf laeuft\nvollstaendig durch, bevor er antwortet; `rows`, `written` und `failed`\nsind endgueltige Zahlen, kein Zwischenstand. Bei grossen Bestaenden\nentsprechend lange Laufzeit.\n\nZWEI ZAHLEN, DIE AUSEINANDERGEHEN DUERFEN: `rows` ist, was geholt wurde,\n`written`, was ankam. Einzelne gescheiterte Datensaetze stehen unter\n`errors` und brechen den Lauf nicht ab — er gilt trotzdem als\n`succeeded`. Wer wissen will, ob alles ankam, vergleicht die beiden\nZahlen, statt auf den Status zu sehen.\n\n`written: 0` MIT `skipped` HEISST: NICHTS GESCHRIEBEN. `no_schema_mapping`\nbedeutet, dass es fuer diese Datenart keine Feldzuordnung gibt — geholt\nwurde dann zwar, gespeichert nichts. Kontaktartige Datenarten werden\nausdruecklich uebersprungen und nie nach crm/contacts geschrieben.\n\nFuer DATEV (dateibasiert) kann der Inhalt als `csvContent` im Rumpf\nmitgegeben werden.\n\nJeder Lauf legt eine Zeile in `public.migration_runs` an und wird dort\nabgeschlossen; `runId` verweist darauf und ist `null`, wenn die Tabelle\nfehlt. Danach werden `last_sync_at`, `last_error` und der Zustand des\nKonnektors fortgeschrieben — auch im Fehlerfall, dann auf `error`.\n\nErfordert `manager` oder hoeher. Zugangsdaten muessen vorher ueber\n`POST /connectors/{slug}/configure` hinterlegt sein; fehlen sie, laeuft\nder Abruf mit einem leeren Objekt los und scheitert am Fremdsystem\n(502).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity":{"type":"string","minLength":1},"full":{"type":"boolean"},"csvContent":{"type":"string"}},"required":["entity"]},"example":{"entity":"string","full":true,"csvContent":"string"}}}}}},"/api/v1/connectors/{slug}/schema/{entity}":{"get":{"responses":{"200":{"description":"Die Feldpaare Quelle → Ziel.","content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string"},"entity":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"sourceField":{"type":"string"},"targetField":{"type":"string"}},"required":["sourceField","targetField"]}}},"required":["slug","entity","fields"]},"example":{"slug":"string","entity":"string","fields":[{"sourceField":"string","targetField":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Konnektor unbekannt ODER fuer diese Datenart gibt es keine Abbildung — die Meldung unterscheidet die beiden Faelle."}},"operationId":"getApiV1ConnectorsBySlugSchemaByEntity","tags":["Konnektoren"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"entity","required":true}],"summary":"Feldabbildung eines Konnektors fuer eine Datenart","description":"Zeigt, welches Feld des Fremdsystems auf welches Feld in Nemix abgebildet wird — die Uebersetzungstabelle des Abgleichs.\n\nWie der Katalog steht auch diese Abbildung FEST IM QUELLTEXT: keine Datenbank, nichts Mandantenbezogenes, kein Zustand. Jeder Mandant bekommt dieselbe Antwort, und sie aendert sich nur mit einer neuen Programmfassung.\n\nDie gueltigen Werte fuer `{entity}` nennt `entities` in der Konnektorliste. Ein unbekannter Konnektor ergibt `connector_not_found`, eine unbekannte Datenart `schema_not_found:{slug}/{entity}` — beide 404 als `text/plain`, unterscheidbar an der Meldung.\n\nDie Reihenfolge der Felder folgt der Abbildung im Quelltext und ist keine Zusage.\n\nKeine Rollenpruefung: jeder angemeldete Benutzer des Mandanten darf das. Gegatet sind nur `configure` und `sync` (`manager`)."}},"/api/v1/compliance/verfahrensdoku.pdf":{"get":{"responses":{"200":{"description":"Die Verfahrensdokumentation als Datei, immer als Anhang (`Content-Disposition: attachment`, `Cache-Control: no-store`). Regelfall ist das PDF; schlaegt der PDF-Satz fehl, kommt derselbe Inhalt als HTML in UTF-8 zurueck — ebenfalls mit 200. Welcher Weg gelaufen ist, steht im Kopf `Content-Type`.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"text/html":{"schema":{"type":"string"}}}},"401":{"description":"Nicht authentifiziert"},"503":{"description":"Datenbank nicht verfügbar"}},"operationId":"getApiV1ComplianceVerfahrensdoku.pdf","tags":["compliance"],"parameters":[],"summary":"Erzeugt die GoBD-Verfahrensdokumentation als PDF","description":"Generiert die GoBD-Verfahrensdokumentation gemäß IDW PS 880 als PDF (oder HTML-Fallback). Audit-Log-Eintrag wird erzeugt."}},"/api/v1/prozess-charts":{"get":{"responses":{"200":{"description":"Die Charts des Satzes, je mit Kennung, Titel und Dateiliste.","content":{"application/json":{"schema":{"type":"object","properties":{"charts":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"Kennung, eindeutig innerhalb eines Mandantensatzes"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"version":{"type":"string"},"status":{"type":"string","enum":["entwurf","review","freigegeben","archiviert"]},"verantwortlich":{"type":["string","null"]},"erstelltAm":{"type":["string","null"],"description":"ISO-Datum"},"geaendertAm":{"type":["string","null"],"description":"ISO-Datum"},"artefakte":{"type":"array","items":{"type":"string","enum":["bpmn","dmn","form"]},"description":"Welche Artefakttypen vorhanden sind — abgeleitet aus den Dateipfaden"}},"required":["slug","name","beschreibung","kategorie","tags","version","status","verantwortlich","erstelltAm","geaendertAm","artefakte"]}}},"required":["charts"]},"example":{"charts":[{"slug":"string","name":"string","beschreibung":"string","kategorie":"string","tags":["string"],"version":"string","status":"entwurf","verantwortlich":"string","erstelltAm":"string","geaendertAm":"string","artefakte":["bpmn"]}]}}}},"400":{"description":"Mandantennummer nicht sechsstellig (`invalid_tenant_number`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"401":{"description":"Nicht angemeldet (`not_authenticated`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"404":{"description":"Satz existiert nicht — oder gehört einem anderen Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}}},"operationId":"getApiV1Prozess-charts","tags":["prozess-charts"],"parameters":[{"name":"mandant","in":"query","required":false,"description":"Mandantennummer des Satzes, sechsstellig. Vorgabe ist `000000` — das Referenzmodell, der zentrale Standard. Ein fremder Satz antwortet mit 404, nicht 403: die Antwort soll nicht verraten, welche Mandantennummern vergeben sind.","schema":{"type":"string","pattern":"^[0-9]{6}$","default":"000000"}}],"summary":"Prozess-Charts eines Satzes auflisten","description":"Listet die Prozess-Charts eines Mandantensatzes oder des Referenzmodells. Lesen steht jedem Angemeldeten offen, das Referenzmodell ist Wissen und kein Geheimnis; ein fremder Mandantensatz jedoch nicht."},"post":{"responses":{"201":{"description":"Angelegt, mit dem erzeugten Chart in der Antwort.","content":{"application/json":{"schema":{"type":"object","properties":{"chart":{"type":"object","properties":{"slug":{"type":"string","description":"Kennung, eindeutig innerhalb eines Mandantensatzes"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"version":{"type":"string"},"status":{"type":"string","enum":["entwurf","review","freigegeben","archiviert"]},"verantwortlich":{"type":["string","null"]},"erstelltAm":{"type":["string","null"],"description":"ISO-Datum"},"geaendertAm":{"type":["string","null"],"description":"ISO-Datum"},"artefakte":{"type":"array","items":{"type":"string","enum":["bpmn","dmn","form"]},"description":"Welche Artefakttypen vorhanden sind — abgeleitet aus den Dateipfaden"}},"required":["slug","name","beschreibung","kategorie","tags","version","status","verantwortlich","erstelltAm","geaendertAm","artefakte"]}},"required":["chart"]},"example":{"chart":{"slug":"string","name":"string","beschreibung":"string","kategorie":"string","tags":["string"],"version":"string","status":"entwurf","verantwortlich":"string","erstelltAm":"string","geaendertAm":"string","artefakte":["bpmn"]}}}}},"400":{"description":"Kennung oder Name fehlt (`name_required`), `dateien` fehlt oder ist leer (`files_required`), oder ein Dateiinhalt ist kein Text (`file_not_text`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"401":{"description":"Nicht angemeldet (`not_authenticated`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"403":{"description":"Referenzmodell, aber nicht `super_admin` (`reference_model_readonly`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"409":{"description":"Diese Kennung ist im Satz schon vergeben (`chart_exists`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}}},"operationId":"postApiV1Prozess-charts","tags":["prozess-charts"],"parameters":[{"name":"mandant","in":"query","required":false,"description":"Mandantennummer des Satzes, sechsstellig. Vorgabe ist `000000` — das Referenzmodell, der zentrale Standard. Ein fremder Satz antwortet mit 404, nicht 403: die Antwort soll nicht verraten, welche Mandantennummern vergeben sind.","schema":{"type":"string","pattern":"^[0-9]{6}$","default":"000000"}}],"summary":"Prozess-Chart anlegen","description":"Legt ein Chart in einem Mandantensatz oder im Referenzmodell an. SCHREIBEN IST ENGER ALS LESEN: das Referenzmodell (`000000`) ändert nur der Systemadministrator (`super_admin`), den eigenen Satz ändern `admin` und `owner` dieses Mandanten. Ein fremder Satz antwortet 404, nicht 403."}},"/api/v1/prozess-charts/{slug}":{"get":{"responses":{"200":{"description":"Das Chart mit Kennung, Titel und Dateipfaden.","content":{"application/json":{"schema":{"type":"object","properties":{"chart":{"type":"object","properties":{"slug":{"type":"string","description":"Kennung, eindeutig innerhalb eines Mandantensatzes"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"version":{"type":"string"},"status":{"type":"string","enum":["entwurf","review","freigegeben","archiviert"]},"verantwortlich":{"type":["string","null"]},"erstelltAm":{"type":["string","null"],"description":"ISO-Datum"},"geaendertAm":{"type":["string","null"],"description":"ISO-Datum"},"artefakte":{"type":"array","items":{"type":"string","enum":["bpmn","dmn","form"]},"description":"Welche Artefakttypen vorhanden sind — abgeleitet aus den Dateipfaden"}},"required":["slug","name","beschreibung","kategorie","tags","version","status","verantwortlich","erstelltAm","geaendertAm","artefakte"]}},"required":["chart"]},"example":{"chart":{"slug":"string","name":"string","beschreibung":"string","kategorie":"string","tags":["string"],"version":"string","status":"entwurf","verantwortlich":"string","erstelltAm":"string","geaendertAm":"string","artefakte":["bpmn"]}}}}},"400":{"description":"Kennung oder Mandantennummer verletzt das Format.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"401":{"description":"Nicht angemeldet (`not_authenticated`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"404":{"description":"Chart nicht vorhanden — oder in einem fremden Satz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}}},"operationId":"getApiV1Prozess-chartsBySlug","tags":["prozess-charts"],"parameters":[{"name":"mandant","in":"query","required":false,"description":"Mandantennummer des Satzes, sechsstellig. Vorgabe ist `000000` — das Referenzmodell, der zentrale Standard. Ein fremder Satz antwortet mit 404, nicht 403: die Antwort soll nicht verraten, welche Mandantennummern vergeben sind.","schema":{"type":"string","pattern":"^[0-9]{6}$","default":"000000"}},{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Ein Prozess-Chart lesen","description":"Liefert ein einzelnes Chart samt seiner Dateiliste. Die Kennung (`slug`) ist innerhalb eines Satzes eindeutig, nicht darüber hinaus: dasselbe Chart trägt im Referenzmodell und in der Mandantenkopie dieselbe Kennung."},"patch":{"responses":{"200":{"description":"Das geänderte Chart. Ein verworfener Status ist hier sichtbar.","content":{"application/json":{"schema":{"type":"object","properties":{"chart":{"type":"object","properties":{"slug":{"type":"string","description":"Kennung, eindeutig innerhalb eines Mandantensatzes"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"version":{"type":"string"},"status":{"type":"string","enum":["entwurf","review","freigegeben","archiviert"]},"verantwortlich":{"type":["string","null"]},"erstelltAm":{"type":["string","null"],"description":"ISO-Datum"},"geaendertAm":{"type":["string","null"],"description":"ISO-Datum"},"artefakte":{"type":"array","items":{"type":"string","enum":["bpmn","dmn","form"]},"description":"Welche Artefakttypen vorhanden sind — abgeleitet aus den Dateipfaden"}},"required":["slug","name","beschreibung","kategorie","tags","version","status","verantwortlich","erstelltAm","geaendertAm","artefakte"]}},"required":["chart"]},"example":{"chart":{"slug":"string","name":"string","beschreibung":"string","kategorie":"string","tags":["string"],"version":"string","status":"entwurf","verantwortlich":"string","erstelltAm":"string","geaendertAm":"string","artefakte":["bpmn"]}}}}},"400":{"description":"Kennung oder Mandantennummer verletzt das Format.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"401":{"description":"Nicht angemeldet (`not_authenticated`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"403":{"description":"Referenzmodell, aber nicht `super_admin` (`reference_model_readonly`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"404":{"description":"Chart nicht vorhanden — oder in einem fremden Satz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}}},"operationId":"patchApiV1Prozess-chartsBySlug","tags":["prozess-charts"],"parameters":[{"name":"mandant","in":"query","required":false,"description":"Mandantennummer des Satzes, sechsstellig. Vorgabe ist `000000` — das Referenzmodell, der zentrale Standard. Ein fremder Satz antwortet mit 404, nicht 403: die Antwort soll nicht verraten, welche Mandantennummern vergeben sind.","schema":{"type":"string","pattern":"^[0-9]{6}$","default":"000000"}},{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Angaben eines Charts ändern","description":"Ändert Name, Beschreibung, Kategorie, Schlagworte, Version, Status und Verantwortlichen. Nicht mitgeschickte Felder bleiben stehen. ACHTUNG: ein Status ausserhalb der erlaubten Werte wird STILLSCHWEIGEND verworfen — der Aufruf antwortet 200 mit dem alten Status, nicht 400. Wer den Status setzt, muss ihn in der Antwort gegenprüfen. Rechte wie beim Anlegen: Referenzmodell nur `super_admin`, eigener Satz `admin` und `owner`."},"delete":{"responses":{"200":{"description":"Gelöscht.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"400":{"description":"Kennung oder Mandantennummer verletzt das Format.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"401":{"description":"Nicht angemeldet (`not_authenticated`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"403":{"description":"Referenzmodell, aber nicht `super_admin` (`reference_model_readonly`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"404":{"description":"Chart nicht vorhanden (`chart_not_found`) — oder in einem fremden Satz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}}},"operationId":"deleteApiV1Prozess-chartsBySlug","tags":["prozess-charts"],"parameters":[{"name":"mandant","in":"query","required":false,"description":"Mandantennummer des Satzes, sechsstellig. Vorgabe ist `000000` — das Referenzmodell, der zentrale Standard. Ein fremder Satz antwortet mit 404, nicht 403: die Antwort soll nicht verraten, welche Mandantennummern vergeben sind.","schema":{"type":"string","pattern":"^[0-9]{6}$","default":"000000"}},{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Ein Prozess-Chart löschen","description":"Löscht das Chart mit allen seinen Dateien. Endgültig, kein Papierkorb. Löscht man es im Referenzmodell, sind bereits verteilte Mandantenkopien NICHT betroffen — jede Kopie steht unter der Nummer ihres Mandanten für sich. Rechte wie beim Anlegen: Referenzmodell nur `super_admin`, eigener Satz `admin` und `owner`."}},"/api/v1/prozess-charts/{slug}/dateien":{"get":{"responses":{"200":{"description":"Die Pfade als Liste. Der Inhalt kommt einzeln über den Pfad-Aufruf.","content":{"application/json":{"schema":{"type":"object","properties":{"pfade":{"type":"array","items":{"type":"string"},"description":"Dateipfade des Charts, relativ zu seinem Verzeichnis"}},"required":["pfade"]},"example":{"pfade":["string"]}}}},"400":{"description":"Kennung oder Mandantennummer verletzt das Format.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"401":{"description":"Nicht angemeldet (`not_authenticated`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"404":{"description":"Chart nicht vorhanden — oder in einem fremden Satz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}}},"operationId":"getApiV1Prozess-chartsBySlugDateien","tags":["prozess-charts"],"parameters":[{"name":"mandant","in":"query","required":false,"description":"Mandantennummer des Satzes, sechsstellig. Vorgabe ist `000000` — das Referenzmodell, der zentrale Standard. Ein fremder Satz antwortet mit 404, nicht 403: die Antwort soll nicht verraten, welche Mandantennummern vergeben sind.","schema":{"type":"string","pattern":"^[0-9]{6}$","default":"000000"}},{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"summary":"Dateipfade eines Charts auflisten","description":"Nennt jeden Dateipfad, der zu einem Chart gehört — das BPMN-Modell und die Begleitdateien, die mitwandern, wenn ein Mandant eine Kopie erhält. Der Übernahmelauf liest genau diese Liste."}},"/api/v1/prozess-charts/{slug}/dateien/{pfad}":{"get":{"responses":{"200":{"description":"Der Dateiinhalt als Text, OHNE JSON-Hülle.","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Pfad, Kennung oder Mandantennummer verletzt das Format.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"401":{"description":"Nicht angemeldet (`not_authenticated`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"404":{"description":"Datei oder Chart nicht vorhanden — oder in einem fremden Satz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}}},"operationId":"getApiV1Prozess-chartsBySlugDateienByPfad","tags":["prozess-charts"],"parameters":[{"name":"mandant","in":"query","required":false,"description":"Mandantennummer des Satzes, sechsstellig. Vorgabe ist `000000` — das Referenzmodell, der zentrale Standard. Ein fremder Satz antwortet mit 404, nicht 403: die Antwort soll nicht verraten, welche Mandantennummern vergeben sind.","schema":{"type":"string","pattern":"^[0-9]{6}$","default":"000000"}},{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"pfad","required":true}],"summary":"Den Inhalt einer Chart-Datei lesen","description":"Gibt den Inhalt einer einzelnen Datei als Text zurück, OHNE JSON-Hülle: ein BPMN-Modell von 37 KB durch JSON zu schicken hieße, es zweimal zu kodieren. Wer den Rückgabewert als JSON zu lesen versucht, scheitert deshalb zu Recht. Der Pfad darf Schrägstriche enthalten und wird gegen Ausbruch aus dem Chart-Verzeichnis geprüft."},"put":{"responses":{"200":{"description":"Geschrieben, mit dem Chart und seiner aktualisierten Dateiliste.","content":{"application/json":{"schema":{"type":"object","properties":{"chart":{"type":"object","properties":{"slug":{"type":"string","description":"Kennung, eindeutig innerhalb eines Mandantensatzes"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"version":{"type":"string"},"status":{"type":"string","enum":["entwurf","review","freigegeben","archiviert"]},"verantwortlich":{"type":["string","null"]},"erstelltAm":{"type":["string","null"],"description":"ISO-Datum"},"geaendertAm":{"type":["string","null"],"description":"ISO-Datum"},"artefakte":{"type":"array","items":{"type":"string","enum":["bpmn","dmn","form"]},"description":"Welche Artefakttypen vorhanden sind — abgeleitet aus den Dateipfaden"}},"required":["slug","name","beschreibung","kategorie","tags","version","status","verantwortlich","erstelltAm","geaendertAm","artefakte"]}},"required":["chart"]},"example":{"chart":{"slug":"string","name":"string","beschreibung":"string","kategorie":"string","tags":["string"],"version":"string","status":"entwurf","verantwortlich":"string","erstelltAm":"string","geaendertAm":"string","artefakte":["bpmn"]}}}}},"400":{"description":"Pfad, Kennung oder Mandantennummer verletzt das Format.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"401":{"description":"Nicht angemeldet (`not_authenticated`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"403":{"description":"Referenzmodell, aber nicht `super_admin` (`reference_model_readonly`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"404":{"description":"Chart nicht vorhanden — oder in einem fremden Satz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}}},"operationId":"putApiV1Prozess-chartsBySlugDateienByPfad","tags":["prozess-charts"],"parameters":[{"name":"mandant","in":"query","required":false,"description":"Mandantennummer des Satzes, sechsstellig. Vorgabe ist `000000` — das Referenzmodell, der zentrale Standard. Ein fremder Satz antwortet mit 404, nicht 403: die Antwort soll nicht verraten, welche Mandantennummern vergeben sind.","schema":{"type":"string","pattern":"^[0-9]{6}$","default":"000000"}},{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"pfad","required":true}],"summary":"Eine Chart-Datei schreiben","description":"Schreibt eine Datei. Der Rumpf der Anfrage IST der Inhalt, unverändert und ohne JSON-Hülle — dieselbe Begründung wie beim Lesen. Eine vorhandene Datei wird ersetzt, eine neue angelegt. Rechte wie beim Anlegen: Referenzmodell nur `super_admin`, eigener Satz `admin` und `owner`."},"delete":{"responses":{"200":{"description":"Gelöscht, mit dem Chart und seiner verbliebenen Dateiliste.","content":{"application/json":{"schema":{"type":"object","properties":{"chart":{"type":"object","properties":{"slug":{"type":"string","description":"Kennung, eindeutig innerhalb eines Mandantensatzes"},"name":{"type":"string"},"beschreibung":{"type":["string","null"]},"kategorie":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"version":{"type":"string"},"status":{"type":"string","enum":["entwurf","review","freigegeben","archiviert"]},"verantwortlich":{"type":["string","null"]},"erstelltAm":{"type":["string","null"],"description":"ISO-Datum"},"geaendertAm":{"type":["string","null"],"description":"ISO-Datum"},"artefakte":{"type":"array","items":{"type":"string","enum":["bpmn","dmn","form"]},"description":"Welche Artefakttypen vorhanden sind — abgeleitet aus den Dateipfaden"}},"required":["slug","name","beschreibung","kategorie","tags","version","status","verantwortlich","erstelltAm","geaendertAm","artefakte"]}},"required":["chart"]},"example":{"chart":{"slug":"string","name":"string","beschreibung":"string","kategorie":"string","tags":["string"],"version":"string","status":"entwurf","verantwortlich":"string","erstelltAm":"string","geaendertAm":"string","artefakte":["bpmn"]}}}}},"400":{"description":"Pfad, Kennung oder Mandantennummer verletzt das Format.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"401":{"description":"Nicht angemeldet (`not_authenticated`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"403":{"description":"Referenzmodell, aber nicht `super_admin` (`reference_model_readonly`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"404":{"description":"Datei oder Chart nicht vorhanden — oder in einem fremden Satz.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}},"409":{"description":"Es ist das letzte Artefakt des Charts (`last_artifact`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbarer Code, z. B. `chart_not_found`"},"message":{"type":"string","description":"Englischer Klartext. Die deutsche Anzeige entsteht in apps/web."}},"required":["error","message"]}}}}},"operationId":"deleteApiV1Prozess-chartsBySlugDateienByPfad","tags":["prozess-charts"],"parameters":[{"name":"mandant","in":"query","required":false,"description":"Mandantennummer des Satzes, sechsstellig. Vorgabe ist `000000` — das Referenzmodell, der zentrale Standard. Ein fremder Satz antwortet mit 404, nicht 403: die Antwort soll nicht verraten, welche Mandantennummern vergeben sind.","schema":{"type":"string","pattern":"^[0-9]{6}$","default":"000000"}},{"schema":{"type":"string"},"in":"path","name":"slug","required":true},{"schema":{"type":"string"},"in":"path","name":"pfad","required":true}],"summary":"Eine Chart-Datei löschen","description":"Löscht eine einzelne Datei des Charts. Das LETZTE Artefakt lässt sich nicht löschen (`last_artifact`, 409): ein Chart ohne Modell stünde sonst in der Liste, ohne eines zu sein. Rechte wie beim Anlegen: Referenzmodell nur `super_admin`, eigener Satz `admin` und `owner`."}},"/api/v1/admin/tenant-routes":{"get":{"responses":{"200":{"description":"Liste der Routen, neueste zuerst. Auch die Antwort ohne Datenbankverbindung — dann leer.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"method":{"type":"string"},"path":{"type":"string"},"handler_config_jsonb":{},"status":{"type":"string"},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","method","path","status","created_by","created_at","updated_at"]}}},"required":["items"]},"example":{"items":[{"id":"string","tenant_id":"string","method":"string","path":"string","status":"string","created_by":"string","created_at":"string","updated_at":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"500":{"description":"Abfrage fehlgeschlagen — `error: \"query_failed\"` plus Meldung.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}}},"operationId":"getApiV1AdminTenant-routes","tags":["admin"],"parameters":[],"summary":"Dynamische Mandanten-Routen auflisten","description":"Listet die dynamisch registrierten API-Routen des angemeldeten Mandanten.\n\n`?all=true` zeigt die Routen ALLER Mandanten — nur fuer `super_admin`; fuer alle anderen wird der Schalter still ignoriert und die eigene Liste geliefert. `?include_deleted=true` nimmt auch `status = deleted` auf; ohne den Schalter erscheinen nur aktive.\n\nOHNE DATENBANK ANTWORTET DIESE ROUTE MIT 200 UND `items: []` — eine leere Liste heisst hier also „keine Routen ODER keine Verbindung\". Wer den Unterschied braucht, prueft `/health/ready`. Ein Fehler WAEHREND der Abfrage kommt dagegen als 500 heraus.\n\nEs gibt keine Blaetterung: die Abfrage liefert alle Zeilen."},"post":{"responses":{"201":{"description":"Route angelegt ODER ueberschrieben. `id` ist die Kennung der Zeile.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":["string","null"]}},"required":["id"]},"example":{"id":"string"}}}},"400":{"description":"Rumpf ungueltig — `path` oder `handler` passen nicht."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"500":{"description":"Schreiben fehlgeschlagen — `error: \"insert_failed\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1AdminTenant-routes","tags":["admin"],"parameters":[],"summary":"Dynamische Route registrieren oder ueberschreiben","description":"DER STATUS 201 IST HIER NICHT WOERTLICH ZU NEHMEN. Das Statement traegt `ON CONFLICT (tenant_id, method, path) DO UPDATE` — gibt es die Kombination aus Methode und Pfad schon, wird die bestehende Route UEBERSCHRIEBEN und trotzdem 201 gemeldet. Die Antwort sagt nicht, welcher der beiden Faelle eintrat; die Kennung ist in beiden dieselbe. Wer „nur neu anlegen\" braucht, fragt vorher die Liste ab.\n\nEine geloeschte Route wird durch dieselbe Anmeldung wieder `active` — das ist der zweite Weg zurueck neben `/{id}/restore`.\n\n`path` muss der Form `/custom/<entity>` folgen; `handler.entity` ist auf `[a-z][a-z0-9_]{0,59}` begrenzt. Beides lehnt der Validator mit 400 ab.\n\nDer Vorgang wird protokolliert (`tenant_route_created`), weil er einem Schema-Eingriff nahekommt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"method":{"type":"string","enum":["GET","POST","PUT","PATCH","DELETE"]},"path":{"type":"string"},"handler":{"type":"object","properties":{"entity":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,59}$"},"fields":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["string","number","boolean","date","json"]},"required":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"}},"required":["name","type"]}},"filters":{"type":"array","items":{"type":"object","properties":{"column":{"type":"string"},"op":{"type":"string","enum":["eq","gt","lt","gte","lte","like"]}},"required":["column","op"]}},"auth_role":{"type":"string","enum":["user","manager","admin"]},"audit":{"type":"boolean"}},"required":["entity","fields"]}},"required":["method","path","handler"]}}}}}},"/api/v1/admin/tenant-routes/{id}":{"delete":{"responses":{"200":{"description":"Abgeschaltet. Der Cache aller Instanzen wird benachrichtigt.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"404":{"description":"Nicht gefunden ODER fremder Mandant — `error: \"not_found_or_forbidden\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Schreiben fehlgeschlagen — `error: \"delete_failed\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"deleteApiV1AdminTenant-routesById","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Dynamische Route abschalten (weich)","description":"Setzt `status = deleted`. Die Zeile bleibt stehen und ist ueber `POST /{id}/restore` wieder einzuschalten — es wird nichts geloescht.\n\nDer Zugriff endet an der eigenen Mandantengrenze; nur `super_admin` erreicht fremde Routen. Der 404 deckt deshalb ZWEI Faelle ab, die von aussen nicht zu unterscheiden sind: „gibt es nicht\" und „gehoert einem anderen Mandanten\". Das ist Absicht — die Unterscheidung waere selbst schon eine Auskunft ueber fremde Bestaende.\n\nEine bereits abgeschaltete Route erneut abzuschalten gelingt und meldet 200; es gibt keine Pruefung auf den Vorzustand."}},"/api/v1/admin/tenant-routes/{id}/restore":{"post":{"responses":{"200":{"description":"Wieder aktiv. Der Cache aller Instanzen wird benachrichtigt.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["ok","id"]},"example":{"ok":true,"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `manager`."},"404":{"description":"Nicht gefunden ODER fremder Mandant — `error: \"not_found_or_forbidden\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Schreiben fehlgeschlagen — `error: \"restore_failed\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"postApiV1AdminTenant-routesByIdRestore","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Abgeschaltete Route wieder einschalten","description":"Setzt `status = active`. Gegenstueck zum weichen Abschalten, mit denselben Grenzen: eigener Mandant, `super_admin` auch fremde, und der 404 steht wieder fuer beide Faelle.\n\nDer Pfad liegt eine Ebene unter `/{id}` und wird von ihm nicht verdeckt.\n\nEine bereits aktive Route erneut einzuschalten gelingt und meldet 200."}},"/api/v1/admin/test-email":{"post":{"responses":{"200":{"description":"Mail dispatched (check inbox).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true,"description":"Im 200-Fall immer true; der Anbieter hat die Mail angenommen"},"from":{"type":"string","description":"Die tatsaechlich verwendete Absenderadresse nach der Aufloesung explizit > Mandanten-Override > Vorgabe. Bei mandanteneigenem SMTP steht hier \"(tenant)\"."},"to":{"type":"string","description":"Der Empfaenger — aus dem Rumpf, sonst die E-Mail des Aufrufers"},"provider":{"type":"string","enum":["ses","resend","console"],"description":"Der genutzte Transport; mandanteneigenes SMTP wird als \"ses\" gefuehrt"},"sentAt":{"type":"string","description":"Zeitpunkt des Versands als ISO-8601"}},"required":["ok","from","to","provider","sentAt"]},"example":{"ok":true,"from":"string","to":"string","provider":"ses","sentAt":"string"}}}},"400":{"description":"Invalid recipient."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Transport-level failure (see body.error)."}},"operationId":"postApiV1AdminTest-email","tags":["admin","email"],"parameters":[],"description":"Send a test email through the live mail transport. Returns the resolved From-Address and provider so operators can validate Wave V2 (noreply@mail.nemix-erp.de) wiring.","summary":"Send a test email through the live mail transport","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/admin/email-debug":{"get":{"responses":{"200":{"description":"Diagnose. `versandSimuliert: true` heisst: es geht nichts hinaus.","content":{"application/json":{"schema":{"type":"object","properties":{"timestamp":{"type":"string"},"tenantId":{"type":["string","null"]},"userEmail":{"type":["string","null"]},"env":{"type":"object","additionalProperties":{"type":"string"}},"sdk":{},"summary":{"type":"object","properties":{"emailClientWillUse":{"type":"string"},"versandSimuliert":{"type":"boolean"},"providerAusEnv":{"type":"string"},"hint":{"type":"string"},"mailerWillUse":{"type":"string"}},"required":["emailClientWillUse","versandSimuliert","providerAusEnv","hint","mailerWillUse"]}},"required":["timestamp","tenantId","userEmail","env","summary"]},"example":{"timestamp":"string","tenantId":"string","userEmail":"string","env":{"beispiel":"string"},"summary":{"emailClientWillUse":"string","versandSimuliert":true,"providerAusEnv":"string","hint":"string","mailerWillUse":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin`."}},"operationId":"getApiV1AdminEmail-debug","tags":["admin"],"parameters":[],"summary":"Mail-Versand diagnostizieren","description":"Sagt, WARUM keine Mail ankommt — Umgebung, erkannter Anbieter, SDK-Verfuegbarkeit.\n\nDIE WICHTIGSTE ZEILE IST `summary.versandSimuliert`. Steht sie auf `true`, meldet der Versand ueberall Erfolg samt Kennung, und es geht trotzdem NICHTS hinaus. Das ist auf dev der Normalfall. `summary.providerAusEnv` sagt daneben, was in der Umgebung steht — bei simuliertem Versand ist das bewusst uebersteuert. Die beiden Felder auseinanderzuhalten ist der ganze Zweck dieser Route: die frueheren Fassungen lasen nur die Umgebung und meldeten „ses\", waehrend der Mock lief.\n\nWerte unter `env` sind maskiert (`abcd…yz (len=40)`) oder auf `(set)`/`(unset)` reduziert. Kein Geheimnis verlaesst diese Route.\n\nReine Auskunft — es wird nichts versendet und nichts veraendert."}},"/api/v1/admin/email-debug/probe":{"post":{"responses":{"200":{"description":"Zugestellt an die Schicht. ZWEI FORMEN: `ses-direct` mit `ok` und `messageId`, die beiden anderen Wege mit `result`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"path":{"type":"string"},"to":{"type":"string"},"from":{"type":"string"},"region":{"type":"string"},"ok":{"type":"boolean","const":true},"messageId":{"type":["string","null"]}},"required":["path","to","from","region","ok","messageId"]},{"type":"object","properties":{"path":{"type":"string"},"to":{"type":"string"},"result":{}},"required":["path","to"]}]},"example":{"path":"string","to":"string","from":"string","region":"string","ok":true,"messageId":"string"}}}},"400":{"description":"Empfaenger ist keine Adresse — `error: \"invalid_recipient\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","const":"invalid_recipient"},"to":{"type":"string"}},"required":["ok","error","to"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin`."},"500":{"description":"SES hat abgelehnt (`error` mit `name`, `message`, `code`, `metadata` — die ROHE Antwort, das ist der Zweck) oder das SDK fehlt (`require_unavailable`, `aws-sdk_sesv2_not_installed`).","content":{"application/json":{"schema":{"type":"object","properties":{"path":{"type":"string"},"to":{"type":"string"},"ok":{"type":"boolean","const":false},"error":{"anyOf":[{"type":"string"},{"type":"object","additionalProperties":{}}]}},"required":["path","to","ok","error"]}}}}},"operationId":"postApiV1AdminEmail-debugProbe","tags":["admin"],"parameters":[],"summary":"Testmail ueber einen bestimmten Weg verschicken","description":"Verschickt WIRKLICH eine Mail — auf einer Umgebung mit echtem Versand geht sie hinaus. Der Text ist fest vorgegeben (Betreff „Nemix Debug-Probe\" plus Zeitstempel); es laesst sich kein eigener Inhalt einschleusen. Die EMPFAENGERADRESSE ist dagegen frei: ein `to` im Rumpf geht an eine beliebige Adresse, ohne `to` an die des Aufrufers. Die Rolle `admin` ist die einzige Schranke davor.\n\n`path` waehlt die Schicht: `ses-direct` (Standard) umgeht Mailer und Client und spricht SES an — genau dafuer ist die Route da, denn nur so trennt sich „SES lehnt ab\" von „unsere Schicht verschluckt es\". `mailer` und `email-client` gehen den normalen Weg. Ein unbekannter Wert faellt still auf `ses-direct` zurueck; einen 400 gibt es dafuer nicht.\n\nDIE DREI WEGE ANTWORTEN NICHT GLEICH. Nur `ses-direct` liefert `ok` und `messageId`. `mailer` und `email-client` geben stattdessen `result` heraus — was immer die jeweilige Schicht zurueckgibt, ungeprueft durchgereicht. Wer auf `ok` prueft, bekommt dort `undefined`, nicht `false`.\n\nMeldet die Diagnose `versandSimuliert: true`, meldet auch diese Route Erfolg samt Kennung, ohne dass etwas hinausgeht."}},"/api/v1/admin/email-debug/test-domain":{"get":{"responses":{"200":{"description":"Befund zur Domaene. `ok: false` nennt den Grund im Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"domain":{"type":"string"},"region":{"type":"string"},"error":{"type":"string"},"hint":{"type":"string"}},"required":["ok","domain","region"]},"example":{"ok":true,"domain":"string","region":"string","error":"string","hint":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin`."},"500":{"description":"Die Pruefung selbst kam nicht zustande — SDK fehlt, `require` nicht verfuegbar, oder SES antwortete mit einem Fehler.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"domain":{"type":"string"},"region":{"type":"string"},"error":{},"hint":{"type":"string"}},"required":["ok","domain","region"]}}}}},"operationId":"getApiV1AdminEmail-debugTest-domain","tags":["admin"],"parameters":[],"summary":"Ist die Absenderdomaene bei SES freigeschaltet?","description":"Fragt SES, ob die Domaene des Absenders verifiziert ist. Die Antwort traegt die Ampel fuer die Einstellungsseite: gruen, gelb, rot samt Handlungshinweis. Ohne sie steht der Anwender vor „All providers failed\" und weiss nicht, ob die Umgebung falsch ist, die IAM-Rolle fehlt oder die Domaene schlicht noch nicht freigeschaltet wurde.\n\nDie gepruefte Domaene kommt aus `NEMIX_EMAIL_DKIM_DOMAIN`, ersatzweise aus dem Teil hinter dem `@` von `NEMIX_EMAIL_FROM`; ohne beides aus einem festen Rueckfallwert. Sie kann also von der Domaene abweichen, mit der wirklich versendet wird.\n\nReine Auskunft — es wird nichts versendet.\n\nDer Pfad ist fest und steht in derselben Datei wie `/probe`; einen Platzhalter gibt es hier nicht, der ihn verdecken koennte."}},"/api/v1/admin/ai-monitoring/cost-trend":{"get":{"responses":{"200":{"description":"Cost-trend points { day, cost_cents }. Also the answer when the tenant has no events, the ledger table is missing or the DB is down — an empty series, never 404/500.","content":{"application/json":{"schema":{"type":"object","properties":{"points":{"type":"array","items":{"type":"object","properties":{"day":{"type":"string","description":"UTC day label, YYYY-MM-DD"},"cost_cents":{"type":"integer","description":"Spend on that day in whole cents; 0 when no events"}},"required":["day","cost_cents"]},"description":"Exactly `days` points, oldest first, gaps back-filled with 0"},"series":{"type":"array","items":{"type":"string","enum":["cost_cents"]},"description":"Field names the chart widget plots"}},"required":["points","series"]},"example":{"points":[{"day":"string","cost_cents":0}],"series":["cost_cents"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1AdminAi-monitoringCost-trend","tags":["admin","ai"],"parameters":[],"summary":"Daily AI spend over the last N days, tenant-scoped","description":"Daily AI spend (public.ai_cost_events, tenant-scoped) over the last N days, whole cents per day, missing days back-filled with 0. Backs the dashboard AI-Cost-Trend line-chart widget."}},"/api/v1/organizations/{orgId}/tree":{"get":{"responses":{"200":{"description":"Org tree","content":{"application/json":{"schema":{"type":"object","properties":{"orgId":{"type":"string","description":"Die Organisation aus dem Pfad, unveraendert zurueckgegeben"},"groups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"groupType":{"type":"string","enum":["region","sector","department","custom"]},"parentGroup":{"type":["string","null"],"description":"Uebergeordnete Gruppe; `null` auf oberster Ebene"}},"required":["id","name","groupType","parentGroup"]}},"memberships":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"groupId":{"type":"string"}},"required":["tenantId","groupId"]},"description":"Eine Zeile JE ZUGEHOERIGKEIT — ein Mandant kann mehrfach vorkommen"}},"required":["orgId","groups","memberships"]},"example":{"orgId":"string","groups":[{"id":"string","name":"string","groupType":"region","parentGroup":"string"}],"memberships":[{"tenantId":"string","groupId":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1OrganizationsByOrgIdTree","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true}],"summary":"Tree-view of an organization — groups plus tenant memberships","description":"Liest `public.tenant_groups` der Organisation (Kennung, Name, Gruppenart, uebergeordnete Gruppe) und dazu aus `public.tenant_membership` je Zugehoerigkeit EINE Zeile mit Mandanten- und Gruppenkennung — ein Mandant, der in mehreren Gruppen steht, erscheint entsprechend mehrfach. Die Verknuepfung der beiden Listen macht der Aufrufer.\n\nEs gibt weder Blaetterung noch Obergrenze noch Filter; die Antwort enthaelt immer den vollstaendigen Baum. Voraussetzung ist mindestens die Organisationsrolle `org_member` — fehlt sie, antwortet der Endpunkt mit 403 und `code: ORG_ROLE_REQUIRED`, ohne Anmeldung mit 401."}},"/api/v1/organizations/{orgId}/groups/{groupId}/members":{"post":{"responses":{"201":{"description":"Membership added","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1OrganizationsByOrgIdGroupsByGroupIdMembers","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true},{"schema":{"type":"string"},"in":"path","name":"groupId","required":true}],"summary":"Attach a tenant to a tenant-group (M:N membership)","description":"Schreibt eine Zeile nach `public.tenant_membership` und verbindet damit den Mandanten aus dem Rumpf (`tenantId`) mit der Gruppe aus dem Pfad. Der Schreibvorgang ist `ON CONFLICT DO NOTHING`: ein zweiter Aufruf mit derselben Paarung aendert nichts und antwortet trotzdem mit 201. Die Antwort ist `{ ok: true }` und nennt die entstandene Mitgliedschaft nicht.\n\nGeprueft wird die Organisationsrolle `org_admin` auf `orgId` — fehlt sie, 403 mit `code: ORG_ROLE_REQUIRED`, ohne Anmeldung 401. Nicht geprueft wird, ob `groupId` ueberhaupt zu `orgId` gehoert; die Einfuegung nimmt Mandanten- und Gruppenkennung unveraendert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid"}},"required":["tenantId"]},"example":{"tenantId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/v1/organizations/{tenantId}/resolve":{"get":{"responses":{"200":{"description":"Resolved","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"groupIds":{"type":"array","items":{"type":"string"},"description":"Alle Gruppen, in denen der Mandant steht"},"organization":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"parentOrg":{"type":["string","null"]}},"required":["id","name","slug","parentOrg"]},"ancestors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"parentOrg":{"type":["string","null"]}},"required":["id","name","slug","parentOrg"]},"description":"Die uebergeordneten Organisationen, oberste zuerst"},"role":{"type":["string","null"],"description":"Hoechste Organisationsrolle des Aufrufers; `null`, wenn keine hinterlegt ist"}},"required":["tenantId","groupIds","organization","ancestors","role"]},"example":{"tenantId":"string","groupIds":["string"],"organization":{"id":"string","name":"string","slug":"string","parentOrg":"string"},"ancestors":[{"id":"string","name":"string","slug":"string","parentOrg":"string"}],"role":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not in any org"}},"operationId":"getApiV1OrganizationsByTenantIdResolve","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"summary":"Resolve a tenant to its org chain","description":"Laeuft von `public.tenant_membership` ueber `public.tenant_groups` zu `public.organizations` und von dort die `parent_org`-Kette nach oben (hoechstens 32 Stufen, Zyklen werden abgebrochen). Die Antwort nennt `tenantId`, alle `groupIds` des Mandanten, die unmittelbare `organization`, die `ancestors` mit der obersten zuerst und die Rolle des Aufrufers. Es wird nichts geschrieben.\n\nGehoert der Mandant zu keiner Organisation, ist die Antwort 404. Denselben 404 bekommt, wer die Organisationsrolle `org_member` nicht hat — der Fall wird bewusst nicht von „unbekannt\" unterschieden, damit sich fremde Zugehoerigkeiten nicht erraten lassen. Ohne Anmeldung 401."}},"/api/v1/organizations/{orgId}/shared/{entityType}":{"get":{"responses":{"200":{"description":"Records","content":{"application/json":{"schema":{"type":"object","properties":{"records":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"orgId":{"type":"string"},"entityType":{"type":"string","enum":["customer","product","supplier","contact"]},"externalId":{"type":["string","null"]},"payload":{"type":"object","additionalProperties":{},"description":"Die Nutzdaten des Satzes, unveraendert aus JSONB"},"orgShared":{"type":"boolean"}},"required":["id","orgId","entityType","externalId","payload","orgShared"]}}},"required":["records"]},"example":{"records":[{"id":"string","orgId":"string","entityType":"customer","externalId":"string","payload":{},"orgShared":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1OrganizationsByOrgIdSharedByEntityType","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true},{"schema":{"type":"string"},"in":"path","name":"entityType","required":true}],"summary":"List shared master-data records for an entity type within the org","description":"Liest `public.shared_master_data` fuer diese Organisation und die im Pfad genannte Art, neueste Aenderung zuerst. Zeilen mit `org_shared = false` bleiben aussen vor — wer eine Zeile privat gestellt hat, findet sie hier nicht mehr. Je Zeile kommen Kennung, `orgId`, `entityType`, `externalId`, `payload` und `orgShared`.\n\nEs gibt weder Blaetterung noch Obergrenze; alle Zeilen kommen in einer Antwort. Der Pfadwert `entityType` wird nicht gegen die vier erlaubten Arten geprueft (`customer`, `product`, `supplier`, `contact`) — ein anderer Wert liefert eine leere Liste statt eines Fehlers. Voraussetzung ist die Organisationsrolle `org_member`: fehlt sie, 403 mit `code: ORG_ROLE_REQUIRED`, ohne Anmeldung 401."}},"/api/v1/organizations/{orgId}/shared":{"post":{"responses":{"201":{"description":"Upserted","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"example":{"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiV1OrganizationsByOrgIdShared","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true}],"summary":"Upsert a shared master-data record across the org","description":"Schreibt einen Satz nach `public.shared_master_data`. Der Schluessel ist das Tripel aus Organisation, `entityType` und `externalId`: gibt es ihn schon, werden `payload`, `orgShared` und der Aenderungszeitpunkt ueberschrieben — sonst entsteht eine neue Zeile. Beide Faelle antworten mit 201 und nur der Kennung (`{ id }`); ob angelegt oder ersetzt wurde, sagt die Antwort nicht. Ohne `externalId` gilt `null` als Schluesselteil, es kann also nur EINE solche Zeile je Art geben.\n\n`orgShared: false` nimmt den Satz aus der Verteilung an die Mandanten heraus; die Zeile bleibt erhalten, verschwindet aber aus der Liste und aus `materialise`.\n\nVerlangt wird die Organisationsrolle `org_admin`. Anders als bei den uebrigen Endpunkten dieses Routers wird die Ablehnung hier NICHT in ein 403 uebersetzt: fehlt die Rolle, antwortet der Endpunkt mit 500. Ohne Anmeldung 401.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entityType":{"type":"string","enum":["customer","product","supplier","contact"]},"externalId":{"type":["string","null"]},"payload":{"type":"object","additionalProperties":{}},"orgShared":{"type":"boolean","default":true}},"required":["entityType","payload"]},"example":{"entityType":"customer","externalId":"string","payload":{},"orgShared":true}}}}}},"/api/v1/organizations/{orgId}/shared/{entityType}/materialise":{"get":{"responses":{"200":{"description":"Materialised rows","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"recordId":{"type":"string"},"entityType":{"type":"string","enum":["customer","product","supplier","contact"]},"payload":{"type":"object","additionalProperties":{}}},"required":["tenantId","recordId","entityType","payload"]}}},"required":["rows"]},"example":{"rows":[{"tenantId":"string","recordId":"string","entityType":"customer","payload":{}}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiV1OrganizationsByOrgIdSharedByEntityTypeMaterialise","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true},{"schema":{"type":"string"},"in":"path","name":"entityType","required":true}],"summary":"Materialise shared master-data into per-tenant rows","description":"Rechnet die Verteilung nur AUS und gibt sie zurueck — es wird nichts in die Mandanten-Schemata geschrieben. Dazu werden die geteilten Saetze der Art (`org_shared = true`) und die Mandanten aller Gruppen der Organisation gelesen und ueber Kreuz gestellt: je Mandant und Satz eine Zeile mit `tenantId`, `recordId`, `entityType` und `payload`. Die Antwortlaenge ist damit Saetze mal Mandanten; Blaetterung oder Obergrenze gibt es nicht.\n\nDer Pfadwert `entityType` wird nicht gegen die vier erlaubten Arten geprueft — ein anderer Wert liefert eine leere Liste. Voraussetzung ist die Organisationsrolle `org_member`: fehlt sie, 403 mit `code: ORG_ROLE_REQUIRED`, ohne Anmeldung 401."}},"/admin/metrics":{"get":{"responses":{"200":{"description":"Metrics snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"tenants":{"type":"object","properties":{"total":{"type":"integer"},"active":{"type":"integer"},"newThisMonth":{"type":"integer"}},"required":["total","active","newThisMonth"]},"users":{"type":"object","properties":{"total":{"type":"integer"},"verifiedEmails":{"type":"integer"}},"required":["total","verifiedEmails"]},"revenue":{"type":"object","properties":{"currentMonth":{"type":"number"},"lastMonth":{"type":"number"}},"required":["currentMonth","lastMonth"]},"system":{"type":"object","properties":{"dbConnected":{"type":"boolean"},"apiVersion":{"type":"string"}},"required":["dbConnected","apiVersion"]}},"required":["tenants","users","revenue","system"]},"example":{"tenants":{"total":0,"active":0,"newThisMonth":0},"users":{"total":0,"verifiedEmails":0},"revenue":{"currentMonth":0,"lastMonth":0},"system":{"dbConnected":true,"apiVersion":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminMetrics","tags":["admin"],"parameters":[],"summary":"Kennzahlen fuers Admin-Dashboard: Mandanten, Nutzer, Umsatz","description":"Mischt zwei Bezugsgroeszen in EINER Antwort: Mandanten- und Nutzerzahlen gelten PLATTFORMWEIT, der Umsatz dagegen nur fuer den Mandanten des Aufrufers. Gezaehlt werden alle Mandanten, die aktiven und die im laufenden Kalendermonat angelegten, dazu alle Nutzer und die mit bestaetigter Adresse. Der Umsatz summiert die als bezahlt gekennzeichneten Rechnungen des laufenden und des Vormonats, nach Anlagedatum, nicht nach Zahlungsdatum. Der Endpunkt scheitert nie: ohne Datenbank oder bei einem Abfragefehler kommen NULLEN mit `system.dbConnected: false` — dieses Feld unterscheidet „nichts vorhanden\" von „nicht gemessen\"."}},"/admin/tenants/{id}/packs/{pack}":{"post":{"responses":{"200":{"description":"Paket installiert — Antwort nennt die Zahl angelegter Entitaeten/Felder"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant oder Paket nicht gefunden"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"postAdminTenantsByIdPacksByPack","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"pack","required":true}],"summary":"Aktiviert ein Branchenpaket fuer einen Mandanten","description":"Aktiviert ein Branchenpaket fuer einen Mandanten (legt dessen Entitaeten + Felder an). Verwalter-Weg zu POST /industry-packs/{slug}/install."}},"/admin/audit/verify":{"get":{"responses":{"200":{"description":"Verification result","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"brokenAt":{"type":["integer","null"]},"rows":{"type":"integer"},"mode":{"type":"string","const":"demo"},"error":{"type":"string"}},"required":["valid","brokenAt"]},"example":{"valid":true,"brokenAt":0,"rows":0,"mode":"demo","error":"string"}}}},"400":{"description":"No tenant context"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage des Audit-Logs fehlgeschlagen"},"503":{"description":"Verifier not ready"}},"operationId":"getAdminAuditVerify","tags":["admin"],"parameters":[],"summary":"Verify the HMAC hash-chain of the tenant audit_log","description":"Liest das Audit-Log des EIGENEN Mandanten vollstaendig — ohne Blaetterung, aeltester Eintrag zuerst — und rechnet die HMAC-Kette nach. `brokenAt` ist die null-basierte Position der ersten Zeile, deren gespeicherte Signatur nicht zur gerechneten passt; null heiszt „kein Bruch gefunden\". ACHTUNG bei der Deutung: verglichen wird nur, wo eine Zeile ueberhaupt eine Signatur traegt — Zeilen ohne Signatur werden durchgereicht, ein `valid: true` beweist also nicht, dass alle Eintraege signiert sind. Fehlt der Schluessel AUDIT_HMAC_KEY oder ist er kuerzer als 32 Byte, LEHNT der Endpunkt mit 503 ab, statt mit einem Ersatzschluessel ein falsches Gruen zu erzeugen. Ohne Datenbank kommt 200 mit mode=\"demo\" — dann wurde nichts geprueft. Rein lesend."}},"/admin/organizations":{"get":{"responses":{"200":{"description":"Organisationen — hoechstens 100, `total` zaehlt die gelieferten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"name":{},"slug":{},"country":{},"legalForm":{},"vatId":{},"parentOrg":{},"plan":{},"billingMode":{},"activePacks":{},"aiQuotaMonthly":{},"stripeCustomerId":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminOrganizations","tags":["organizations"],"parameters":[],"description":"Listet Organisationen. ACHTUNG: entgegen dem Dateikopf filtert dieser Aufruf NICHT nach Rolle — er gibt alle Organisationen zurueck (hoechstens 100, optional per `search` eingegrenzt). Der Zugang ist allein dadurch begrenzt, dass die gesamte Admin-Anwendung `requireSuperAdmin` vorgeschaltet hat.","summary":"Listet Organisationen","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Organisation angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"slug":{},"country":{},"legalForm":{},"vatId":{},"parentOrg":{},"plan":{},"billingMode":{},"activePacks":{},"aiQuotaMonthly":{},"stripeCustomerId":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"409":{"description":"Kuerzel bereits vergeben (text/plain)"}},"operationId":"postAdminOrganizations","tags":["organizations"],"parameters":[],"description":"Legt eine Organisation an. `slug` ist der Schluessel und muss frei sein — ist er vergeben, bricht der Aufruf mit 409 ab, BEVOR etwas geschrieben wird; erlaubt sind 2 bis 63 Zeichen aus Kleinbuchstaben, Ziffern und Bindestrich. Ohne Angabe gelten `country: DE`, `plan: enterprise` und `billingMode: central`. Nur fuer `super_admin` oder `owner`, sonst 403. Die Antwort traegt die angelegte Organisation nackt, ohne Huelle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]*[a-z0-9]$","minLength":2,"maxLength":63},"country":{"type":"string","minLength":2,"maxLength":2,"default":"DE"},"legalForm":{"type":"string","maxLength":16},"vatId":{"type":"string","maxLength":32},"parentOrg":{"type":"string","format":"uuid"},"plan":{"type":"string","enum":["enterprise","professional","starter"],"default":"enterprise"},"billingMode":{"type":"string","enum":["central","decentral","hybrid"],"default":"central"},"aiQuotaMonthly":{"type":"integer","exclusiveMinimum":0}},"required":["name","slug"]},"example":{"name":"string","slug":"00000000-0000-4000-8000-000000000000","country":"st","legalForm":"string","vatId":"string","parentOrg":"00000000-0000-4000-8000-000000000000","plan":"enterprise","billingMode":"central","aiQuotaMonthly":1}}}},"summary":"Legt eine Organisation an","x-nemix-summary-source":"description:first-sentence"}},"/admin/organizations/me/tenants":{"get":{"responses":{"200":{"description":"Die sichtbaren Mandanten — die Zeilen des Repositorys unveraendert, ohne Serialisierer. Der Umfang haengt an der Rolle des Aufrufers.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[],"total":0}}}},"401":{"description":"Nicht angemeldet (text/plain)"}},"operationId":"getAdminOrganizationsMeTenants","tags":["organizations"],"parameters":[],"summary":"Mandanten auflisten, die der angemeldete Nutzer sehen darf","description":"Die Reichweite haengt an der Rolle (Festlegung vom 10.09.2026): `super_admin` sieht ALLE Mandanten des Systems; wer in `organization_users` Eigentuemer oder Admin einer Organisation ist (Global Admin), sieht alle Mandanten dieser Organisation(en); alle anderen sehen die Mandanten, auf die sie ueber `organization_tenant_access` einen ausdruecklichen Zugriff haben. Ohne Anmeldung 401. Keine Blaetterung, keine Obergrenze. ACHTUNG: Sichtbarkeit ist nicht Erlaubnis — der Wechsel selbst prueft erneut (POST /:tenantIdOrSlug/switch), und fuer den Super Admin ist er dort noch nicht freigegeben."}},"/admin/organizations/{tenantIdOrSlug}/switch":{"post":{"responses":{"200":{"description":"Gewechselt. Nebenwirkung: der httpOnly-Cookie `nemix-active-tenant-slug` wird gesetzt (24 h) — DER entscheidet ab jetzt, welchen Mandanten nachfolgende Aufrufe sehen, nicht die Antwort hier.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"role":{}},"required":["ok","tenantId","tenantSlug"],"additionalProperties":false},"example":{"ok":true,"tenantId":"string","tenantSlug":"string"}}}},"400":{"description":"Kennung fehlt oder laenger als 80 Zeichen (text/plain)"},"401":{"description":"Nicht angemeldet (text/plain)"},"403":{"description":"Kein Zugriff auf diesen Mandanten (text/plain)"}},"operationId":"postAdminOrganizationsByTenantIdOrSlugSwitch","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantIdOrSlug","required":true}],"summary":"Wechselt den Mandanten-Kontext serverseitig","description":"Setzt den httpOnly-Cookie `nemix-active-tenant-slug` nach Pruefung. Fuer `super_admin` ist JEDER Mandant erlaubt (Festlegung vom 10.09.2026: „ersteinmal ohne genehmigung und protokoll\"); fuer alle anderen gilt weiterhin `organization_tenant_access`, sonst 403. Der Wechsel schreibt KEIN Protokoll — das wird gebaut, wenn es beauftragt ist."}},"/admin/organizations/{id}":{"get":{"responses":{"200":{"description":"Organisation — nackt, ohne Huelle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"slug":{},"country":{},"legalForm":{},"vatId":{},"parentOrg":{},"plan":{},"billingMode":{},"activePacks":{},"aiQuotaMonthly":{},"stripeCustomerId":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Organisation nicht gefunden (text/plain)"}},"operationId":"getAdminOrganizationsById","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest eine Organisation ueber ihre ID. Der Aufruf ist an keine Rolle gebunden — es gilt allein die Wache der Admin-Anwendung davor. Gibt es die ID nicht, kommt 404 als text/plain, nicht als JSON.","summary":"Liest eine Organisation ueber ihre ID","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Geaendert — die vollstaendige Organisation nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"slug":{},"country":{},"legalForm":{},"vatId":{},"parentOrg":{},"plan":{},"billingMode":{},"activePacks":{},"aiQuotaMonthly":{},"stripeCustomerId":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"404":{"description":"Organisation nicht gefunden (text/plain)"}},"operationId":"patchAdminOrganizationsById","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aendert einzelne Stammdaten einer Organisation: Name, Rechtsform, USt-IdNr., Tarif, Abrechnungsmodus, KI-Monatskontingent, Stripe-Kundennummer und den freien `settings`-Block. Alle Felder sind einzeln optional; was nicht im Rumpf steht, bleibt unveraendert. `slug` und `parentOrg` sind hier NICHT aenderbar. Nur fuer `super_admin`, sonst 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"legalForm":{"type":"string","maxLength":16},"vatId":{"type":"string","maxLength":32},"plan":{"type":"string","enum":["enterprise","professional","starter"]},"billingMode":{"type":"string","enum":["central","decentral","hybrid"]},"aiQuotaMonthly":{"type":"integer","exclusiveMinimum":0},"stripeCustomerId":{"type":"string"},"settings":{"type":"object","additionalProperties":{}}}},"example":{"name":"string","legalForm":"string","vatId":"string","plan":"enterprise","billingMode":"central","aiQuotaMonthly":1,"stripeCustomerId":"string","settings":{}}}}},"summary":"Aendert einzelne Stammdaten einer Organisation","x-nemix-summary-source":"description:first-sentence"}},"/admin/organizations/{id}/tenants":{"get":{"responses":{"200":{"description":"Mandanten dieser Organisation — die Zeilen des Repositorys unveraendert, ohne Serialisierer und ohne Obergrenze","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminOrganizationsByIdTenants","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Listet die Mandanten, die an dieser Organisation haengen. Ohne Filter, ohne Blaetterung und ohne Obergrenze — bei einer grossen Organisation kommt alles auf einmal. Eine unbekannte Organisations-ID ergibt hier KEIN 404, sondern eine leere Liste.","summary":"Listet die Mandanten, die an dieser Organisation haengen","x-nemix-summary-source":"description:first-sentence"}},"/admin/organizations/{id}/tenants/{tid}":{"post":{"responses":{"200":{"description":"Angehaengt. Eine Zeile wurde geaendert.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"organizationId":{"type":"string"},"tenantId":{"type":"string"}},"required":["ok","organizationId","tenantId"],"additionalProperties":false},"example":{"ok":true,"organizationId":"string","tenantId":"string"}}}},"400":{"description":"Rumpf ungueltig (`invalid_body`)"},"401":{"description":"Nicht angemeldet"},"403":{"description":"Keine super_admin-Rolle"},"404":{"description":"Die Mandanten-ID trifft keine Zeile"}},"operationId":"postAdminOrganizationsByIdTenantsByTid","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"tid","required":true}],"description":"Haengt einen bereits bestehenden Mandanten an eine Organisation. Organisation und Mandant stehen im Pfad. Der Rumpf ist optional, wird aber GEPRUEFT: `groupId` und `parentTenant` muessen UUIDs sein, `companyType` einer von parent, subsidiary, branch, standalone; ein unbekanntes Feld wird abgewiesen (400). Trifft die Mandanten-ID keine Zeile, antwortet die Route 404 statt 200. NUR fuer `super_admin`: Wer zu einer Organisation gehoert, ist die Reichweite des Global Admin selbst und darf nicht von ihm gesetzt werden.","summary":"Haengt einen bereits bestehenden Mandanten an eine Organisation","x-nemix-summary-source":"description:first-sentence"}},"/admin/organizations/{id}/users":{"post":{"responses":{"201":{"description":"Nutzer angelegt, Mandanten-Zugriffe erteilt. `grantedTenants` zaehlt die angefragten — und stimmt, weil ein Fehler beim Erteilen den ganzen Aufruf abbrechen laesst. Ein Zwischenzustand „201, aber nur die Haelfte erteilt\" ist nicht moeglich; ein Abbruch NACH dem Anlegen des Nutzers schon (dann existiert der Nutzer ohne Zugriffe).","content":{"application/json":{"schema":{"type":"object","properties":{"organizationUserId":{},"grantedTenants":{"type":"number"}},"required":["grantedTenants"],"additionalProperties":false},"example":{"grantedTenants":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"409":{"description":"Die Aenderung liesse die Organisation ohne Eigentuemer (`last_owner`)"}},"operationId":"postAdminOrganizationsByIdUsers","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Nimmt einen bestehenden Nutzer in die Organisation auf — Rolle `owner`, `admin`, `member` (Vorgabe) oder `auditor`, dazu das Kennzeichen `isConsolidationUser`. Die Mitgliedschaft ist wiederholbar: ein zweiter Aufruf fuer denselben Nutzer legt nichts doppelt an, sondern schreibt Rolle und Kennzeichen neu. Optional erteilt `tenantAccess` in einem Zug Zugriff auf einzelne Mandanten; die Zugriffe werden nacheinander gesetzt, nicht in einer Transaktion. Nur fuer `super_admin`, `owner`, sonst 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","minLength":1},"role":{"type":"string","enum":["owner","admin","member","auditor"],"default":"member"},"isConsolidationUser":{"type":"boolean","default":false},"tenantAccess":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid"},"role":{"type":"string","default":"user"}},"required":["tenantId"]}}},"required":["userId"]},"example":{"userId":"string","role":"owner","isConsolidationUser":true,"tenantAccess":[{"tenantId":"00000000-0000-4000-8000-000000000000","role":"string"}]}}}},"summary":"Nimmt einen bestehenden Nutzer in die Organisation auf","x-nemix-summary-source":"description:first-sentence"}},"/admin/organizations/{id}/groups":{"get":{"responses":{"200":{"description":"Untergruppen (Region/Sparte/Abteilung) — die Zeilen des Repositorys unveraendert, ohne Serialisierer","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminOrganizationsByIdGroups","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Listet die Untergruppen einer Organisation — Regionen, Sparten, Abteilungen oder eigene Zuschnitte, nach Name sortiert. Die Liste ist flach: eine Gruppe kann laut `parentGroup` unter einer anderen haengen, aufgebaut wird der Baum hier aber nicht. Ohne Filter und ohne Blaetterung.","summary":"Listet die Untergruppen einer Organisation","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Gruppe angelegt — die Antwort traegt NUR die neue ID, nicht die Gruppe. Wer Name oder Typ zurueckbraucht, muss die Liste neu lesen.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"}},"operationId":"postAdminOrganizationsByIdGroups","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Legt eine Untergruppe in der Organisation an. `groupType` ist `region` (Vorgabe), `sector`, `department` oder `custom`; `parentGroup` haengt die neue Gruppe unter eine bestehende. Der Name wird nicht auf Eindeutigkeit geprueft — derselbe Name zweimal ergibt zwei Gruppen. Nur fuer `super_admin`, sonst 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"groupType":{"type":"string","enum":["region","sector","department","custom"],"default":"region"},"parentGroup":{"type":"string","format":"uuid"}},"required":["name"]},"example":{"name":"string","groupType":"region","parentGroup":"00000000-0000-4000-8000-000000000000"}}}},"summary":"Legt eine Untergruppe in der Organisation an","x-nemix-summary-source":"description:first-sentence"}},"/admin/organizations/{orgId}/tree":{"get":{"responses":{"200":{"description":"Org tree","content":{"application/json":{"schema":{"type":"object","properties":{"orgId":{"type":"string","description":"Die Organisation aus dem Pfad, unveraendert zurueckgegeben"},"groups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"groupType":{"type":"string","enum":["region","sector","department","custom"]},"parentGroup":{"type":["string","null"],"description":"Uebergeordnete Gruppe; `null` auf oberster Ebene"}},"required":["id","name","groupType","parentGroup"]}},"memberships":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"groupId":{"type":"string"}},"required":["tenantId","groupId"]},"description":"Eine Zeile JE ZUGEHOERIGKEIT — ein Mandant kann mehrfach vorkommen"}},"required":["orgId","groups","memberships"]},"example":{"orgId":"string","groups":[{"id":"string","name":"string","groupType":"region","parentGroup":"string"}],"memberships":[{"tenantId":"string","groupId":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminOrganizationsByOrgIdTree","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true}],"summary":"Tree-view of an organization — groups plus tenant memberships","description":"Liest `public.tenant_groups` der Organisation (Kennung, Name, Gruppenart, uebergeordnete Gruppe) und dazu aus `public.tenant_membership` je Zugehoerigkeit EINE Zeile mit Mandanten- und Gruppenkennung — ein Mandant, der in mehreren Gruppen steht, erscheint entsprechend mehrfach. Die Verknuepfung der beiden Listen macht der Aufrufer.\n\nEs gibt weder Blaetterung noch Obergrenze noch Filter; die Antwort enthaelt immer den vollstaendigen Baum. Voraussetzung ist mindestens die Organisationsrolle `org_member` — fehlt sie, antwortet der Endpunkt mit 403 und `code: ORG_ROLE_REQUIRED`, ohne Anmeldung mit 401."}},"/admin/organizations/{orgId}/groups/{groupId}/members":{"post":{"responses":{"201":{"description":"Membership added","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postAdminOrganizationsByOrgIdGroupsByGroupIdMembers","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true},{"schema":{"type":"string"},"in":"path","name":"groupId","required":true}],"summary":"Attach a tenant to a tenant-group (M:N membership)","description":"Schreibt eine Zeile nach `public.tenant_membership` und verbindet damit den Mandanten aus dem Rumpf (`tenantId`) mit der Gruppe aus dem Pfad. Der Schreibvorgang ist `ON CONFLICT DO NOTHING`: ein zweiter Aufruf mit derselben Paarung aendert nichts und antwortet trotzdem mit 201. Die Antwort ist `{ ok: true }` und nennt die entstandene Mitgliedschaft nicht.\n\nGeprueft wird die Organisationsrolle `org_admin` auf `orgId` — fehlt sie, 403 mit `code: ORG_ROLE_REQUIRED`, ohne Anmeldung 401. Nicht geprueft wird, ob `groupId` ueberhaupt zu `orgId` gehoert; die Einfuegung nimmt Mandanten- und Gruppenkennung unveraendert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid"}},"required":["tenantId"]},"example":{"tenantId":"00000000-0000-4000-8000-000000000000"}}}}}},"/admin/organizations/{tenantId}/resolve":{"get":{"responses":{"200":{"description":"Resolved","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"groupIds":{"type":"array","items":{"type":"string"},"description":"Alle Gruppen, in denen der Mandant steht"},"organization":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"parentOrg":{"type":["string","null"]}},"required":["id","name","slug","parentOrg"]},"ancestors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"parentOrg":{"type":["string","null"]}},"required":["id","name","slug","parentOrg"]},"description":"Die uebergeordneten Organisationen, oberste zuerst"},"role":{"type":["string","null"],"description":"Hoechste Organisationsrolle des Aufrufers; `null`, wenn keine hinterlegt ist"}},"required":["tenantId","groupIds","organization","ancestors","role"]},"example":{"tenantId":"string","groupIds":["string"],"organization":{"id":"string","name":"string","slug":"string","parentOrg":"string"},"ancestors":[{"id":"string","name":"string","slug":"string","parentOrg":"string"}],"role":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not in any org"}},"operationId":"getAdminOrganizationsByTenantIdResolve","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"summary":"Resolve a tenant to its org chain","description":"Laeuft von `public.tenant_membership` ueber `public.tenant_groups` zu `public.organizations` und von dort die `parent_org`-Kette nach oben (hoechstens 32 Stufen, Zyklen werden abgebrochen). Die Antwort nennt `tenantId`, alle `groupIds` des Mandanten, die unmittelbare `organization`, die `ancestors` mit der obersten zuerst und die Rolle des Aufrufers. Es wird nichts geschrieben.\n\nGehoert der Mandant zu keiner Organisation, ist die Antwort 404. Denselben 404 bekommt, wer die Organisationsrolle `org_member` nicht hat — der Fall wird bewusst nicht von „unbekannt\" unterschieden, damit sich fremde Zugehoerigkeiten nicht erraten lassen. Ohne Anmeldung 401."}},"/admin/organizations/{orgId}/shared/{entityType}":{"get":{"responses":{"200":{"description":"Records","content":{"application/json":{"schema":{"type":"object","properties":{"records":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"orgId":{"type":"string"},"entityType":{"type":"string","enum":["customer","product","supplier","contact"]},"externalId":{"type":["string","null"]},"payload":{"type":"object","additionalProperties":{},"description":"Die Nutzdaten des Satzes, unveraendert aus JSONB"},"orgShared":{"type":"boolean"}},"required":["id","orgId","entityType","externalId","payload","orgShared"]}}},"required":["records"]},"example":{"records":[{"id":"string","orgId":"string","entityType":"customer","externalId":"string","payload":{},"orgShared":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminOrganizationsByOrgIdSharedByEntityType","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true},{"schema":{"type":"string"},"in":"path","name":"entityType","required":true}],"summary":"List shared master-data records for an entity type within the org","description":"Liest `public.shared_master_data` fuer diese Organisation und die im Pfad genannte Art, neueste Aenderung zuerst. Zeilen mit `org_shared = false` bleiben aussen vor — wer eine Zeile privat gestellt hat, findet sie hier nicht mehr. Je Zeile kommen Kennung, `orgId`, `entityType`, `externalId`, `payload` und `orgShared`.\n\nEs gibt weder Blaetterung noch Obergrenze; alle Zeilen kommen in einer Antwort. Der Pfadwert `entityType` wird nicht gegen die vier erlaubten Arten geprueft (`customer`, `product`, `supplier`, `contact`) — ein anderer Wert liefert eine leere Liste statt eines Fehlers. Voraussetzung ist die Organisationsrolle `org_member`: fehlt sie, 403 mit `code: ORG_ROLE_REQUIRED`, ohne Anmeldung 401."}},"/admin/organizations/{orgId}/shared":{"post":{"responses":{"201":{"description":"Upserted","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"example":{"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postAdminOrganizationsByOrgIdShared","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true}],"summary":"Upsert a shared master-data record across the org","description":"Schreibt einen Satz nach `public.shared_master_data`. Der Schluessel ist das Tripel aus Organisation, `entityType` und `externalId`: gibt es ihn schon, werden `payload`, `orgShared` und der Aenderungszeitpunkt ueberschrieben — sonst entsteht eine neue Zeile. Beide Faelle antworten mit 201 und nur der Kennung (`{ id }`); ob angelegt oder ersetzt wurde, sagt die Antwort nicht. Ohne `externalId` gilt `null` als Schluesselteil, es kann also nur EINE solche Zeile je Art geben.\n\n`orgShared: false` nimmt den Satz aus der Verteilung an die Mandanten heraus; die Zeile bleibt erhalten, verschwindet aber aus der Liste und aus `materialise`.\n\nVerlangt wird die Organisationsrolle `org_admin`. Anders als bei den uebrigen Endpunkten dieses Routers wird die Ablehnung hier NICHT in ein 403 uebersetzt: fehlt die Rolle, antwortet der Endpunkt mit 500. Ohne Anmeldung 401.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entityType":{"type":"string","enum":["customer","product","supplier","contact"]},"externalId":{"type":["string","null"]},"payload":{"type":"object","additionalProperties":{}},"orgShared":{"type":"boolean","default":true}},"required":["entityType","payload"]},"example":{"entityType":"customer","externalId":"string","payload":{},"orgShared":true}}}}}},"/admin/organizations/{orgId}/shared/{entityType}/materialise":{"get":{"responses":{"200":{"description":"Materialised rows","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"recordId":{"type":"string"},"entityType":{"type":"string","enum":["customer","product","supplier","contact"]},"payload":{"type":"object","additionalProperties":{}}},"required":["tenantId","recordId","entityType","payload"]}}},"required":["rows"]},"example":{"rows":[{"tenantId":"string","recordId":"string","entityType":"customer","payload":{}}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminOrganizationsByOrgIdSharedByEntityTypeMaterialise","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true},{"schema":{"type":"string"},"in":"path","name":"entityType","required":true}],"summary":"Materialise shared master-data into per-tenant rows","description":"Rechnet die Verteilung nur AUS und gibt sie zurueck — es wird nichts in die Mandanten-Schemata geschrieben. Dazu werden die geteilten Saetze der Art (`org_shared = true`) und die Mandanten aller Gruppen der Organisation gelesen und ueber Kreuz gestellt: je Mandant und Satz eine Zeile mit `tenantId`, `recordId`, `entityType` und `payload`. Die Antwortlaenge ist damit Saetze mal Mandanten; Blaetterung oder Obergrenze gibt es nicht.\n\nDer Pfadwert `entityType` wird nicht gegen die vier erlaubten Arten geprueft — ein anderer Wert liefert eine leere Liste. Voraussetzung ist die Organisationsrolle `org_member`: fehlt sie, 403 mit `code: ORG_ROLE_REQUIRED`, ohne Anmeldung 401."}},"/admin/tenants/clone":{"post":{"responses":{"201":{"description":"Der Klon steht. `tablesCloned` und `rowsCopied` sagen, wie viel tatsaechlich kopiert wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"tenantId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"tenantNumber":{"type":["string","null"]},"tablesCloned":{"type":"integer"},"rowsCopied":{"type":"integer"}},"required":["ok","tenantId","slug","name","tenantNumber","tablesCloned","rowsCopied"]},"example":{"ok":true,"tenantId":"string","slug":"string","name":"string","tenantNumber":"string","tablesCloned":0,"rowsCopied":0}}}},"400":{"description":"Quelle nicht bestimmbar (`source_required`), Slug ungueltig (`invalid_slug`), oder der Rumpf haelt das Schema nicht ein (`newName` fehlt, `newSlug` passt nicht auf das Muster)."},"401":{"description":"Keine Benutzer-ID im Kontext (`{ \"error\": \"unauthorized\" }`)."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"404":{"description":"Quell-Mandant nicht gefunden (`source_missing`)."},"409":{"description":"Slug oder Ziel-Schema existiert bereits (`slug_taken`) — es wurde nichts angelegt und nichts zusammengefuehrt."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"500":{"description":"Klon fehlgeschlagen (`clone_failed`); die Transaktion ist zurueckgerollt, es bleibt kein halber Mandant zurueck."},"503":{"description":"Keine Datenbankverbindung (`db_unavailable`)."}},"operationId":"postAdminTenantsClone","tags":["admin","tenants"],"parameters":[],"summary":"Mandanten als vollstaendige Dublette klonen","description":"Legt einen NEUEN, eigenstaendigen Mandanten an, der eine vollstaendige Dublette eines bestehenden ist — Struktur UND Daten. Gedacht fuer Test-/Experimentiermandanten.\n\nWIE VIEL ANGELEGT WIRD — es ist kein Auszug, es ist alles:\n- eine neue Zeile in `public.tenants` (eigene Mandantennummer, Status `active`, Tarif/Pakete/Einstellungen/KI-Konfiguration/Branding/Steuernummer von der Quelle uebernommen)\n- ein neues Postgres-Schema `tenant_<newSlug>`\n- JEDE Basistabelle des Quell-Schemas, angelegt per `CREATE TABLE (LIKE … INCLUDING ALL)` (Spalten, Vorgaben, Pruefregeln, Indizes — KEINE Fremdschluessel)\n- JEDE ZEILE dieser Tabellen, kopiert per `INSERT … SELECT` ohne `WHERE` und ohne `LIMIT`\n\nES GIBT KEINE OBERGRENZE. Keine Zeilen-, Tabellen- oder Groessenschranke, keine Vorabpruefung, kein Trockenlauf. Ein Mandant mit Millionen Zeilen wird mit Millionen Zeilen kopiert; die Antwort nennt hinterher `tablesCloned` und `rowsCopied`. Alles laeuft in EINER Transaktion (DDL eingeschlossen) — bricht etwas ab, ist nichts angelegt, aber bis dahin haelt der Vorgang Sperren und Plattenplatz.\n\nNEBENWIRKUNGEN AUF DIE QUELLE UND AUF DEN AUFRUFER: der Handler sichert den Zugang ueber die Organisationsebene. Hat der Quell-Mandant keine Organisation, wird eine angelegt (`<Name> (Gruppe)`, Tarif `enterprise`). Der aufrufende Benutzer wird Mitglied (`owner`) und bekommt Zugriff auf den NEUEN UND den QUELL-Mandanten. Anschliessend wird `organization_id` bei beiden Mandanten gesetzt, falls sie noch leer war — der Quell-Mandant wird also mit veraendert.\n\nQUELLE: `sourceSlug` nennt den Quell-Mandanten. Fehlt er, greift der Handler auf den Mandanten der Sitzung zurueck — den setzt aber nur `tenantMiddleware`, und die haengt nicht an der admin-Sub-App. AN DIESEN PFADEN IST `sourceSlug` DAHER IN DER PRAXIS PFLICHT; ohne ihn kommt 400 `source_required`. An der Laufzeit nachgemessen (30.08.2026).\n\nZIEL-SLUG: `newSlug` ist optional. Ohne Angabe wird er aus `newName` abgeleitet und um `-test-<vier Zeichen>` ergaenzt (Zeitstempel zur Basis 36). Erlaubt ist `^[a-z][a-z0-9-]{1,40}$`.\n\nNICHT WIEDERHOLBAR, KEIN ZUSAMMENFUEHREN: existiert der Slug oder das Ziel-Schema schon, endet der Aufruf mit 409 — es wird nichts ergaenzt und nichts ueberschrieben.\n\nBEKANNTER KOMPROMISS: `LIKE INCLUDING DEFAULTS` uebernimmt SERIAL-Vorgaben als Verweis auf die SEQUENZ DER QUELLE. Der Klon zaehlt an dieser Stelle also mit der Quelle mit. Fuer Experimentiermandanten hingenommen, fuer einen produktiven Zwilling nicht geeignet.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sourceSlug":{"type":"string","minLength":2,"maxLength":63},"newName":{"type":"string","minLength":1,"maxLength":255},"newSlug":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"}},"required":["newName"]},"example":{"sourceSlug":"string","newName":"string"}}}}}},"/admin/tenants/stats":{"get":{"responses":{"200":{"description":"Zaehlung nach Status. Ohne Datenbank kommen ebenfalls 200 und lauter Nullen — eine 0 heisst hier also „keiner\" ODER „nicht messbar\".","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"number"},"active":{"type":"number"},"trial":{"type":"number"},"suspended":{"type":"number"},"cancelled":{"type":"number"}},"required":["total","active","trial","suspended"]},"example":{"total":0,"active":0,"trial":0,"suspended":0,"cancelled":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminTenantsStats","tags":["admin","tenants"],"parameters":[],"description":"Zaehlt die Mandanten nach Status. Eine einzige Abfrage auf `public.tenants` liefert total, active, trial, suspended und cancelled ueber COUNT(*) FILTER; Nutzer-, Tarif- oder Verbrauchsdaten werden dafuer nicht gelesen. Filter oder Blaetterung gibt es nicht. Der Endpunkt ist bewusst VOR `/:id` registriert, sonst laese der Router „stats\" als Mandanten-Id.","summary":"Zaehlt die Mandanten nach Status","x-nemix-summary-source":"description:first-sentence"}},"/admin/tenants":{"get":{"responses":{"200":{"description":"Eine Seite der Mandantenliste. `total` zaehlt mit denselben Bedingungen wie die Liste. Ohne Datenbank kommt `{data: [], total: 0}` — ebenfalls mit 200 und ohne `page`/`limit`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"slug":{},"name":{},"tenantNumber":{},"status":{},"createdAt":{},"updatedAt":{},"plan":{},"planPrice":{},"userCount":{"type":"number"}},"required":["userCount"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"},"error":{"type":"string"}},"required":["data","total"]},"example":{"data":[{"userCount":0}],"total":0,"page":0,"limit":0,"error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — die Antwort traegt eine LEERE Liste UND den rohen Fehlertext im Feld `error`","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"slug":{},"name":{},"tenantNumber":{},"status":{},"createdAt":{},"updatedAt":{},"plan":{},"planPrice":{},"userCount":{"type":"number"}},"required":["userCount"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"},"error":{"type":"string"}},"required":["data","total"]}}}}},"operationId":"getAdminTenants","tags":["admin","tenants"],"parameters":[],"description":"Liest eine Seite aus `public.tenants`. Je Zeile kommen Tarifname und Monatspreis aus `plans` sowie die Anzahl der nicht geloeschten Nutzer aus `users` dazu. `page` beginnt bei 1, `limit` liegt zwischen 1 und 100 (Vorgabe 50); gefiltert wird ueber `status`, `plan` (Tarifname) und `search` (Teiltreffer in Name oder Slug). Sortiert nach Anlagedatum, neueste zuerst. `total` zaehlt mit denselben Bedingungen wie die Liste — die Zahl passt also zur Filterung.","summary":"Liest eine Seite aus `public.tenants`","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Mandant samt Verwalter-Konto angelegt. ACHTUNG: `tempPassword` steht im Klartext in dieser Antwort — einmalig, aber ungeschuetzt. Sie gehoert nicht in Protokolle oder Zwischenspeicher; das Passwort ist ueber einen sicheren Kanal weiterzugeben. Eine Willkommensmail geht nebenher raus, ihr Scheitern aendert die Antwort NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{},"userId":{},"slug":{"type":"string"},"companyName":{"type":"string"},"plan":{"type":"string"},"status":{"type":"string","const":"trial"},"loginUrl":{"type":"string"},"tempPassword":{"type":"string"}},"required":["slug","companyName","plan","status","loginUrl","tempPassword"],"additionalProperties":false},"example":{"slug":"string","companyName":"string","plan":"string","status":"trial","loginUrl":"string","tempPassword":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"E-Mail bereits vergeben (`duplicate_email`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Anlegen fehlgeschlagen (`create_failed`) — `message` traegt den Grund","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postAdminTenants","tags":["admin","tenants"],"parameters":[],"description":"Legt Mandant UND erstes Verwalter-Konto in einem Aufruf an. Der Slug entsteht aus dem Firmennamen und wird bei Kollision mit `-1`, `-2` … eindeutig gemacht (nach 99 Versuchen bricht der Aufruf ab). Der Mandant startet mit Status `trial` und dem Paket `core`; ist der genannte Tarif unbekannt, faellt die Zuordnung auf `trial` zurueck. Das Konto bekommt die Rolle `admin` und ein einmalig zurueckgegebenes Zufallspasswort — scheitert seine Anlage, wird die eben erzeugte Mandantenzeile wieder entfernt. Eine Willkommensmail geht nebenher raus; ihr Scheitern aendert das Ergebnis nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string","minLength":2,"maxLength":255},"firstName":{"type":"string","minLength":1,"maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email"},"plan":{"type":"string","enum":["starter","professional","enterprise"],"default":"starter"},"sitzland":{"type":"string","pattern":"^[A-Z]{2}$"},"profil":{"type":"object","properties":{"branchen":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":200,"default":[]},"groesse":{"type":"string","maxLength":32,"default":""},"laender":{"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"maxItems":200,"default":[]},"sprachen":{"type":"array","items":{"type":"string","minLength":2,"maxLength":8},"maxItems":200,"default":[]}}}},"required":["companyName","firstName","lastName","email","sitzland"]}}}},"summary":"Legt Mandant UND erstes Verwalter-Konto in einem Aufruf an","x-nemix-summary-source":"description:first-sentence"}},"/admin/tenants/{id}":{"get":{"responses":{"200":{"description":"Ein Mandant — die ganze Tabellenzeile (`t.*`) plus Plan-Name, Preis, Nutzer- und Speichergrenze und die gezaehlten Nutzer. Welche Spalten `t.*` umfasst, bestimmt das Tabellenschema, nicht diese Route.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getAdminTenantsById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest einen Mandanten ueber seine Id. Zur vollstaendigen Tabellenzeile kommen Tarifname, Monatspreis, Nutzer- und Speichergrenze aus `plans` sowie die Zahl der nicht geloeschten Nutzer. Ein Zugriff ueber den Slug ist hier nicht vorgesehen. Eine unbekannte Id ergibt 404, nicht eine leere Zeile.","summary":"Liest einen Mandanten ueber seine Id","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Geaendert — die Antwort traegt NUR id, name und status zurueck, nicht den ganzen Mandanten. Ein unbekannter Planname wird still uebergangen: kam daneben ein anderes Feld, meldet der Aufruf 200, obwohl der Plan unveraendert blieb; kam nur der Plan, meldet er 400 `no_changes`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Kein aenderbares Feld im Rumpf (`no_changes`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Aenderung fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchAdminTenantsById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Namen, Tarif oder Status eines Mandanten aendern","description":"Aendert Name, Tarif und/oder Status eines Mandanten; nicht mitgeschickte Felder bleiben unberuehrt. Der Tarif wird ueber seinen Namen in `plans` aufgeloest — ein unbekannter Name wird still uebergangen und aendert nichts. Bleibt danach kein einziges Feld zum Schreiben uebrig, antwortet der Aufruf 400 `no_changes`. `updated_at` wird bei jeder echten Aenderung mitgezogen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":255},"plan":{"type":"string","enum":["starter","professional","enterprise","trial"]},"status":{"type":"string","enum":["active","trial","suspended","cancelled"]}}},"example":{"name":"string","plan":"starter","status":"active"}}}}},"delete":{"responses":{"200":{"description":"Geloescht. Die Antwort sagt, was wirklich fiel.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"schemaGeloescht":{"type":"string","description":"Name des verworfenen Mandanten-Schemas"},"organisationGeloescht":{"type":"boolean","description":"true, wenn die Gruppe leer zurueckblieb"},"kundennummer":{"type":["string","null"],"description":"Die sechsstellige Nummer der Prozessdatenbank, die die Oberflaeche noch wegraeumen muss. null, wenn der Mandant keine Nummer trug."}},"required":["ok","tenantId","slug","name","schemaGeloescht","organisationGeloescht","kundennummer"]},"example":{"ok":true,"tenantId":"string","slug":"string","name":"string","schemaGeloescht":"string","organisationGeloescht":true,"kundennummer":"string"}}}},"400":{"description":"Bestaetigung fehlt oder passt nicht zum Slug (`confirmation_mismatch`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"404":{"description":"Mandant nicht gefunden (`not_found`)"},"409":{"description":"Ein Fremdschluessel haelt den Mandanten (`in_use`) — `message` nennt die Einschraenkung. Es wurde NICHTS geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)"}},"operationId":"deleteAdminTenantsById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mandanten endgueltig loeschen, mit allen Daten","description":"Verwirft das Mandanten-Schema `tenant_<slug>` samt Inhalt und loescht die Zeile in `public.tenants`. Per ON DELETE CASCADE fallen mit: Benutzer (und daran Sitzungen und Konten), API-Schluessel, `organization_tenant_access` und Ebenen. NICHT MIT FALLEN `support_sessions` und `support_session_events`: das Protokoll haelt fest, wer vom Hersteller in diesen Mandanten gesehen hat, und ein Nachweis, der mit seinem Gegenstand verschwindet, ist keiner (Fremdschluessel geloest in 20260910163000_support_protokoll_ueberlebt_den_mandanten.sql). Die Organisation faellt NUR, wenn kein anderer Mandant mehr an ihr haengt. BEIDES IN EINER TRANSAKTION: entweder alles oder nichts. NICHT UMKEHRBAR — es gibt kein Wiederherstellen. Verlangt `?bestaetigung=<slug>`; stimmt sie nicht, 400 und es passiert nichts. Die Prozessdatenbank der Oberflaeche liegt im Dateisystem und faellt NICHT mit; die Antwort nennt ihre Nummer. Nur fuer `super_admin`, sonst 403."}},"/admin/tenants/{id}/suspend":{"post":{"responses":{"200":{"description":"Gesperrt (`status=suspended`). Die Antwort traegt nur id, name und status. Laufende Sitzungen des Mandanten beendet dieser Aufruf NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postAdminTenantsByIdSuspend","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt den Status des Mandanten auf `suspended` und zieht `updated_at` mit. Mehr passiert nicht: Daten, Tarif und Pakete bleiben unberuehrt, und bereits laufende Sitzungen des Mandanten beendet der Aufruf NICHT. Rueckgaengig ueber `POST /api/admin/tenants/{id}/activate`. Eine unbekannte Id ergibt 404.","summary":"Setzt den Status des Mandanten auf `suspended` und zieht `updated_at` mit","x-nemix-summary-source":"description:first-sentence"}},"/admin/tenants/{id}/activate":{"post":{"responses":{"200":{"description":"Freigeschaltet (`status=active`). Die Antwort traegt nur id, name und status.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postAdminTenantsByIdActivate","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt den Status des Mandanten auf `active` und zieht `updated_at` mit — die Gegenrichtung zu `suspend`. Der vorherige Status wird nicht geprueft: der Aufruf wirkt auf einen gesperrten wie auf einen Test-Mandanten und laesst einen bereits aktiven unveraendert aktiv. Tarif, Pakete und Grenzen ruehrt er nicht an. Eine unbekannte Id ergibt 404.","summary":"Setzt den Status des Mandanten auf `active` und zieht `updated_at` mit","x-nemix-summary-source":"description:first-sentence"}},"/admin/tenants/{id}/reset-password":{"post":{"responses":{"200":{"description":"ACHTUNG: es wurde KEINE E-Mail verschickt. Der Aufruf sucht bis zu zehn Nutzer des Mandanten, schreibt sie ins Serverprotokoll und meldet „Password-Reset-E-Mail wurde ausgeloest.\" — der Versand ueber Better Auth fehlt noch. `users` sind die Konten, die sie bekommen WUERDEN.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"versandGebaut":{"type":"boolean"},"users":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{}},"additionalProperties":false}}},"required":["message","versandGebaut","users"],"additionalProperties":false},"example":{"message":"string","versandGebaut":true,"users":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Der Mandant hat keine Nutzer (`no_users`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postAdminTenantsByIdReset-password","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Sucht bis zu zehn Nutzer des Mandanten und gibt sie zurueck. WICHTIG: es wird derzeit KEINE E-Mail verschickt — der Versand ueber Better Auth fehlt noch, der Aufruf schreibt die betroffenen Adressen nur ins Serverprotokoll. Auch die Einschraenkung auf Verwalter-Konten ist noch nicht wirksam: es kommen alle Nutzer zurueck, unabhaengig von der Rolle. Hat der Mandant gar keine Nutzer, antwortet der Aufruf 404 `no_users`.","summary":"Sucht bis zu zehn Nutzer des Mandanten und gibt sie zurueck","x-nemix-summary-source":"description:first-sentence"}},"/admin/tenants/{id}/users":{"get":{"responses":{"200":{"description":"Alle Nutzer des Mandanten, neueste zuerst — ohne Obergrenze und ohne Blaettern. Geloeschte Konten sind NICHT ausgenommen (anders als in der Nutzerzaehlung der Liste, die `deleted_at IS NULL` fordert). Ohne Datenbank kommt `{data: []}` mit 200 und ohne `total`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{},"role":{},"emailVerified":{},"lastLoginAt":{},"createdAt":{}},"additionalProperties":false}},"total":{"type":"number"}},"required":["data"],"additionalProperties":false},"example":{"data":[{}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getAdminTenantsByIdUsers","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest alle Nutzer mit dieser `tenant_id`, neueste zuerst — ohne Obergrenze und ohne Blaettern. Je Konto kommen id, E-Mail, Name, Rolle, Bestaetigungsstatus, letzte Anmeldung und Anlagedatum; Passwortdaten nicht. Geloeschte Konten sind hier NICHT ausgenommen, anders als bei der Nutzerzaehlung der Mandantenliste, die `deleted_at IS NULL` fordert — die beiden Zahlen koennen deshalb auseinandergehen.","summary":"Liest alle Nutzer mit dieser `tenant_id`, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/admin/tenants/{id}/usage":{"get":{"responses":{"200":{"description":"Umfang eines Mandanten. Von den vier Zahlen ist NUR `userCount` gemessen. `invoiceCount` steht seit dem 03.08.2026 fest auf 0 (die Rechnungszahl ist Geschaeftsvolumen und geht die Verwaltung nichts an), `storageUsedGb` und `apiCallsThisMonth` sind Platzhalter fuer eine Messwert-Tabelle, die es noch nicht gibt. Eine 0 heisst hier „wird nicht erhoben\", nicht „ist null\". ZWEITE FORM: fehlt die Datenbank, kommt ebenfalls 200 — dann aber mit `storage`/`apiCalls` statt `storageUsedGb`/`apiCallsThisMonth`. Wer nur die langen Namen liest, bekommt `undefined` und merkt den Ausfall nicht.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"tenantId":{"type":"string"},"slug":{"type":"string"},"userCount":{"type":"number"},"invoiceCount":{"type":"number"},"storageUsedGb":{"type":"number"},"apiCallsThisMonth":{"type":"number"}},"required":["tenantId","slug","userCount","invoiceCount","storageUsedGb","apiCallsThisMonth"],"additionalProperties":false},{"type":"object","properties":{"storage":{"type":"number"},"apiCalls":{"type":"number"},"invoiceCount":{"type":"number"}},"required":["storage","apiCalls","invoiceCount"],"additionalProperties":false}]},"example":{"tenantId":"string","slug":"string","userCount":0,"invoiceCount":0,"storageUsedGb":0,"apiCallsThisMonth":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getAdminTenantsByIdUsage","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Umfangs-Momentaufnahme eines Mandanten fuer die Verwaltungsuebersicht. GEMESSEN wird davon nur `userCount` (alle Konten mit dieser `tenant_id`); `storageUsedGb` und `apiCallsThisMonth` sind Platzhalter fuer eine Messwert-Tabelle, die es noch nicht gibt, und `invoiceCount` steht seit dem 03.08.2026 bewusst fest auf 0, weil die Rechnungszahl Geschaeftsvolumen ist. Eine 0 heisst hier also „wird nicht erhoben\", nicht „ist null\". Eine unbekannte Id ergibt 404.","summary":"Umfangs-Momentaufnahme eines Mandanten fuer die Verwaltungsuebersicht","x-nemix-summary-source":"description:first-sentence"}},"/admin/tenants/{id}/support":{"get":{"responses":{"200":{"description":"Konfiguration und Umbauten des Mandanten. Die drei Listen sind gekappt (Felder 500, Umbauten und Bau-Doku je 200) und jede Quelle ist einzeln abgesichert: fehlt eine Tabelle, kommt SIE leer und der Rest trotzdem. Ob eine leere Liste „nichts angepasst\" oder „nicht lesbar\" bedeutet, sagen `customFieldsLesbar` / `umbautenLesbar` / `bauDokuLesbar`; der Grund steht in `quellenFehler`.","content":{"application/json":{"schema":{"type":"object","properties":{"mandant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"],"additionalProperties":false},"customFields":{"type":"array","items":{}},"umbauten":{"type":"array","items":{}},"bauDoku":{"type":"array","items":{}},"customFieldsLesbar":{"type":"boolean"},"umbautenLesbar":{"type":"boolean"},"bauDokuLesbar":{"type":"boolean"},"quellenFehler":{"type":"array","items":{"type":"object","properties":{"quelle":{"type":"string"},"meldung":{"type":"string"}},"required":["quelle","meldung"],"additionalProperties":false}},"generatedAt":{"type":"string"}},"required":["mandant","customFields","umbauten","bauDoku","customFieldsLesbar","umbautenLesbar","bauDokuLesbar","quellenFehler","generatedAt"],"additionalProperties":false},"example":{"mandant":{"id":"string","slug":"string","name":"string"},"customFields":[],"umbauten":[],"bauDoku":[],"customFieldsLesbar":true,"umbautenLesbar":true,"bauDokuLesbar":true,"quellenFehler":[{"quelle":"string","meldung":"string"}],"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`tenant_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`database_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getAdminTenantsByIdSupport","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Support-Ansicht eines Mandanten, ohne Geschaeftsdaten","description":"Support-Ansicht: eigene Felder, Umbauten, Bau-Doku eines Mandanten (ohne Geschaeftsdaten)"}},"/admin/tenants/{id}/umbauten/zurueckrollen":{"post":{"responses":{"200":{"description":"Zurueckgerollt — die erfassten Werte bleiben erhalten","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"zurueckgerollt":{"type":"object","properties":{"kind":{"type":"string","enum":["custom_field","custom_entity","validation_rule","ui_config","relation","action","workflow"]},"entity":{"type":"string"},"artifactId":{"type":"string"}},"required":["kind","entity","artifactId"]},"geistBeseitigt":{"type":"boolean"},"hinweis":{"type":"string"}},"required":["ok","zurueckgerollt","hinweis"]},"example":{"ok":true,"zurueckgerollt":{"kind":"custom_field","entity":"string","artifactId":"string"},"geistBeseitigt":true,"hinweis":"string"}}}},"400":{"description":"Ungueltige Eingabe, unerlaubte Entitaet, oder eine Art ohne Rueckweg — die Antwort traegt dann `grund` im Klartext"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant oder Umbau nicht gefunden"},"503":{"description":"Datenbank nicht verfuegbar — ODER der Umbau wurde entfernt, das Manifest liess sich aber nicht stilllegen (`manifest_nicht_stillgelegt`)"}},"operationId":"postAdminTenantsByIdUmbautenZurueckrollen","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Umbau eines Mandanten zurueckrollen (Support)","description":"Nimmt einen Umbau eines Mandanten zurueck. Einen echten Rueckweg haben eigenes Feld (custom_field) und eigenes Modul (custom_entity); die uebrigen Arten antworten mit 400 UND dem Grund, warum es fuer sie keinen gibt. Fehlt der Umbau schon, ist aber im Manifest noch aktiv („Geist\"), wird der Manifest-Eintrag stillgelegt statt 404 zu melden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["custom_field","custom_entity","validation_rule","ui_config","relation","action","workflow"]},"entity":{"type":"string","minLength":1,"maxLength":64},"artifactId":{"type":"string","minLength":1,"maxLength":128},"grund":{"type":"string","maxLength":500}},"required":["kind","entity","artifactId"]},"example":{"kind":"custom_field","entity":"string","artifactId":"string","grund":"string"}}}}}},"/admin/tenants/{id}/anwenderdoku":{"get":{"responses":{"200":{"description":"Nach Entitaet gruppierte Anwenderdoku — ausschliesslich Einrichtung, keine Geschaeftsdaten des Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"tenant":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"]},"abschnitte":{"type":"array","items":{"type":"object","properties":{"entitaet":{"type":["string","null"],"description":"null = uebergreifend, ohne Entitaet"},"titel":{"type":"string"},"felder":{"type":"array","items":{"type":"object","properties":{"fieldId":{"type":"string"},"label":{"type":"string","description":"Fehlt das Label, steht hier der humanisierte Feld-Slug"},"typ":{"type":"string","description":"Datentyp in deutschen Worten"},"pflicht":{"type":"boolean"},"inListe":{"type":"boolean","description":"Ob das Feld in der Listenansicht als Spalte erscheint"},"erklaerung":{"type":["string","null"],"description":"Wunsch des Anwenders aus der Bau-Doku, sonst der Hilfetext des Feldes"},"seitWann":{"type":["string","null"]},"version":{"type":["number","null"],"description":"Versionsstand aus dem Manifest; null = kein Eintrag"},"stand":{"type":["string","null"]}},"required":["fieldId","label","typ","pflicht","inListe","erklaerung","seitWann","version","stand"]}},"automatiken":{"type":"array","items":{"type":"object","properties":{"art":{"type":"string"},"beschreibung":{"type":"string"},"erklaerung":{"type":["string","null"]},"seitWann":{"type":["string","null"]},"urheber":{"type":["string","null"]}},"required":["art","beschreibung","erklaerung","seitWann","urheber"]}}},"required":["entitaet","titel","felder","automatiken"]},"description":"Nach deutschem Titel sortiert; das Uebergreifende steht am Ende"},"erzeugtAm":{"type":"string","format":"date-time"}},"required":["tenant","abschnitte","erzeugtAm"]},"example":{"tenant":{"id":"string","name":"string","slug":"string"},"abschnitte":[{"entitaet":"string","titel":"string","felder":[{"fieldId":"string","label":"string","typ":"string","pflicht":true,"inListe":true,"erklaerung":"string","seitWann":"string","version":0,"stand":"string"}],"automatiken":[{"art":"string","beschreibung":"string","erklaerung":"string","seitWann":"string","urheber":"string"}]}],"erzeugtAm":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Super-Administratoren"},"404":{"description":"Mandant nicht gefunden"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getAdminTenantsByIdAnwenderdoku","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lesbare Anwenderdoku eines Mandanten: eigene Felder und Automatiken","description":"Lesbare Anwenderdoku eines Mandanten: eigene Felder je Entitaet mit Begruendung und Versionsstand plus Automatiken — ausschliesslich Einrichtung, keine Geschaeftsdaten."}},"/admin/tenants/{id}/gesundheit":{"get":{"responses":{"200":{"description":"Befundliste (leer = keine technischen Stolpersteine). War eine Quelle nicht lesbar, steht das als eigener Befund DRIN — die Liste ist dann nicht leer, und die uebrigen Pruefungen sagen nichts ueber diese eine aus.","content":{"application/json":{"schema":{"type":"object","properties":{"tenant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"}},"required":["id","slug"]},"befunde":{"type":"array","items":{"type":"object","properties":{"schwere":{"type":"string","description":"kritisch | warnung | info — in dieser Reihenfolge sortiert"},"titel":{"type":"string"},"erklaerung":{"type":"string","description":"Deutscher Klartext fuer den Support"},"quelle":{"type":"string","description":"Woher der Befund stammt, damit man nachsehen kann"}},"required":["schwere","titel","erklaerung","quelle"]},"description":"Leer = keine technischen Stolpersteine gefunden"},"geprueftAm":{"type":"string","description":"Zeitpunkt DIESER Pruefung — nichts wird zwischengespeichert"}},"required":["tenant","befunde","geprueftAm"]},"example":{"tenant":{"id":"string","slug":"string"},"befunde":[{"schwere":"string","titel":"string","erklaerung":"string","quelle":"string"}],"geprueftAm":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden — auch bei einer Id, die keine UUID ist"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getAdminTenantsByIdGesundheit","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Technische Stolpersteine eines Mandanten","description":"Technische Stolpersteine EINES Mandanten: fehlende Kerntabellen, KI-Kontingent, Einrichtungs-Aktivitaet, auseinanderlaufende Feld-Speicher (Registry vs. Manifest), Wissensbasis mit Platzhalter-Vektoren. Enthaelt keine Geschaeftsdaten des Mandanten."}},"/admin/customers/bulk/plan":{"post":{"responses":{"200":{"description":"Bulk update result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"},"targetPlan":{"type":"string","enum":["starter","professional","enterprise","trial"]}},"required":["updated","targetPlan"]},"example":{"updated":0,"targetPlan":"starter"}}}},"400":{"description":"Unknown plan name"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Bulk update failed"},"503":{"description":"Database unavailable"}},"operationId":"postAdminCustomersBulkPlan","tags":["admin","customers","bulk"],"parameters":[],"summary":"Bulk plan upgrade/downgrade for multiple tenants","description":"Repoints `tenants.plan_id` for 1…500 tenant ids in a single UPDATE and reports how many rows it actually hit — ids that match no tenant are silently skipped, so `updated` can be lower than the list sent. Unlike the single-tenant plan change this writes NO proration record. A `bulk.plan_change` entry goes to public.admin_audit_log best-effort. 400 when the plan name is unknown; no tenant is touched then.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"targetPlan":{"type":"string","enum":["starter","professional","enterprise","trial"]}},"required":["tenantIds","targetPlan"]},"example":{"tenantIds":["00000000-0000-4000-8000-000000000000"],"targetPlan":"starter"}}}}}},"/admin/customers/bulk/suspend":{"post":{"responses":{"200":{"description":"Bulk suspend result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}},"required":["updated"]},"example":{"updated":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Bulk suspend failed"},"503":{"description":"Database unavailable"}},"operationId":"postAdminCustomersBulkSuspend","tags":["admin","customers","bulk"],"parameters":[],"summary":"Bulk suspend tenants (login blocked)","description":"Sets `tenants.status` to \"suspended\" for 1…500 ids in one UPDATE — regardless of the previous status, so an already suspended tenant counts as updated too. `updated` reports the rows actually hit; unknown ids are skipped without an error. The optional `reason` is not stored on the tenant, it only travels into the best-effort `bulk.suspend` entry in public.admin_audit_log. Reversible via POST /bulk/activate.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"reason":{"type":"string","maxLength":500}},"required":["tenantIds"]},"example":{"tenantIds":["00000000-0000-4000-8000-000000000000"],"reason":"string"}}}}}},"/admin/customers/bulk/activate":{"post":{"responses":{"200":{"description":"Bulk activate result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}},"required":["updated"]},"example":{"updated":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Bulk activate failed"},"503":{"description":"Database unavailable"}},"operationId":"postAdminCustomersBulkActivate","tags":["admin","customers","bulk"],"parameters":[],"summary":"Bulk reactivate suspended/trial tenants","description":"Sets `tenants.status` to \"active\" for 1…500 ids in one UPDATE. The previous status is not checked, so a trial tenant loses its trial marker here as well. `updated` reports the rows actually hit; unknown ids are skipped without an error. Writes a best-effort `bulk.activate` entry to public.admin_audit_log.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500}},"required":["tenantIds"]},"example":{"tenantIds":["00000000-0000-4000-8000-000000000000"]}}}}}},"/admin/customers/bulk/email":{"post":{"responses":{"200":{"description":"Empfaenger ermittelt — nichts verschickt","content":{"application/json":{"schema":{"type":"object","properties":{"versandBereit":{"type":"integer"},"versendet":{"type":"number","const":0},"hinweis":{"type":"string"},"recipients":{"type":"array","items":{"type":"string"}}},"required":["versandBereit","versendet","hinweis","recipients"]},"example":{"versandBereit":0,"versendet":0,"hinweis":"string","recipients":["string"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Empfaenger konnten nicht ermittelt werden"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"postAdminCustomersBulkEmail","tags":["admin","customers","bulk"],"parameters":[],"description":"Ermittelt zu jedem Mandanten die E-Mail des Haupt-Administrators: je Mandant genau ein Nutzer, Rolle \"admin\" zuerst, sonst der aelteste; geloeschte Nutzer bleiben auszen vor. Mandanten ohne passenden Nutzer fallen still heraus, `versandBereit` kann darum kleiner sein als die Zahl der gesendeten IDs. Es wird NICHTS verschickt: ein Versender ist nicht gebaut. `subject`, `body` und `fromName` werden geprueft, aber nur der Betreff landet im Audit-Eintrag `bulk.email` — der Rumpf wird nirgends abgelegt. Die Antwort nennt die Empfaengerzahl (versandBereit), versendet=0 und einen Hinweis.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"subject":{"type":"string","minLength":2,"maxLength":200},"body":{"type":"string","minLength":2,"maxLength":20000},"fromName":{"type":"string","maxLength":100}},"required":["tenantIds","subject","body"]},"example":{"tenantIds":["00000000-0000-4000-8000-000000000000"],"subject":"string","body":"string","fromName":"string"}}}},"summary":"Ermittelt zu jedem Mandanten die E-Mail des Haupt-Administrators","x-nemix-summary-source":"description:first-sentence"}},"/admin/customers/clone":{"post":{"responses":{"201":{"description":"Der Klon steht. `tablesCloned` und `rowsCopied` sagen, wie viel tatsaechlich kopiert wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"tenantId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"tenantNumber":{"type":["string","null"]},"tablesCloned":{"type":"integer"},"rowsCopied":{"type":"integer"}},"required":["ok","tenantId","slug","name","tenantNumber","tablesCloned","rowsCopied"]},"example":{"ok":true,"tenantId":"string","slug":"string","name":"string","tenantNumber":"string","tablesCloned":0,"rowsCopied":0}}}},"400":{"description":"Quelle nicht bestimmbar (`source_required`), Slug ungueltig (`invalid_slug`), oder der Rumpf haelt das Schema nicht ein (`newName` fehlt, `newSlug` passt nicht auf das Muster)."},"401":{"description":"Keine Benutzer-ID im Kontext (`{ \"error\": \"unauthorized\" }`)."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"404":{"description":"Quell-Mandant nicht gefunden (`source_missing`)."},"409":{"description":"Slug oder Ziel-Schema existiert bereits (`slug_taken`) — es wurde nichts angelegt und nichts zusammengefuehrt."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"500":{"description":"Klon fehlgeschlagen (`clone_failed`); die Transaktion ist zurueckgerollt, es bleibt kein halber Mandant zurueck."},"503":{"description":"Keine Datenbankverbindung (`db_unavailable`)."}},"operationId":"postAdminCustomersClone","tags":["admin","tenants"],"parameters":[],"summary":"Mandanten als vollstaendige Dublette klonen","description":"Legt einen NEUEN, eigenstaendigen Mandanten an, der eine vollstaendige Dublette eines bestehenden ist — Struktur UND Daten. Gedacht fuer Test-/Experimentiermandanten.\n\nWIE VIEL ANGELEGT WIRD — es ist kein Auszug, es ist alles:\n- eine neue Zeile in `public.tenants` (eigene Mandantennummer, Status `active`, Tarif/Pakete/Einstellungen/KI-Konfiguration/Branding/Steuernummer von der Quelle uebernommen)\n- ein neues Postgres-Schema `tenant_<newSlug>`\n- JEDE Basistabelle des Quell-Schemas, angelegt per `CREATE TABLE (LIKE … INCLUDING ALL)` (Spalten, Vorgaben, Pruefregeln, Indizes — KEINE Fremdschluessel)\n- JEDE ZEILE dieser Tabellen, kopiert per `INSERT … SELECT` ohne `WHERE` und ohne `LIMIT`\n\nES GIBT KEINE OBERGRENZE. Keine Zeilen-, Tabellen- oder Groessenschranke, keine Vorabpruefung, kein Trockenlauf. Ein Mandant mit Millionen Zeilen wird mit Millionen Zeilen kopiert; die Antwort nennt hinterher `tablesCloned` und `rowsCopied`. Alles laeuft in EINER Transaktion (DDL eingeschlossen) — bricht etwas ab, ist nichts angelegt, aber bis dahin haelt der Vorgang Sperren und Plattenplatz.\n\nNEBENWIRKUNGEN AUF DIE QUELLE UND AUF DEN AUFRUFER: der Handler sichert den Zugang ueber die Organisationsebene. Hat der Quell-Mandant keine Organisation, wird eine angelegt (`<Name> (Gruppe)`, Tarif `enterprise`). Der aufrufende Benutzer wird Mitglied (`owner`) und bekommt Zugriff auf den NEUEN UND den QUELL-Mandanten. Anschliessend wird `organization_id` bei beiden Mandanten gesetzt, falls sie noch leer war — der Quell-Mandant wird also mit veraendert.\n\nQUELLE: `sourceSlug` nennt den Quell-Mandanten. Fehlt er, greift der Handler auf den Mandanten der Sitzung zurueck — den setzt aber nur `tenantMiddleware`, und die haengt nicht an der admin-Sub-App. AN DIESEN PFADEN IST `sourceSlug` DAHER IN DER PRAXIS PFLICHT; ohne ihn kommt 400 `source_required`. An der Laufzeit nachgemessen (30.08.2026).\n\nZIEL-SLUG: `newSlug` ist optional. Ohne Angabe wird er aus `newName` abgeleitet und um `-test-<vier Zeichen>` ergaenzt (Zeitstempel zur Basis 36). Erlaubt ist `^[a-z][a-z0-9-]{1,40}$`.\n\nNICHT WIEDERHOLBAR, KEIN ZUSAMMENFUEHREN: existiert der Slug oder das Ziel-Schema schon, endet der Aufruf mit 409 — es wird nichts ergaenzt und nichts ueberschrieben.\n\nBEKANNTER KOMPROMISS: `LIKE INCLUDING DEFAULTS` uebernimmt SERIAL-Vorgaben als Verweis auf die SEQUENZ DER QUELLE. Der Klon zaehlt an dieser Stelle also mit der Quelle mit. Fuer Experimentiermandanten hingenommen, fuer einen produktiven Zwilling nicht geeignet.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sourceSlug":{"type":"string","minLength":2,"maxLength":63},"newName":{"type":"string","minLength":1,"maxLength":255},"newSlug":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"}},"required":["newName"]},"example":{"sourceSlug":"string","newName":"string"}}}}}},"/admin/customers/stats":{"get":{"responses":{"200":{"description":"Zaehlung nach Status. Ohne Datenbank kommen ebenfalls 200 und lauter Nullen — eine 0 heisst hier also „keiner\" ODER „nicht messbar\".","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"number"},"active":{"type":"number"},"trial":{"type":"number"},"suspended":{"type":"number"},"cancelled":{"type":"number"}},"required":["total","active","trial","suspended"]},"example":{"total":0,"active":0,"trial":0,"suspended":0,"cancelled":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminCustomersStats","tags":["admin","tenants"],"parameters":[],"description":"Zaehlt die Mandanten nach Status. Eine einzige Abfrage auf `public.tenants` liefert total, active, trial, suspended und cancelled ueber COUNT(*) FILTER; Nutzer-, Tarif- oder Verbrauchsdaten werden dafuer nicht gelesen. Filter oder Blaetterung gibt es nicht. Der Endpunkt ist bewusst VOR `/:id` registriert, sonst laese der Router „stats\" als Mandanten-Id.","summary":"Zaehlt die Mandanten nach Status","x-nemix-summary-source":"description:first-sentence"}},"/admin/customers":{"get":{"responses":{"200":{"description":"Eine Seite der Mandantenliste. `total` zaehlt mit denselben Bedingungen wie die Liste. Ohne Datenbank kommt `{data: [], total: 0}` — ebenfalls mit 200 und ohne `page`/`limit`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"slug":{},"name":{},"tenantNumber":{},"status":{},"createdAt":{},"updatedAt":{},"plan":{},"planPrice":{},"userCount":{"type":"number"}},"required":["userCount"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"},"error":{"type":"string"}},"required":["data","total"]},"example":{"data":[{"userCount":0}],"total":0,"page":0,"limit":0,"error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — die Antwort traegt eine LEERE Liste UND den rohen Fehlertext im Feld `error`","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"slug":{},"name":{},"tenantNumber":{},"status":{},"createdAt":{},"updatedAt":{},"plan":{},"planPrice":{},"userCount":{"type":"number"}},"required":["userCount"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"},"error":{"type":"string"}},"required":["data","total"]}}}}},"operationId":"getAdminCustomers","tags":["admin","tenants"],"parameters":[],"description":"Liest eine Seite aus `public.tenants`. Je Zeile kommen Tarifname und Monatspreis aus `plans` sowie die Anzahl der nicht geloeschten Nutzer aus `users` dazu. `page` beginnt bei 1, `limit` liegt zwischen 1 und 100 (Vorgabe 50); gefiltert wird ueber `status`, `plan` (Tarifname) und `search` (Teiltreffer in Name oder Slug). Sortiert nach Anlagedatum, neueste zuerst. `total` zaehlt mit denselben Bedingungen wie die Liste — die Zahl passt also zur Filterung.","summary":"Liest eine Seite aus `public.tenants`","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Mandant samt Verwalter-Konto angelegt. ACHTUNG: `tempPassword` steht im Klartext in dieser Antwort — einmalig, aber ungeschuetzt. Sie gehoert nicht in Protokolle oder Zwischenspeicher; das Passwort ist ueber einen sicheren Kanal weiterzugeben. Eine Willkommensmail geht nebenher raus, ihr Scheitern aendert die Antwort NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{},"userId":{},"slug":{"type":"string"},"companyName":{"type":"string"},"plan":{"type":"string"},"status":{"type":"string","const":"trial"},"loginUrl":{"type":"string"},"tempPassword":{"type":"string"}},"required":["slug","companyName","plan","status","loginUrl","tempPassword"],"additionalProperties":false},"example":{"slug":"string","companyName":"string","plan":"string","status":"trial","loginUrl":"string","tempPassword":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"E-Mail bereits vergeben (`duplicate_email`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Anlegen fehlgeschlagen (`create_failed`) — `message` traegt den Grund","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postAdminCustomers","tags":["admin","tenants"],"parameters":[],"description":"Legt Mandant UND erstes Verwalter-Konto in einem Aufruf an. Der Slug entsteht aus dem Firmennamen und wird bei Kollision mit `-1`, `-2` … eindeutig gemacht (nach 99 Versuchen bricht der Aufruf ab). Der Mandant startet mit Status `trial` und dem Paket `core`; ist der genannte Tarif unbekannt, faellt die Zuordnung auf `trial` zurueck. Das Konto bekommt die Rolle `admin` und ein einmalig zurueckgegebenes Zufallspasswort — scheitert seine Anlage, wird die eben erzeugte Mandantenzeile wieder entfernt. Eine Willkommensmail geht nebenher raus; ihr Scheitern aendert das Ergebnis nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string","minLength":2,"maxLength":255},"firstName":{"type":"string","minLength":1,"maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email"},"plan":{"type":"string","enum":["starter","professional","enterprise"],"default":"starter"},"sitzland":{"type":"string","pattern":"^[A-Z]{2}$"},"profil":{"type":"object","properties":{"branchen":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":200,"default":[]},"groesse":{"type":"string","maxLength":32,"default":""},"laender":{"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"maxItems":200,"default":[]},"sprachen":{"type":"array","items":{"type":"string","minLength":2,"maxLength":8},"maxItems":200,"default":[]}}}},"required":["companyName","firstName","lastName","email","sitzland"]}}}},"summary":"Legt Mandant UND erstes Verwalter-Konto in einem Aufruf an","x-nemix-summary-source":"description:first-sentence"}},"/admin/customers/{id}":{"get":{"responses":{"200":{"description":"Ein Mandant — die ganze Tabellenzeile (`t.*`) plus Plan-Name, Preis, Nutzer- und Speichergrenze und die gezaehlten Nutzer. Welche Spalten `t.*` umfasst, bestimmt das Tabellenschema, nicht diese Route.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getAdminCustomersById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest einen Mandanten ueber seine Id. Zur vollstaendigen Tabellenzeile kommen Tarifname, Monatspreis, Nutzer- und Speichergrenze aus `plans` sowie die Zahl der nicht geloeschten Nutzer. Ein Zugriff ueber den Slug ist hier nicht vorgesehen. Eine unbekannte Id ergibt 404, nicht eine leere Zeile.","summary":"Liest einen Mandanten ueber seine Id","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Geaendert — die Antwort traegt NUR id, name und status zurueck, nicht den ganzen Mandanten. Ein unbekannter Planname wird still uebergangen: kam daneben ein anderes Feld, meldet der Aufruf 200, obwohl der Plan unveraendert blieb; kam nur der Plan, meldet er 400 `no_changes`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Kein aenderbares Feld im Rumpf (`no_changes`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Aenderung fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchAdminCustomersById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Namen, Tarif oder Status eines Mandanten aendern","description":"Aendert Name, Tarif und/oder Status eines Mandanten; nicht mitgeschickte Felder bleiben unberuehrt. Der Tarif wird ueber seinen Namen in `plans` aufgeloest — ein unbekannter Name wird still uebergangen und aendert nichts. Bleibt danach kein einziges Feld zum Schreiben uebrig, antwortet der Aufruf 400 `no_changes`. `updated_at` wird bei jeder echten Aenderung mitgezogen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":255},"plan":{"type":"string","enum":["starter","professional","enterprise","trial"]},"status":{"type":"string","enum":["active","trial","suspended","cancelled"]}}},"example":{"name":"string","plan":"starter","status":"active"}}}}},"delete":{"responses":{"200":{"description":"Geloescht. Die Antwort sagt, was wirklich fiel.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"schemaGeloescht":{"type":"string","description":"Name des verworfenen Mandanten-Schemas"},"organisationGeloescht":{"type":"boolean","description":"true, wenn die Gruppe leer zurueckblieb"},"kundennummer":{"type":["string","null"],"description":"Die sechsstellige Nummer der Prozessdatenbank, die die Oberflaeche noch wegraeumen muss. null, wenn der Mandant keine Nummer trug."}},"required":["ok","tenantId","slug","name","schemaGeloescht","organisationGeloescht","kundennummer"]},"example":{"ok":true,"tenantId":"string","slug":"string","name":"string","schemaGeloescht":"string","organisationGeloescht":true,"kundennummer":"string"}}}},"400":{"description":"Bestaetigung fehlt oder passt nicht zum Slug (`confirmation_mismatch`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"404":{"description":"Mandant nicht gefunden (`not_found`)"},"409":{"description":"Ein Fremdschluessel haelt den Mandanten (`in_use`) — `message` nennt die Einschraenkung. Es wurde NICHTS geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)"}},"operationId":"deleteAdminCustomersById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mandanten endgueltig loeschen, mit allen Daten","description":"Verwirft das Mandanten-Schema `tenant_<slug>` samt Inhalt und loescht die Zeile in `public.tenants`. Per ON DELETE CASCADE fallen mit: Benutzer (und daran Sitzungen und Konten), API-Schluessel, `organization_tenant_access` und Ebenen. NICHT MIT FALLEN `support_sessions` und `support_session_events`: das Protokoll haelt fest, wer vom Hersteller in diesen Mandanten gesehen hat, und ein Nachweis, der mit seinem Gegenstand verschwindet, ist keiner (Fremdschluessel geloest in 20260910163000_support_protokoll_ueberlebt_den_mandanten.sql). Die Organisation faellt NUR, wenn kein anderer Mandant mehr an ihr haengt. BEIDES IN EINER TRANSAKTION: entweder alles oder nichts. NICHT UMKEHRBAR — es gibt kein Wiederherstellen. Verlangt `?bestaetigung=<slug>`; stimmt sie nicht, 400 und es passiert nichts. Die Prozessdatenbank der Oberflaeche liegt im Dateisystem und faellt NICHT mit; die Antwort nennt ihre Nummer. Nur fuer `super_admin`, sonst 403."}},"/admin/customers/{id}/suspend":{"post":{"responses":{"200":{"description":"Gesperrt (`status=suspended`). Die Antwort traegt nur id, name und status. Laufende Sitzungen des Mandanten beendet dieser Aufruf NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postAdminCustomersByIdSuspend","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt den Status des Mandanten auf `suspended` und zieht `updated_at` mit. Mehr passiert nicht: Daten, Tarif und Pakete bleiben unberuehrt, und bereits laufende Sitzungen des Mandanten beendet der Aufruf NICHT. Rueckgaengig ueber `POST /api/admin/tenants/{id}/activate`. Eine unbekannte Id ergibt 404.","summary":"Setzt den Status des Mandanten auf `suspended` und zieht `updated_at` mit","x-nemix-summary-source":"description:first-sentence"}},"/admin/customers/{id}/activate":{"post":{"responses":{"200":{"description":"Freigeschaltet (`status=active`). Die Antwort traegt nur id, name und status.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postAdminCustomersByIdActivate","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt den Status des Mandanten auf `active` und zieht `updated_at` mit — die Gegenrichtung zu `suspend`. Der vorherige Status wird nicht geprueft: der Aufruf wirkt auf einen gesperrten wie auf einen Test-Mandanten und laesst einen bereits aktiven unveraendert aktiv. Tarif, Pakete und Grenzen ruehrt er nicht an. Eine unbekannte Id ergibt 404.","summary":"Setzt den Status des Mandanten auf `active` und zieht `updated_at` mit","x-nemix-summary-source":"description:first-sentence"}},"/admin/customers/{id}/reset-password":{"post":{"responses":{"200":{"description":"ACHTUNG: es wurde KEINE E-Mail verschickt. Der Aufruf sucht bis zu zehn Nutzer des Mandanten, schreibt sie ins Serverprotokoll und meldet „Password-Reset-E-Mail wurde ausgeloest.\" — der Versand ueber Better Auth fehlt noch. `users` sind die Konten, die sie bekommen WUERDEN.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"versandGebaut":{"type":"boolean"},"users":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{}},"additionalProperties":false}}},"required":["message","versandGebaut","users"],"additionalProperties":false},"example":{"message":"string","versandGebaut":true,"users":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Der Mandant hat keine Nutzer (`no_users`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postAdminCustomersByIdReset-password","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Sucht bis zu zehn Nutzer des Mandanten und gibt sie zurueck. WICHTIG: es wird derzeit KEINE E-Mail verschickt — der Versand ueber Better Auth fehlt noch, der Aufruf schreibt die betroffenen Adressen nur ins Serverprotokoll. Auch die Einschraenkung auf Verwalter-Konten ist noch nicht wirksam: es kommen alle Nutzer zurueck, unabhaengig von der Rolle. Hat der Mandant gar keine Nutzer, antwortet der Aufruf 404 `no_users`.","summary":"Sucht bis zu zehn Nutzer des Mandanten und gibt sie zurueck","x-nemix-summary-source":"description:first-sentence"}},"/admin/customers/{id}/users":{"get":{"responses":{"200":{"description":"Alle Nutzer des Mandanten, neueste zuerst — ohne Obergrenze und ohne Blaettern. Geloeschte Konten sind NICHT ausgenommen (anders als in der Nutzerzaehlung der Liste, die `deleted_at IS NULL` fordert). Ohne Datenbank kommt `{data: []}` mit 200 und ohne `total`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{},"role":{},"emailVerified":{},"lastLoginAt":{},"createdAt":{}},"additionalProperties":false}},"total":{"type":"number"}},"required":["data"],"additionalProperties":false},"example":{"data":[{}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getAdminCustomersByIdUsers","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest alle Nutzer mit dieser `tenant_id`, neueste zuerst — ohne Obergrenze und ohne Blaettern. Je Konto kommen id, E-Mail, Name, Rolle, Bestaetigungsstatus, letzte Anmeldung und Anlagedatum; Passwortdaten nicht. Geloeschte Konten sind hier NICHT ausgenommen, anders als bei der Nutzerzaehlung der Mandantenliste, die `deleted_at IS NULL` fordert — die beiden Zahlen koennen deshalb auseinandergehen.","summary":"Liest alle Nutzer mit dieser `tenant_id`, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/admin/customers/{id}/usage":{"get":{"responses":{"200":{"description":"Umfang eines Mandanten. Von den vier Zahlen ist NUR `userCount` gemessen. `invoiceCount` steht seit dem 03.08.2026 fest auf 0 (die Rechnungszahl ist Geschaeftsvolumen und geht die Verwaltung nichts an), `storageUsedGb` und `apiCallsThisMonth` sind Platzhalter fuer eine Messwert-Tabelle, die es noch nicht gibt. Eine 0 heisst hier „wird nicht erhoben\", nicht „ist null\". ZWEITE FORM: fehlt die Datenbank, kommt ebenfalls 200 — dann aber mit `storage`/`apiCalls` statt `storageUsedGb`/`apiCallsThisMonth`. Wer nur die langen Namen liest, bekommt `undefined` und merkt den Ausfall nicht.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"tenantId":{"type":"string"},"slug":{"type":"string"},"userCount":{"type":"number"},"invoiceCount":{"type":"number"},"storageUsedGb":{"type":"number"},"apiCallsThisMonth":{"type":"number"}},"required":["tenantId","slug","userCount","invoiceCount","storageUsedGb","apiCallsThisMonth"],"additionalProperties":false},{"type":"object","properties":{"storage":{"type":"number"},"apiCalls":{"type":"number"},"invoiceCount":{"type":"number"}},"required":["storage","apiCalls","invoiceCount"],"additionalProperties":false}]},"example":{"tenantId":"string","slug":"string","userCount":0,"invoiceCount":0,"storageUsedGb":0,"apiCallsThisMonth":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getAdminCustomersByIdUsage","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Umfangs-Momentaufnahme eines Mandanten fuer die Verwaltungsuebersicht. GEMESSEN wird davon nur `userCount` (alle Konten mit dieser `tenant_id`); `storageUsedGb` und `apiCallsThisMonth` sind Platzhalter fuer eine Messwert-Tabelle, die es noch nicht gibt, und `invoiceCount` steht seit dem 03.08.2026 bewusst fest auf 0, weil die Rechnungszahl Geschaeftsvolumen ist. Eine 0 heisst hier also „wird nicht erhoben\", nicht „ist null\". Eine unbekannte Id ergibt 404.","summary":"Umfangs-Momentaufnahme eines Mandanten fuer die Verwaltungsuebersicht","x-nemix-summary-source":"description:first-sentence"}},"/admin/customers/{id}/support":{"get":{"responses":{"200":{"description":"Konfiguration und Umbauten des Mandanten. Die drei Listen sind gekappt (Felder 500, Umbauten und Bau-Doku je 200) und jede Quelle ist einzeln abgesichert: fehlt eine Tabelle, kommt SIE leer und der Rest trotzdem. Ob eine leere Liste „nichts angepasst\" oder „nicht lesbar\" bedeutet, sagen `customFieldsLesbar` / `umbautenLesbar` / `bauDokuLesbar`; der Grund steht in `quellenFehler`.","content":{"application/json":{"schema":{"type":"object","properties":{"mandant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"],"additionalProperties":false},"customFields":{"type":"array","items":{}},"umbauten":{"type":"array","items":{}},"bauDoku":{"type":"array","items":{}},"customFieldsLesbar":{"type":"boolean"},"umbautenLesbar":{"type":"boolean"},"bauDokuLesbar":{"type":"boolean"},"quellenFehler":{"type":"array","items":{"type":"object","properties":{"quelle":{"type":"string"},"meldung":{"type":"string"}},"required":["quelle","meldung"],"additionalProperties":false}},"generatedAt":{"type":"string"}},"required":["mandant","customFields","umbauten","bauDoku","customFieldsLesbar","umbautenLesbar","bauDokuLesbar","quellenFehler","generatedAt"],"additionalProperties":false},"example":{"mandant":{"id":"string","slug":"string","name":"string"},"customFields":[],"umbauten":[],"bauDoku":[],"customFieldsLesbar":true,"umbautenLesbar":true,"bauDokuLesbar":true,"quellenFehler":[{"quelle":"string","meldung":"string"}],"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`tenant_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`database_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getAdminCustomersByIdSupport","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Support-Ansicht eines Mandanten, ohne Geschaeftsdaten","description":"Support-Ansicht: eigene Felder, Umbauten, Bau-Doku eines Mandanten (ohne Geschaeftsdaten)"}},"/admin/customers/{id}/umbauten/zurueckrollen":{"post":{"responses":{"200":{"description":"Zurueckgerollt — die erfassten Werte bleiben erhalten","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"zurueckgerollt":{"type":"object","properties":{"kind":{"type":"string","enum":["custom_field","custom_entity","validation_rule","ui_config","relation","action","workflow"]},"entity":{"type":"string"},"artifactId":{"type":"string"}},"required":["kind","entity","artifactId"]},"geistBeseitigt":{"type":"boolean"},"hinweis":{"type":"string"}},"required":["ok","zurueckgerollt","hinweis"]},"example":{"ok":true,"zurueckgerollt":{"kind":"custom_field","entity":"string","artifactId":"string"},"geistBeseitigt":true,"hinweis":"string"}}}},"400":{"description":"Ungueltige Eingabe, unerlaubte Entitaet, oder eine Art ohne Rueckweg — die Antwort traegt dann `grund` im Klartext"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant oder Umbau nicht gefunden"},"503":{"description":"Datenbank nicht verfuegbar — ODER der Umbau wurde entfernt, das Manifest liess sich aber nicht stilllegen (`manifest_nicht_stillgelegt`)"}},"operationId":"postAdminCustomersByIdUmbautenZurueckrollen","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Umbau eines Mandanten zurueckrollen (Support)","description":"Nimmt einen Umbau eines Mandanten zurueck. Einen echten Rueckweg haben eigenes Feld (custom_field) und eigenes Modul (custom_entity); die uebrigen Arten antworten mit 400 UND dem Grund, warum es fuer sie keinen gibt. Fehlt der Umbau schon, ist aber im Manifest noch aktiv („Geist\"), wird der Manifest-Eintrag stillgelegt statt 404 zu melden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["custom_field","custom_entity","validation_rule","ui_config","relation","action","workflow"]},"entity":{"type":"string","minLength":1,"maxLength":64},"artifactId":{"type":"string","minLength":1,"maxLength":128},"grund":{"type":"string","maxLength":500}},"required":["kind","entity","artifactId"]},"example":{"kind":"custom_field","entity":"string","artifactId":"string","grund":"string"}}}}}},"/admin/customers/{id}/plan":{"post":{"responses":{"200":{"description":"Plan changed","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"targetPlan":{"type":"string","enum":["starter","professional","enterprise","trial"]},"planId":{"type":"string"},"monthlyPriceEur":{"type":["string","null"]}},"required":["tenantId","targetPlan","planId","monthlyPriceEur"]},"example":{"tenantId":"string","targetPlan":"starter","planId":"string","monthlyPriceEur":"string"}}}},"400":{"description":"Unknown plan name"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not found"},"500":{"description":"Plan change failed"},"503":{"description":"Database unavailable"}},"operationId":"postAdminCustomersByIdPlan","tags":["admin","customers","billing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Change tenant plan (upgrade/downgrade), with optional proration record","description":"Looks the target plan up by name in public.plans, then repoints `tenants.plan_id` and bumps `tenants.updated_at`. A row is appended to public.tenant_plan_changes carrying `prorate` and `effectiveAt` — but only best-effort: if that table is missing the plan change still stands, only the proration record is lost. Same for the `billing.plan_change` entry in public.admin_audit_log. Nothing is charged or refunded here; the endpoint only records the change. 400 when the plan name is unknown, 404 when the tenant does not exist.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"targetPlan":{"type":"string","enum":["starter","professional","enterprise","trial"]},"effectiveAt":{"type":"string","format":"date-time"},"prorate":{"type":"boolean","default":true}},"required":["targetPlan"]},"example":{"targetPlan":"starter","effectiveAt":"2026-01-01T12:00:00.000Z","prorate":true}}}}}},"/admin/customers/{id}/billing":{"get":{"responses":{"200":{"description":"Billing summary","content":{"application/json":{"schema":{"type":"object","properties":{"tenant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"plan_name":{"type":["string","null"]},"price_eur_monthly":{"type":["string","null"]},"status":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","slug","name","plan_name","price_eur_monthly","status","created_at"]},"invoices":{"type":"array","items":{"type":"object","properties":{"id":{"anyOf":[{"type":"string"},{"type":"number"}]},"period_start":{"type":["string","null"]},"period_end":{"type":["string","null"]},"amount_eur":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"status":{"type":["string","null"]},"paid_at":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","period_start","period_end","amount_eur","status","paid_at","created_at"]}},"paymentMethod":{"type":"null"},"paymentMethodQuelle":{"type":"string","const":"nicht_erhoben"},"mrrEur":{"type":"number"}},"required":["tenant","invoices","paymentMethod","paymentMethodQuelle","mrrEur"]},"example":{"tenant":{"id":"string","slug":"string","name":"string","plan_name":"string","price_eur_monthly":"string","status":"string","created_at":"string"},"invoices":[{"id":"string","period_start":"string","period_end":"string","amount_eur":"string","status":"string","paid_at":"string","created_at":"string"}],"paymentMethod":null,"paymentMethodQuelle":"nicht_erhoben","mrrEur":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not found"},"500":{"description":"Billing summary could not be read"},"503":{"description":"Database unavailable"}},"operationId":"getAdminCustomersByIdBilling","tags":["admin","customers","billing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Tenant billing overview: current plan, invoice history, payment method","description":"Joins the tenant against its plan and appends up to 24 rows from public.tenant_invoices, newest billing period first; if that table is missing the list stays empty instead of failing. `mrrEur` is simply the plan's monthly price as a number. The payment method is deliberately NOT reported: it lives at Stripe and is not mirrored here, so `paymentMethod` is always null and `paymentMethodQuelle` says \"nicht_erhoben\" — that means \"not collected\", not \"the customer has none\". 404 when the tenant does not exist."}},"/admin/customers/{id}/audit-log":{"get":{"responses":{"200":{"description":"Audit log entries, newest first — `source` names the table that answered","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"anyOf":[{"type":"string"},{"type":"number"}]},"action":{"type":"string"},"actorUserId":{"type":["string","null"]},"targetTenantId":{"type":["string","null"]},"payload":{},"createdAt":{"type":"string"}},"required":["id","action","actorUserId","targetTenantId","createdAt"]}},"source":{"type":"string","enum":["admin_audit_log","audit_log","none"]},"message":{"type":"string"}},"required":["data"]},"example":{"data":[{"id":"string","action":"string","actorUserId":"string","targetTenantId":"string","createdAt":"string"}],"source":"admin_audit_log","message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Unexpected error while reading the audit log"}},"operationId":"getAdminCustomersByIdAudit-log","tags":["admin","customers","audit"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Last 100 audit entries for a tenant, across admin and tenant actions","description":"Reads public.admin_audit_log newest first; if that table is missing the endpoint silently falls back to public.audit_log with the same column aliases, and if neither exists it answers 200 with an empty list and `source: \"none\"`. The `source` field names which table actually answered. Page size comes from the `limit` query parameter — default 100, clamped to 1…500. Read-only; no table is created."}},"/admin/customers/{id}/notes":{"get":{"responses":{"200":{"description":"Notes — pinned first, then newest first, at most 200","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"pinned":{"type":"boolean"},"visibility":{"type":"string"},"authorUserId":{"type":["string","null"]},"authorName":{"type":["string","null"]},"authorEmail":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","body","pinned","visibility","authorUserId","authorName","authorEmail","createdAt","updatedAt"]}},"message":{"type":"string"}},"required":["data"]},"example":{"data":[{"id":"string","body":"string","pinned":true,"visibility":"string","authorUserId":"string","authorName":"string","authorEmail":"string","createdAt":"string","updatedAt":"string"}],"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminCustomersByIdNotes","tags":["admin","customers","notes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Internal support notes for a tenant (NOT visible to tenant)","description":"Reads public.admin_tenant_notes for the tenant, pinned entries first, then newest first, capped at 200 rows — there is no paging. Each row is joined to public.users to carry the author name and e-mail. Answers 200 with an empty list even when the notes table does not exist; the `message` field then says so."},"post":{"responses":{"201":{"description":"Note created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"pinned":{"type":"boolean"},"visibility":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","body","pinned","visibility","createdAt"]}},"required":["data"]},"example":{"data":{"id":"string","body":"string","pinned":true,"visibility":"string","createdAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Insert failed — e.g. the notes table does not exist"},"503":{"description":"Database unavailable"}},"operationId":"postAdminCustomersByIdNotes","tags":["admin","customers","notes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Add an internal support note to a tenant","description":"Inserts one row into public.admin_tenant_notes. `body` is required (1…5000 characters); `pinned` defaults to false and `visibility` to \"internal\". The author is taken from the calling admin session, not from the body. The response carries only the columns of the INSERT … RETURNING, so author and updatedAt are absent here — read them back via the list endpoint.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":5000},"pinned":{"type":"boolean","default":false},"visibility":{"type":"string","enum":["internal","support"],"default":"internal"}},"required":["body"]},"example":{"body":"string","pinned":true,"visibility":"internal"}}}}}},"/admin/customers/{id}/notes/{noteId}":{"delete":{"responses":{"200":{"description":"Note deleted (or nothing matched)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Delete failed"},"503":{"description":"Database unavailable"}},"operationId":"deleteAdminCustomersByIdNotesByNoteId","tags":["admin","customers","notes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"noteId","required":true}],"summary":"Delete a tenant support note","description":"Removes the row permanently — a hard DELETE, no soft-delete and no undo. The statement is scoped to both the note id and the tenant id, so a note of another tenant is never hit. A note id that matches nothing is not an error: the endpoint answers 200 either way."}},"/admin/customers/{id}/limits":{"get":{"responses":{"200":{"description":"Effective limits — `data` is absent when no database connection exists","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"apiRateLimitPerMinute":{"anyOf":[{"type":"number"},{"type":"string"}]},"storageCapGb":{"anyOf":[{"type":"number"},{"type":"string"}]},"aiTokensPerMonth":{"anyOf":[{"type":"number"},{"type":"string"}]},"maxUsers":{"anyOf":[{"type":"number"},{"type":"string"}]},"planName":{"type":["string","null"]}},"required":["id","apiRateLimitPerMinute","storageCapGb","aiTokensPerMonth","maxUsers","planName"]}}},"example":{"data":{"id":"string","apiRateLimitPerMinute":0,"storageCapGb":0,"aiTokensPerMonth":0,"maxUsers":0,"planName":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not found"},"500":{"description":"Limits could not be read"}},"operationId":"getAdminCustomersByIdLimits","tags":["admin","customers","limits"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Tenant limits/quotas (rate-limit, storage, AI tokens, users)","description":"Returns the EFFECTIVE limits, resolved per value in three steps: the admin override under `tenants.settings.limits`, then the column of the tenant's plan, then a hard-coded default (600 requests/minute, 10 GB, 1,000,000 AI tokens, 5 users). `planName` names the joined plan and is null when the tenant has none. Read-only. 404 when no tenant carries the given id; on a query error the raw database message stays in the log and the client only sees `limits_unavailable`."},"patch":{"responses":{"200":{"description":"Limits updated — `limits` is the merged state","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"limits":{"type":"object","properties":{"apiRateLimitPerMinute":{"type":"integer","exclusiveMinimum":0,"maximum":100000},"storageCapGb":{"type":"number","exclusiveMinimum":0,"maximum":100000},"aiTokensPerMonth":{"type":"integer","exclusiveMinimum":0,"maximum":100000000},"maxUsers":{"type":"integer","exclusiveMinimum":0,"maximum":100000}},"additionalProperties":true}},"required":["ok","limits"]},"example":{"ok":true,"limits":{"apiRateLimitPerMinute":1,"storageCapGb":1,"aiTokensPerMonth":1,"maxUsers":1}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Database unavailable"}},"operationId":"patchAdminCustomersByIdLimits","tags":["admin","customers","limits"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Override tenant limits/quotas (stored under tenants.settings.limits)","description":"Partial update: only the keys present in the body are written, the remaining stored overrides survive. The merged object replaces `tenants.settings.limits` and `tenants.updated_at` is bumped; the plan itself is not touched, so removing a key here is not possible via this endpoint. Writes a `limits.update` entry to public.admin_audit_log — a failed audit write is swallowed and does not abort the update. Answers 200 even when the id matches no tenant (the UPDATE then hits zero rows).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"apiRateLimitPerMinute":{"type":"integer","exclusiveMinimum":0,"maximum":100000},"storageCapGb":{"type":"number","exclusiveMinimum":0,"maximum":100000},"aiTokensPerMonth":{"type":"integer","exclusiveMinimum":0,"maximum":100000000},"maxUsers":{"type":"integer","exclusiveMinimum":0,"maximum":100000}}},"example":{"apiRateLimitPerMinute":1,"storageCapGb":1,"aiTokensPerMonth":1,"maxUsers":1}}}}}},"/admin/customers/{id}/stammdaten":{"patch":{"responses":{"200":{"description":"Updated — `stammdaten` is the merged state","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"stammdaten":{"type":"object","properties":{"legalName":{"type":"string","maxLength":255},"vatId":{"type":"string","maxLength":50},"legalForm":{"type":"string","maxLength":50},"industry":{"type":"string","maxLength":100},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"street":{"type":"string","maxLength":255},"zip":{"type":"string","maxLength":20},"city":{"type":"string","maxLength":100},"country":{"type":"string","minLength":2,"maxLength":2},"contactName":{"type":"string","maxLength":200},"contactEmail":{"type":"string","format":"email"},"contactPhone":{"type":"string","maxLength":50},"logoUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]}},"additionalProperties":true}},"required":["ok","stammdaten"]},"example":{"ok":true,"stammdaten":{"legalName":"string","vatId":"string","legalForm":"string","industry":"string","website":"https://example.com","street":"string","zip":"string","city":"string","country":"st","contactName":"string","contactEmail":"beispiel@example.com","contactPhone":"string","logoUrl":"https://example.com"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Update failed"},"503":{"description":"Database unavailable"}},"operationId":"patchAdminCustomersByIdStammdaten","tags":["admin","customers","stammdaten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update tenant master data (anschrift, USt-ID, contact person)","description":"Partial update: only the keys present in the body are written, everything already stored under `tenants.settings.stammdaten` survives — there is no way to clear a key here, and `tenants.name` itself is NOT changed by a `legalName` in the body. Bumps `tenants.updated_at` and writes a `stammdaten.update` entry to public.admin_audit_log; a failed audit write is swallowed. Answers 200 even when the id matches no tenant, because the UPDATE then simply hits zero rows.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"legalName":{"type":"string","maxLength":255},"vatId":{"type":"string","maxLength":50},"legalForm":{"type":"string","maxLength":50},"industry":{"type":"string","maxLength":100},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"street":{"type":"string","maxLength":255},"zip":{"type":"string","maxLength":20},"city":{"type":"string","maxLength":100},"country":{"type":"string","minLength":2,"maxLength":2},"contactName":{"type":"string","maxLength":200},"contactEmail":{"type":"string","format":"email"},"contactPhone":{"type":"string","maxLength":50},"logoUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]}}},"example":{"legalName":"string","vatId":"string","legalForm":"string","industry":"string","website":"https://example.com","street":"string","zip":"string","city":"string","country":"st","contactName":"string","contactEmail":"beispiel@example.com","contactPhone":"string","logoUrl":"https://example.com"}}}}},"get":{"responses":{"200":{"description":"Master data block — `data` is absent when no database connection exists","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"legalName":{"type":"string"},"vatId":{"type":"string","maxLength":50},"legalForm":{"type":"string","maxLength":50},"industry":{"type":"string","maxLength":100},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"street":{"type":"string","maxLength":255},"zip":{"type":"string","maxLength":20},"city":{"type":"string","maxLength":100},"country":{"type":"string","minLength":2,"maxLength":2},"contactName":{"type":"string","maxLength":200},"contactEmail":{"type":"string","format":"email"},"contactPhone":{"type":"string","maxLength":50},"logoUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]}},"required":["legalName"],"additionalProperties":true}}},"example":{"data":{"legalName":"string","vatId":"string","legalForm":"string","industry":"string","website":"https://example.com","street":"string","zip":"string","city":"string","country":"st","contactName":"string","contactEmail":"beispiel@example.com","contactPhone":"string","logoUrl":"https://example.com"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not found"},"500":{"description":"Read failed"}},"operationId":"getAdminCustomersByIdStammdaten","tags":["admin","customers","stammdaten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get tenant master data block","description":"Composes the block from two places: `legalName` always comes from the `tenants.name` column, all other fields from `tenants.settings.stammdaten`. A `legalName` stored in the settings therefore overrides the column value in this answer. Never fails on missing master data — an unset block yields just `legalName`. 404 when no tenant carries the given id."}},"/admin/customers/{tenantId}/impersonate":{"post":{"responses":{"200":{"description":"Impersonation token issued — returned as cookie, body carries the target and redirect data","content":{"application/json":{"schema":{"type":"object","properties":{"expiresInSec":{"type":"integer"},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"tenantName":{"type":"string"},"targetUserId":{"type":"string"},"targetEmail":{"type":"string"},"redirectUrl":{"type":"string"}},"required":["expiresInSec","tenantId","tenantSlug","tenantName","targetUserId","targetEmail","redirectUrl"]},"example":{"expiresInSec":0,"tenantId":"string","tenantSlug":"string","tenantName":"string","targetUserId":"string","targetEmail":"string","redirectUrl":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden — not a hersteller-admin"},"404":{"description":"Tenant or admin user not found"},"503":{"description":"Database unavailable"}},"operationId":"postAdminCustomersByTenantIdImpersonate","tags":["admin","customers","impersonate"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"summary":"Hersteller-Admin: issue short-lived impersonation JWT for tenant primary admin","description":"Resolves the tenant and its primary admin user (users with role \"admin\" first, then oldest by created_at) and signs an HS256 JWT that is valid for 15 minutes. The token is NOT part of the response body: it is delivered only as the HttpOnly/Secure/SameSite=Lax cookie `__Secure-nemix_impersonation`, scoped to the shared parent domain so it travels to the tenant sub-domain named in `redirectUrl`. Writes an `impersonate.start` row into public.admin_audit_log (table created on demand); a failed audit write is logged but does not abort the request. Callers must be role \"system\"/\"admin\" or layerType \"hersteller\", otherwise 403. 404 when the tenant does not exist or has no non-deleted user."}},"/admin/customers/{tenantId}/impersonate/end":{"post":{"responses":{"200":{"description":"Impersonation ended — cookie cleared","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postAdminCustomersByTenantIdImpersonateEnd","tags":["admin","customers","impersonate"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"summary":"End an active impersonation session","description":"Deletes the `__Secure-nemix_impersonation` cookie (same path/domain/flags as on set — otherwise the browser keeps the cross-subdomain cookie alive) and appends an `impersonate.end` row to public.admin_audit_log. The stored JWT itself is not revoked; it simply expires after its 15-minute lifetime. Always answers 200, also when no database or no caller identity is available — then only the cookie is cleared."}},"/admin/customers/{id}/history":{"get":{"responses":{"200":{"description":"Seite der Aenderungshistorie, neueste zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"source":{"type":"string","enum":["ai_build_docs","ai_build_versions","tenant_activity_log","undo_log","gobd_chain","tenant_audit_log","ai_audit_log","invoice_versions","document_versions","customization_manifest"]},"refId":{"type":"string"},"occurredAt":{"type":"string"},"category":{"type":"string","enum":["ki_bau","regel","anpassung","daten_edit","loeschung","beleg_version","dokument_version","compliance","undo"]},"entity":{"type":["string","null"]},"targetLabel":{"type":"string"},"summary":{"type":"string"},"actorLabel":{"type":"string"},"actorType":{"type":"string","enum":["mensch","ki","system"]},"reason":{"type":["string","null"]},"details":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"value":{"type":"string"}},"required":["label","value"]}},"diff":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"label":{"type":"string"},"before":{},"after":{}},"required":["field","label"]}},"revertable":{"type":"boolean"},"revertRef":{"type":["object","null"],"properties":{"kind":{"type":"string","enum":["build_version","ai_undo"]},"id":{"type":"string"}},"required":["kind","id"]},"integrityVerified":{"type":"boolean"}},"required":["id","source","refId","occurredAt","category","entity","targetLabel","summary","actorLabel","actorType","reason","details","diff","revertable","revertRef"]}},"nextCursor":{"type":["string","null"]}},"required":["events","nextCursor"]},"example":{"events":[{"id":"string","source":"ai_build_docs","refId":"string","occurredAt":"string","category":"ki_bau","entity":"string","targetLabel":"string","summary":"string","actorLabel":"string","actorType":"mensch","reason":"string","details":[{"label":"string","value":"string"}],"diff":[{"field":"string","label":"string"}],"revertable":true,"revertRef":{"kind":"build_version","id":"string"},"integrityVerified":true}],"nextCursor":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminCustomersByIdHistory","tags":["admin","customers","history"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Konsolidierte Aenderungshistorie eines Mandanten","description":"Mischt die getrennten Historien-Quellen des Ziel-Mandanten (KI-Bau-Log, Feld-Versionen, activity_log, GoBD-Audit, Rechnungs- und Dokument-Versionen, Anpassungs-Manifest) zu einer nach `occurredAt` absteigend sortierten Liste. Blaettert per Keyset: `before` uebernimmt den `nextCursor` der Vorseite, `pageSize` steuert die Seitengroesse. Rein lesend — im fremden Mandanten-Schema wird keine Tabelle angelegt, fehlende Quellen werden uebersprungen. Laesst sich der Mandant nicht aufloesen oder faellt die Datenbank aus, antwortet der Endpunkt mit einer leeren Seite statt mit einem Fehler."}},"/admin/users":{"get":{"responses":{"200":{"description":"Nutzer der Seite plus Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Nutzers"},"email":{"type":"string","description":"E-Mail-Adresse"},"name":{"type":"string","description":"Anzeigename; leerer String wenn keiner erfasst ist"},"role":{"type":"string","description":"Rolle; `user` wenn in der Datenbank keine steht"},"tenantId":{"type":"string","description":"Mandant des Nutzers; leerer String wenn keiner zugeordnet ist"},"tenantName":{"type":"string","description":"Name des Mandanten; FEHLT, wenn keiner ermittelbar war"},"emailVerified":{"type":"boolean","description":"true nur bei ausdruecklich bestaetigter Adresse; NULL zaehlt als false"},"lastLoginAt":{"type":["string","null"],"description":"Letzte Anmeldung; null wenn nie angemeldet"},"createdAt":{"type":["string","null"],"description":"Anlagezeitpunkt"}},"required":["id","email","name","role","tenantId","emailVerified","lastLoginAt","createdAt"]},"description":"Die Nutzer der Seite, neueste zuerst"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Treffer der Filter, unabhaengig von limit/offset"},"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"}},"required":["data","total","limit","offset"]},"example":{"data":[{"id":"string","email":"string","name":"string","role":"string","tenantId":"string","tenantName":"string","emailVerified":true,"lastLoginAt":"string","createdAt":"string"}],"total":0,"limit":1,"offset":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht verfuegbar oder Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getAdminUsers","tags":["admin","users"],"parameters":[{"in":"query","name":"tenantId","schema":{"type":"string","minLength":1}},{"in":"query","name":"q","schema":{"type":"string","minLength":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"summary":"Nutzer aller Mandanten auflisten und filtern","description":"Liest `public.users` ueber ALLE Mandanten hinweg, verbunden mit `public.tenants` fuer den Mandantennamen, neueste zuerst. Geloeschte Nutzer (`deleted_at`) bleiben aussen vor. `tenantId` und `role` filtern exakt, `q` sucht als Teiltext in E-Mail ODER Name; `limit` (1-200, Vorgabe 50) und `offset` blaettern, `total` zaehlt alle Treffer der Filter. Scheitert die Abfrage, kommt 503 und KEINE leere Liste — sonst waere „kein Nutzer\" von „Abfrage kaputt\" nicht zu unterscheiden."}},"/admin/users/{id}":{"delete":{"responses":{"200":{"description":"Removed. The account can no longer sign in.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"},"email":{"type":"string"}},"required":["ok","id","email"]},"example":{"ok":true,"id":"string","email":"string"}}}},"400":{"description":"Confirmation missing or not matching (`confirmation_mismatch`)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Not a super_admin caller"},"404":{"description":"Unknown or already removed (`not_found`)"},"409":{"description":"Own account (`self`), not a super admin (`not_a_super_admin`), or the last active super admin (`last_super_admin`)"},"503":{"description":"Database unavailable"}},"operationId":"deleteAdminUsersById","tags":["admin","users"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Remove a super admin (soft delete)","description":"Sets `deleted_at` on a user whose role is `super_admin`. Requires `?bestaetigung=<email>` matching the target exactly; without it nothing happens. Refuses the caller's own account and refuses anyone who is not a super admin — this area manages super admins only. THE LAST ACTIVE SUPER ADMIN CANNOT BE REMOVED: a trigger on public.users refuses it and this route reports that as 409 `last_super_admin`. The trigger also covers the nine other paths that do not run through here."}},"/admin/layer-sync/export":{"post":{"responses":{"200":{"description":"Das signierte Buendel unter `bundle`. Seine innere Form bestimmt `@nemix/layer-engine`, nicht diese Route — deshalb hier keine Feldliste.","content":{"application/json":{"schema":{"type":"object","properties":{"bundle":{}}}}}},"400":{"description":"`layer` fehlt oder `env` ist kein gueltiger Umgebungsname."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin` (zusaetzlich zum Tor der Admin-App)."},"404":{"description":"Diese Ebene gibt es in der Quellumgebung nicht."},"500":{"description":"`LAYER_SYNC_SECRET` ist nicht gesetzt."},"503":{"description":"Adapter nicht verdrahtet — heute der Regelfall, siehe Beschreibung."}},"operationId":"postAdminLayer-syncExport","tags":["admin"],"parameters":[],"summary":"Eine Ebene als signiertes Buendel ausgeben","description":"Packt eine Ebene samt ihrer Beitraege in ein JSON-Buendel und signiert es mit dem Geheimnis aus `LAYER_SYNC_SECRET` (ersatzweise `LAYER_PROMOTE_SECRET`). Das Buendel ist die Transporteinheit fuer `/import` in einer anderen Umgebung.\n\nBeide Angaben stehen in der ABFRAGE, nicht im Rumpf: `layer` (Kennung) und `env` (Quellumgebung). Gueltige Umgebungsnamen sind `dev`, `sandkasten` und `produktiv` — alles andere ergibt 400.\n\nTrotz `POST` wird KEIN Rumpf gelesen — die Methode ist gewaehlt, weil signiert und protokolliert wird, nicht weil etwas mitgeschickt wird.\n\nHEUTE NICHT BENUTZBAR: der Adapter, ueber den diese Route ihre Daten holt, wird in der laufenden Anwendung nirgends gesetzt (`setLayerSyncAdapter` steht nur im Test). Die Antwort ist deshalb ein 503 — kein voruebergehender Fehler, sondern ein fehlendes Bauteil.\n\nDie `/api/admin`-App ist als Ganzes `requireSuperAdmin` (`index.ts:2615`); der zusaetzliche `admin`-Test im Handler ist ein zweiter Riegel, kein eigenes Tor."}},"/admin/layer-sync/import":{"post":{"responses":{"200":{"description":"Das Ergebnis unter `result`. `applied` sagt, ob wirklich geschrieben wurde — bei `dryRun` niemals.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{}}}}}},"400":{"description":"Umgebungsname ungueltig, `bundle` fehlt, Signatur oder Aufbau falsch, oder die Quellumgebung des Buendels passt nicht zu `from`."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin` — ODER `POLICY_DENIED`/`APPROVAL_REQUIRED` aus der Freigabepruefung. Zwei sehr verschiedene Faelle unter einem Code; die Meldung unterscheidet sie."},"409":{"description":"`CONFLICT` — der Bestand widerspricht, `conflictMode: \"fail\"`."},"500":{"description":"`LAYER_SYNC_SECRET` ist nicht gesetzt."},"503":{"description":"Adapter nicht verdrahtet."}},"operationId":"postAdminLayer-syncImport","tags":["admin"],"parameters":[],"summary":"Ein signiertes Buendel in eine Umgebung einspielen","description":"Prueft die Signatur des Buendels, vergleicht es mit dem Bestand der Zielumgebung und spielt es ein.\n\nDie Umgebungen stehen in der ABFRAGE (`from`, `to`), das Buendel im RUMPF unter `bundle`. Gueltige Umgebungsnamen sind `dev`, `sandkasten` und `produktiv` — alles andere ergibt 400. Stimmt die im Buendel vermerkte Quellumgebung nicht mit `from` ueberein, gibt es 400 — das verhindert, dass ein Buendel aus der falschen Richtung eingespielt wird.\n\nDrei weitere Felder im Rumpf, alle freiwillig:\n· `dryRun` — rechnet durch, SCHREIBT ABER NICHT. Die Antwort sieht aus wie im Ernstfall; nur `applied` verraet den Unterschied.\n· `conflictMode` — `fail` (Standard), `overwrite` oder `skip`. Der Standard ist der vorsichtige: bei einem Widerspruch bricht es mit 409 ab, statt zu ueberschreiben.\n· `approvalsGranted` — setzt eine verlangte Freigabe als erteilt. Wer das mitschickt, umgeht die Freigabe; die Route prueft NICHT, ob sie wirklich erteilt wurde.\n\nDIE FEHLERCODES TRAGEN BEDEUTUNG: 403 heisst `POLICY_DENIED` (dieser Weg zwischen den Umgebungen ist nicht erlaubt) ODER `APPROVAL_REQUIRED` (er waere erlaubt, aber jemand muss zustimmen) — die Meldung nennt den Code. 409 heisst `CONFLICT`. Alles andere aus der Pruefung ergibt 400.\n\nHEUTE NICHT BENUTZBAR: der Adapter, ueber den diese Route ihre Daten holt, wird in der laufenden Anwendung nirgends gesetzt (`setLayerSyncAdapter` steht nur im Test). Die Antwort ist deshalb ein 503 — kein voruebergehender Fehler, sondern ein fehlendes Bauteil. Bei DIESER Route schlaegt allerdings meist schon das fehlende Geheimnis vorher zu (500).\n\nDie `/api/admin`-App ist als Ganzes `requireSuperAdmin` (`index.ts:2615`); der zusaetzliche `admin`-Test im Handler ist ein zweiter Riegel, kein eigenes Tor."}},"/admin/layer-sync/diff":{"get":{"responses":{"200":{"description":"Der Vergleich: `summary` (Zaehlwerk), `entries` (die einzelnen Unterschiede) und `policy` (ob der Weg erlaubt waere). Die innere Form bestimmt `@nemix/layer-engine`.","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{},"entries":{"type":"array","items":{}},"policy":{}},"required":["entries"]},"example":{"entries":[]}}}},"400":{"description":"`layer` fehlt oder ein Umgebungsname ist ungueltig."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin`."},"503":{"description":"Adapter nicht verdrahtet — heute der Regelfall, siehe Beschreibung."}},"operationId":"getAdminLayer-syncDiff","tags":["admin"],"parameters":[],"summary":"Eine Ebene zwischen zwei Umgebungen vergleichen","description":"Zeigt, was sich zwischen zwei Umgebungen an einer Ebene unterscheidet, und ob dieser Weg ueberhaupt erlaubt waere.\n\nAlle drei Angaben stehen in der ABFRAGE: `layer`, `from`, `to`. Gueltige Umgebungsnamen sind `dev`, `sandkasten` und `produktiv` — alles andere ergibt 400.\n\nFEHLT DIE EBENE IN EINER DER BEIDEN UMGEBUNGEN, ist das KEIN Fehler: die fehlende Seite gilt als leer, und der Vergleich zeigt sie entsprechend als vollstaendigen Zugang oder Wegfall. Fehlt sie in BEIDEN, kommt ein leerer Vergleich mit 200 — nicht 404.\n\n`policy` beantwortet die zweite Frage: ob eine Uebernahme von `from` nach `to` fuer diese Ebenenart zulaessig ist. Sie schaut NICHT auf die Unterschiede, sondern nur auf Art und Richtung — eine Wegauskunft, kein Urteil ueber den Inhalt. Ist die Ebene in keiner der beiden Umgebungen vorhanden, wird ersatzweise mit der Art `hersteller` gerechnet.\n\nDiese Route SCHREIBT NICHTS und braucht kein Geheimnis — sie faellt deshalb nicht in den 500, sondern direkt in den 503.\n\nHEUTE NICHT BENUTZBAR: der Adapter, ueber den diese Route ihre Daten holt, wird in der laufenden Anwendung nirgends gesetzt (`setLayerSyncAdapter` steht nur im Test). Die Antwort ist deshalb ein 503 — kein voruebergehender Fehler, sondern ein fehlendes Bauteil.\n\nDie `/api/admin`-App ist als Ganzes `requireSuperAdmin` (`index.ts:2615`); der zusaetzliche `admin`-Test im Handler ist ein zweiter Riegel, kein eigenes Tor."}},"/admin/ai-usage":{"get":{"responses":{"200":{"description":"Wochenverlauf und Mandanten-Aufstellung. Alle Betraege in EUR.","content":{"application/json":{"schema":{"type":"object","properties":{"weekly":{"type":"object","properties":{"totalInputTokens":{"type":"number"},"totalOutputTokens":{"type":"number"},"totalTokens":{"type":"number"},"totalCostEur":{"type":"number"},"avgCostPerDayEur":{"type":"number"},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"costEur":{"type":"number"}},"required":["date","inputTokens","outputTokens","costEur"],"additionalProperties":false}}},"required":["totalInputTokens","totalOutputTokens","totalTokens","totalCostEur","avgCostPerDayEur","days"],"additionalProperties":false},"byTenant":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"tenantName":{"type":"string"},"tenantSlug":{"type":"string"},"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"totalTokens":{"type":"number"},"costEur":{"type":"number"},"callCount":{"type":"number"}},"required":["tenantId","tenantName","tenantSlug","inputTokens","outputTokens","totalTokens","costEur","callCount"],"additionalProperties":false}}},"required":["weekly","byTenant"],"additionalProperties":false},"example":{"weekly":{"totalInputTokens":0,"totalOutputTokens":0,"totalTokens":0,"totalCostEur":0,"avgCostPerDayEur":0,"days":[{"date":"string","inputTokens":0,"outputTokens":0,"costEur":0}]},"byTenant":[{"tenantId":"string","tenantName":"string","tenantSlug":"string","inputTokens":0,"outputTokens":0,"totalTokens":0,"costEur":0,"callCount":0}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht verfuegbar. Bewusst ein Fehler statt einer Woche voller Nullen — die waere von „nichts verbraucht\" nicht zu unterscheiden."}},"operationId":"getAdminAi-usage","tags":["admin","ai"],"parameters":[],"description":"KI-Kosten der letzten 7 Tage und Verteilung auf die Mandanten (Super-Admin). Gelesen wird `public.ai_cost_events` ueber ALLE Mandanten hinweg — der Endpunkt kennt keine Parameter, das Fenster ist fest auf sieben Tage gesetzt. Der Wochenverlauf ist lueckenlos: Tage ohne Ereignisse kommen mit Nullen mit, und die Tagesgrenzen zieht die Datenbank, nicht der API-Container. Die Mandanten-Aufstellung ist nach Kosten absteigend sortiert und auf 100 Zeilen gekappt. Gespeichert wird in USD, ausgewiesen in EUR — umgerechnet mit dem Spot-Kurs OHNE Sicherheitsaufschlag, dieser Wert ist also keine Abrechnungsgrundlage.","summary":"KI-Kosten der letzten 7 Tage und Verteilung auf die Mandanten (Super-Admin)","x-nemix-summary-source":"description:first-sentence"}},"/admin/cost-summary":{"get":{"responses":{"200":{"description":"Wochenuebersicht","content":{"application/json":{"schema":{"type":"object","properties":{"windowStart":{"type":"string","description":"Beginn des Auswertungsfensters als ISO-8601-Zeitstempel"},"windowEnd":{"type":"string","description":"Ende des Auswertungsfensters als ISO-8601-Zeitstempel"},"perTenant":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, dem die Kosten zugeordnet sind"},"aiEurCents":{"type":"number","description":"KI-Kosten in Euro-Cent — Ereignisse mit Praefix `anthropic.`"},"infraEurCents":{"type":"number","description":"Infrastrukturkosten in Euro-Cent — Ereignisse mit Praefix `infra.`"},"totalEurCents":{"type":"number","description":"Summe ALLER Ereignisse des Mandanten, auch solcher ohne bekanntes Praefix"},"weeklyLimitEurCents":{"type":["number","null"],"description":"Monatslimit auf 7 Tage umgerechnet; null heisst unbegrenzt — heute immer null"},"utilization":{"type":["number","null"],"description":"Verbrauch geteilt durch Wochenlimit; null wenn kein Limit hinterlegt ist"},"flagged":{"type":"boolean","description":"true ab 80 % des Wochenlimits — ohne Limit nie"}},"required":["tenantId","aiEurCents","infraEurCents","totalEurCents","weeklyLimitEurCents","utilization","flagged"]},"description":"Je Mandant eine Zeile, nach Gesamtkosten absteigend"},"totalAiEurCents":{"type":"number","description":"KI-Kosten aller Mandanten in Euro-Cent"},"totalInfraEurCents":{"type":"number","description":"Infrastrukturkosten aller Mandanten in Euro-Cent"},"totalEurCents":{"type":"number","description":"Summe aus KI und Infrastruktur in Euro-Cent"},"flaggedCount":{"type":"integer","description":"Anzahl der Mandanten ueber der 80-Prozent-Schwelle"},"infraBreakdown":{"type":"object","properties":{"s3EurCents":{"type":"number","description":"Ereignisse vom Typ `infra.s3_gb`"},"dockerEurCents":{"type":"number","description":"Ereignisse vom Typ `infra.docker_mem_mb`"},"dbEurCents":{"type":"number","description":"Ereignisse vom Typ `infra.db_conn`"}},"required":["s3EurCents","dockerEurCents","dbEurCents"],"description":"Infrastruktur nach Quelle, ueber alle Mandanten. 0 heisst: nichts gesammelt"}},"required":["windowStart","windowEnd","perTenant","totalAiEurCents","totalInfraEurCents","totalEurCents","flaggedCount","infraBreakdown"]},"example":{"windowStart":"string","windowEnd":"string","perTenant":[{"tenantId":"string","aiEurCents":0,"infraEurCents":0,"totalEurCents":0,"weeklyLimitEurCents":0,"utilization":0,"flagged":true}],"totalAiEurCents":0,"totalInfraEurCents":0,"totalEurCents":0,"flaggedCount":0,"infraBreakdown":{"s3EurCents":0,"dockerEurCents":0,"dbEurCents":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getAdminCost-summary","tags":["admin","costs"],"parameters":[],"summary":"Kosten der letzten 7 Tage je Mandant, getrennt nach KI und Infrastruktur.","description":"Aggregiert `public.cost_events` ueber die letzten sieben Tage und reicht die Zeilen an `aggregateWeekly` weiter — dieselbe reine Funktion, die auch den woechentlichen Kostenbericht per Mail rechnet. Zugeordnet wird nach Praefix: `anthropic.` zaehlt als KI, `infra.` als Infrastruktur, und `infraBreakdown` schluesselt Letztere nach S3, Docker-Speicher und Datenbankverbindungen auf. Ein monatliches Kostenlimit je Mandant fuehrt heute keine Tabelle; deshalb bleiben `weeklyLimitEurCents` und `utilization` null und `flagged` false, statt eine 0 zu behaupten. Alle Betraege in Euro-Cent — die Gesamtsumme zaehlt dabei nur KI und Infrastruktur, die Mandantenzeile dagegen jedes Ereignis. Die Rollenpruefung liegt in der Admin-Subapp, nicht in diesem Handler."}},"/admin/ai-cost-summary":{"get":{"responses":{"200":{"description":"Monatsuebersicht","content":{"application/json":{"schema":{"type":"object","properties":{"month":{"type":"string","description":"Der ausgewertete Monat als YYYY-MM (UTC)"},"perTenant":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, dem die Aufrufe zugeordnet sind"},"tenantName":{"type":"string","description":"Anzeigename aus public.tenants; fehlt, wenn zum Mandanten kein Eintrag existiert"},"totalEur":{"type":"number","description":"Kosten des laufenden Monats in Euro, aus USD zum festen Kurs umgerechnet"},"callCount":{"type":"number","description":"Anzahl der KI-Aufrufe im laufenden Monat"},"inputTokens":{"type":"number","description":"Summe der Eingabe-Token"},"outputTokens":{"type":"number","description":"Summe der Ausgabe-Token"},"cachedTokens":{"type":"number","description":"Immer 0 — ai_cost_events fuehrt keine Cache-Spalte"}},"required":["tenantId","totalEur","callCount","inputTokens","outputTokens","cachedTokens"]},"description":"Bis zu 100 Mandanten, nach Kosten absteigend"},"topTools":{"type":"array","items":{"type":"object","properties":{"toolId":{"type":"string","description":"Der `task_type` des Aufrufs; Zeilen ohne Zuordnung erscheinen als \"ohne Zuordnung\""},"totalEur":{"type":"number","description":"Kosten dieses Werkzeugs im laufenden Monat in Euro"},"callCount":{"type":"number","description":"Anzahl der Aufrufe dieses Werkzeugs"},"avgEur":{"type":"number","description":"Kosten je Aufruf; 0 wenn es keinen Aufruf gab"}},"required":["toolId","totalEur","callCount","avgEur"]},"description":"Bis zu 20 Werkzeuge, nach Kosten absteigend"},"grandTotalEur":{"type":"number","description":"Summe der Mandantenkosten in Euro"},"grandTotalCalls":{"type":"number","description":"Summe der Aufrufe ueber alle Mandanten"}},"required":["month","perTenant","topTools","grandTotalEur","grandTotalCalls"]},"example":{"month":"string","perTenant":[{"tenantId":"string","tenantName":"string","totalEur":0,"callCount":0,"inputTokens":0,"outputTokens":0,"cachedTokens":0}],"topTools":[{"toolId":"string","totalEur":0,"callCount":0,"avgEur":0}],"grandTotalEur":0,"grandTotalCalls":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getAdminAi-cost-summary","tags":["admin","ai","costs"],"parameters":[],"summary":"KI-Kosten des laufenden Monats je Mandant und je Werkzeug.","description":"Liest `public.ai_cost_events` ab Monatsanfang (UTC) und gruppiert zweimal: einmal je Mandant — verbunden mit `public.tenants` fuer den Anzeigenamen, hoechstens 100 Zeilen — und einmal je `task_type` fuer die 20 teuersten Werkzeuge. Beide Listen sind nach Kosten absteigend sortiert. Die Tabelle fuehrt Kosten in US-Dollar; die Antwort rechnet sie zu einem festen Kurs in Euro um. Zeilen ohne `task_type` erscheinen als \"ohne Zuordnung\", damit die Summe der Werkzeuge zur Gesamtsumme passt, und `cachedTokens` ist immer 0, weil die Tabelle keine Cache-Spalte hat. Die Rollenpruefung liegt in der Admin-Subapp, nicht in diesem Handler."}},"/admin/usage/overview":{"get":{"responses":{"200":{"description":"Tenant usage overview — one row per active or trial tenant.","content":{"application/json":{"schema":{"type":"object","properties":{"monthBucket":{"type":"string"},"tenantCount":{"type":"number"},"data":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"tenantId":{"type":"string"},"name":{"type":"string"},"plan":{"type":"string"},"monthBucket":{"type":"string"},"apiCalls":{"type":"object","properties":{"month":{"type":"number"},"today":{"type":"number"},"rpm_current":{"type":"number"},"month_limit":{"type":"number"},"rpm_limit":{"type":"number"},"month_pct":{"type":["number","null"]}},"required":["month","today","rpm_current","month_limit","rpm_limit","month_pct"],"additionalProperties":false},"degraded":{"type":"boolean"}},"required":["tenantId","name","plan","monthBucket","apiCalls","degraded"],"additionalProperties":false},{"type":"object","properties":{"tenantId":{"type":"string"},"name":{"type":"string"},"plan":{"type":["string","null"]},"monthBucket":{"type":"string"},"apiCalls":{"type":"null"},"degraded":{"type":"boolean","const":true},"error":{"type":"string"}},"required":["tenantId","name","plan","monthBucket","apiCalls","degraded","error"],"additionalProperties":false}]}}},"required":["monthBucket","tenantCount","data"],"additionalProperties":false},"example":{"monthBucket":"string","tenantCount":0,"data":[{"tenantId":"string","name":"string","plan":"string","monthBucket":"string","apiCalls":{"month":0,"today":0,"rpm_current":0,"month_limit":0,"rpm_limit":0,"month_pct":0},"degraded":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"DB unavailable"}},"operationId":"getAdminUsageOverview","tags":["admin","usage"],"parameters":[],"description":"All-tenant API usage overview for the current month with plan quotas. Tenants come from Postgres (status `active` or `trial` only, newest first); the call counters come from Redis and are read for every tenant in parallel. A tenant whose counter read fails still appears in the list — with `apiCalls: null`, `degraded: true` and an `error` string, so one broken row never hides the rest. `degraded` also goes true when Redis is simply unavailable, in which case zeros mean „not measured\", not „no traffic\". No paging and no filters.","summary":"All-tenant API usage overview for the current month with plan quotas","x-nemix-summary-source":"description:first-sentence"}},"/admin/usage/users":{"get":{"responses":{"200":{"description":"User list — nine fields per user, plus the paging numbers.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{},"role":{},"tenantId":{},"tenantName":{},"emailVerified":{},"lastLoginAt":{},"createdAt":{}},"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"}},"required":["data","total","page","limit"],"additionalProperties":false},"example":{"data":[{}],"total":0,"page":0,"limit":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"DB unavailable"}},"operationId":"getAdminUsageUsers","tags":["admin","usage"],"parameters":[],"description":"System-wide user list across all tenants (paged, searchable). Reads `public.users` without the soft-deleted rows, newest first, and joins the tenant name. `page` starts at 1, `limit` is clamped to 1..200 (default 50); `search` matches email OR name as a substring, `role` matches exactly. `total` is counted with the same conditions as the page. Columns are listed one by one and mapped again afterwards, so `password_hash` and `consents` cannot reach the browser. Despite the path, this endpoint reports no usage counters at all.","summary":"System-wide user list across all tenants (paged, searchable)","x-nemix-summary-source":"description:first-sentence"}},"/admin/usage/{tenantId}":{"get":{"responses":{"200":{"description":"Tenant usage detail — counters, limits and plan quotas side by side.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"plan":{"type":["string","null"]},"monthBucket":{"type":"string"},"apiCalls":{"type":"object","properties":{"month":{"type":"number"},"today":{"type":"number"},"rpm_current":{"type":"number"},"month_limit":{"type":"number"},"rpm_limit":{"type":"number"},"month_pct":{"type":["number","null"]}},"required":["month","today","rpm_current","month_limit","rpm_limit","month_pct"],"additionalProperties":false},"planQuotas":{"type":"object","properties":{"api_rpm":{"type":"number"},"ai_actions_per_month":{"type":["number","null"]},"storage_gb":{"type":"number"},"max_users":{"type":["number","null"]}},"required":["api_rpm","ai_actions_per_month","storage_gb","max_users"],"additionalProperties":false},"degraded":{"type":"boolean"}},"required":["tenantId","plan","monthBucket","apiCalls","planQuotas","degraded"],"additionalProperties":false},"example":{"tenantId":"string","plan":"string","monthBucket":"string","apiCalls":{"month":0,"today":0,"rpm_current":0,"month_limit":0,"rpm_limit":0,"month_pct":0},"planQuotas":{"api_rpm":0,"ai_actions_per_month":0,"storage_gb":0,"max_users":0},"degraded":true}}}},"400":{"description":"tenantId required"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminUsageByTenantId","tags":["admin","usage"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"description":"Detailed API usage and plan quotas for a single tenant. Counters (minute, day, month) come from Redis, the plan from Postgres; if the plan lookup fails the call falls back to the starter limits instead of erroring. An UNKNOWN tenant id is not rejected — it answers 200 with `plan: null` and zeros, so a response here is no proof the tenant exists. `degraded: true` means the counters could not be read: the zeros then mean „not measured\", not „no traffic\". Beyond the API counters this also returns the plan quotas for AI actions, storage and seats, where `null` means unmetered.","summary":"Detailed API usage and plan quotas for a single tenant","x-nemix-summary-source":"description:first-sentence"}},"/admin/usage/report-stripe":{"post":{"responses":{"200":{"description":"Reported. `singleTenant` says which of the two shapes you get: `result` for one tenant, `summary` for the bulk run.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"singleTenant":{"type":"boolean","const":true},"result":{"type":"object","properties":{"tenantId":{"type":"string"},"monthBucket":{"type":"string"},"quantity":{"type":"number"},"ok":{"type":"boolean"},"error":{"type":"string"},"degraded":{"type":"boolean"}},"required":["tenantId","monthBucket","quantity","ok","degraded"],"additionalProperties":false}},"required":["singleTenant","result"],"additionalProperties":false},{"type":"object","properties":{"singleTenant":{"type":"boolean","const":false},"summary":{"type":"object","properties":{"reported":{"type":"number"},"skipped":{"type":"number"},"failed":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"monthBucket":{"type":"string"},"quantity":{"type":"number"},"ok":{"type":"boolean"},"error":{"type":"string"},"degraded":{"type":"boolean"}},"required":["tenantId","monthBucket","quantity","ok","degraded"],"additionalProperties":false}}},"required":["reported","skipped","failed","results"],"additionalProperties":false}},"required":["singleTenant","summary"],"additionalProperties":false}]},"example":{"singleTenant":true,"result":{"tenantId":"string","monthBucket":"string","quantity":0,"ok":true,"error":"string","degraded":true}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Reporting failure — same body as the 200 case, with `ok: false` respectively `reported: 0`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"singleTenant":{"type":"boolean","const":true},"result":{"type":"object","properties":{"tenantId":{"type":"string"},"monthBucket":{"type":"string"},"quantity":{"type":"number"},"ok":{"type":"boolean"},"error":{"type":"string"},"degraded":{"type":"boolean"}},"required":["tenantId","monthBucket","quantity","ok","degraded"],"additionalProperties":false}},"required":["singleTenant","result"],"additionalProperties":false},{"type":"object","properties":{"singleTenant":{"type":"boolean","const":false},"summary":{"type":"object","properties":{"reported":{"type":"number"},"skipped":{"type":"number"},"failed":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"monthBucket":{"type":"string"},"quantity":{"type":"number"},"ok":{"type":"boolean"},"error":{"type":"string"},"degraded":{"type":"boolean"}},"required":["tenantId","monthBucket","quantity","ok","degraded"],"additionalProperties":false}}},"required":["reported","skipped","failed","results"],"additionalProperties":false}},"required":["singleTenant","summary"],"additionalProperties":false}]}}}}},"operationId":"postAdminUsageReport-stripe","tags":["admin","usage"],"parameters":[],"description":"Manually trigger Stripe usage reporting for a single tenant or all tenants. A body with `tenantId` reports just that tenant and answers 500 when the report failed; without it ALL tenants are reported and the call only answers 500 when nothing at all got through (`failed > 0` AND `reported === 0`). A partly failed bulk run therefore answers 200 — the truth is in `summary.failed`. This writes to Stripe: successful reports are billable and cannot be taken back from here.","summary":"Manually trigger Stripe usage reporting for a single tenant or all tenants","x-nemix-summary-source":"description:first-sentence"}},"/admin/tenant-modules/{tenantId}":{"get":{"responses":{"200":{"description":"Modul-Liste samt aufgelöstem Tarif des Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"plan":{"type":"string","enum":["free","starter","professional","enterprise"]},"modules":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["voice","admin","customer_portal","immo"]},"label":{"type":"string"},"minPlan":{"type":["string","null"],"enum":["free","starter","professional","enterprise",null]},"superAdminOnly":{"type":"boolean"},"planAllowed":{"type":"boolean"},"defaultEnabled":{"type":"boolean"},"override":{"type":["boolean","null"]},"effective":{"type":"boolean"}},"required":["key","label","minPlan","superAdminOnly","planAllowed","defaultEnabled","override","effective"],"additionalProperties":false}}},"required":["tenantId","plan","modules"],"additionalProperties":false},"example":{"tenantId":"string","plan":"free","modules":[{"key":"voice","label":"string","minPlan":"free","superAdminOnly":true,"planAllowed":true,"defaultEnabled":true,"override":true,"effective":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminTenant-modulesByTenantId","tags":["admin","modules"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"description":"Gateable Module + effektiver Freischalt-Zustand für einen Mandanten. Aufgeführt sind nur die freischaltbaren Module aus der festen Registry — Kern-Module stehen dort nicht und sind immer aktiv. Je Modul kommen vier Angaben, die zusammen erklären, WARUM es an oder aus ist: `planAllowed` (lässt der Tarif es zu), `defaultEnabled` (Zustand ohne Override), `override` (ausdrückliche Setzung, `null` = keine) und `effective`. `effective` wird für einen NORMALEN Nutzer des Mandanten gerechnet — Module mit `superAdminOnly` stehen deshalb in aller Regel auf false. Ist der Mandant unbekannt oder die Datenbank nicht erreichbar, rechnet der Aufruf mit dem Tarif `free` statt zu scheitern. Nur Plattform-Superadmin.","summary":"Gateable Module + effektiver Freischalt-Zustand für einen Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/admin/tenant-modules/{tenantId}/{moduleKey}":{"put":{"responses":{"200":{"description":"gesetzt — der Aufruf spiegelt nur zurück, was gespeichert wurde. Er nennt NICHT den daraus folgenden effektiven Zustand; den liefert `GET /api/admin/tenant-modules/{tenantId}`.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"moduleKey":{"type":"string","enum":["voice","admin","customer_portal","immo"]},"enabled":{"type":"boolean"}},"required":["tenantId","moduleKey","enabled"],"additionalProperties":false},"example":{"tenantId":"string","moduleKey":"voice","enabled":true}}}},"400":{"description":"unbekanntes Modul"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putAdminTenant-modulesByTenantIdByModuleKey","tags":["admin","modules"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true},{"schema":{"type":"string"},"in":"path","name":"moduleKey","required":true}],"description":"Modul für einen Mandanten freischalten/sperren (Override). Geschrieben wird ein Eintrag in `public.tenant_modules`, der Tarif und Standardzustand übersteuert — der Tarif selbst bleibt unberührt. Ein Modul, das die Registry nicht kennt, ergibt 400 `unknown_module`; ob der Mandant existiert, prüft der Aufruf NICHT. Scheitert das Speichern, kommt 503 und es wurde nichts gesetzt. Der Override wirkt nicht gegen `superAdminOnly`: ein so markiertes Modul bleibt für normale Nutzer aus, auch mit `enabled: true`. Nur Plattform-Superadmin.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]},"example":{"enabled":true}}}},"summary":"Modul für einen Mandanten freischalten/sperren (Override)","x-nemix-summary-source":"description:first-sentence"}},"/admin/voice-numbers":{"get":{"responses":{"200":{"description":"Der gesamte Vorrat samt Zaehlern","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"e164":{"type":"string"},"tenant_id":{"type":["string","null"]},"label":{"type":["string","null"]},"active":{"type":"boolean"},"assigned_at":{"type":["string","null"]}},"required":["id","e164","tenant_id","label","active","assigned_at"],"additionalProperties":false}},"frei":{"type":"number"},"zugeteilt":{"type":"number"},"total":{"type":"number"}},"required":["data","frei","zugeteilt","total"],"additionalProperties":false},"example":{"data":[{"id":"string","e164":"string","tenant_id":"string","label":"string","active":true,"assigned_at":"string"}],"frei":0,"zugeteilt":0,"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"}},"operationId":"getAdminVoice-numbers","tags":["admin","voice"],"parameters":[],"summary":"Nummern-Vorrat: frei und zugeteilt","description":"Liest alle Rufnummern des Vorrats aus `public.voice_phone_numbers`, die den Provider `ainemix` tragen — freie zuerst, danach die zugeteilten. `frei`, `zugeteilt` und `total` sind aus genau dieser Liste gezaehlt und nicht getrennt abgefragt. Nummern anderer Anbieter bleiben aussen vor. Ist die Datenbank nicht erreichbar, kommt eine LEERE Liste mit 200 statt eines Fehlers — leer heisst hier also nicht zwingend, dass kein Vorrat da ist."},"post":{"responses":{"201":{"description":"Aufgenommen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"e164":{"type":"string"}},"required":["ok","e164"],"additionalProperties":false},"example":{"ok":true,"e164":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"},"422":{"description":"Ungültig — `error` nennt den Grund"}},"operationId":"postAdminVoice-numbers","tags":["admin","voice"],"parameters":[],"summary":"Nimmt eine bei AWS bestellte Rufnummer in den Vorrat auf","description":"Traegt eine bereits beschaffte Rufnummer in den Vorrat ein — noch ohne Mandant (201). Die Nummer muss E.164 sein (`+49…`). Hier wird NICHTS bei AWS bestellt: der Aufruf bildet nur ab, was dort schon existiert. Schlaegt das Eintragen fehl, antwortet die Route mit 422 und nennt den Grund im Feld `error`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,15}$"},"label":{"type":"string","maxLength":120}},"required":["e164"]}}}}}},"/admin/voice-numbers/assign":{"post":{"responses":{"200":{"description":"Zugeteilt. `e164` ist die Nummer, die der Mandant jetzt hat — bei der idempotenten Wiederholung also seine bereits vorhandene.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"e164":{"type":"string"}},"required":["ok","tenantId","e164"],"additionalProperties":false},"example":{"ok":true,"tenantId":"string","e164":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"},"409":{"description":"Keine freie Nummer"},"422":{"description":"Zuteilung fehlgeschlagen — `error` nennt den Grund"}},"operationId":"postAdminVoice-numbersAssign","tags":["admin","voice"],"parameters":[],"description":"Teilt einem Mandanten eine Nummer zu. Ohne e164 wird die älteste freie genommen. Idempotent: hat der Mandant schon eine, wird diese zurückgegeben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1},"e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,15}$"}},"required":["tenantId"]},"example":{"tenantId":"string"}}}},"summary":"Teilt einem Mandanten eine Nummer zu","x-nemix-summary-source":"description:first-sentence"}},"/admin/voice-numbers/release":{"post":{"responses":{"200":{"description":"Freigegeben. Der Handler prueft NICHT, ob es die Nummer gab — eine unbekannte Rufnummer wird ebenso mit `ok: true` quittiert.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"},"422":{"description":"Freigabe fehlgeschlagen — `error` nennt den Grund"}},"operationId":"postAdminVoice-numbersRelease","tags":["admin","voice"],"parameters":[],"summary":"Gibt eine Nummer zurück in den Vorrat (z. B. nach Kündigung)","description":"Setzt in `public.voice_phone_numbers` fuer die genannte Rufnummer `tenant_id` und `assigned_at` auf NULL — die Nummer steht damit wieder im Vorrat und wird beim naechsten `/assign` ohne `e164` wieder vergeben. Die Zeile selbst bleibt erhalten und behaelt `active`; geloescht wird nichts.\n\nDie Nebenwirkung reicht weiter als der Datensatz: der bisherige Mandant hat danach keine eigene Absendernummer mehr, ausgehende Anrufe fallen auf die globale Vorgabenummer zurueck.\n\nDer Handler prueft NICHT, ob es die Nummer ueberhaupt gab oder ob sie zugeteilt war: eine unbekannte Rufnummer trifft null Zeilen und wird trotzdem mit 200 und `ok: true` quittiert. 422 kommt nur, wenn das Schreiben selbst scheitert (auch ohne Datenbankverbindung — `error: db_unavailable`). Nur fuer Plattform-Admins.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,15}$"}},"required":["e164"]}}}}}},"/admin/voice-numbers/requests":{"get":{"responses":{"200":{"description":"Die passenden Anfragen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"provider_pref":{"type":["string","null"]},"area_code":{"type":["string","null"]},"number_type":{"type":["string","null"]},"note":{"type":["string","null"]},"status":{"type":"string"},"assigned_e164":{"type":["string","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","provider_pref","area_code","number_type","note","status","assigned_e164","created_by","created_at","updated_at"],"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"string","tenant_id":"string","provider_pref":"string","area_code":"string","number_type":"string","note":"string","status":"string","assigned_e164":"string","created_by":"string","created_at":"string","updated_at":"string"}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"}},"operationId":"getAdminVoice-numbersRequests","tags":["admin","voice"],"parameters":[],"summary":"Offene Nummern-Anfragen aller Mandanten (?status=all für alle)","description":"Zeigt die Nummern-Anfragen ALLER Mandanten aus `public.voice_number_requests`, aelteste zuerst. Ohne `status` sind nur die offenen dabei (`pending`); `?status=all` liefert alle, dann neueste zuerst, und jeder andere Wert filtert genau darauf. `total` ist die Laenge der gelieferten Liste. Ist die Datenbank nicht erreichbar, kommt eine LEERE Liste mit 200 statt eines Fehlers."}},"/admin/voice-numbers/requests/{id}/fulfil":{"post":{"responses":{"200":{"description":"Erledigt: die Nummer ist im Vorrat, dem anfragenden Mandanten zugeteilt und die Anfrage steht auf `provisioned`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"e164":{"type":"string"}},"required":["ok","tenantId","e164"],"additionalProperties":false},"example":{"ok":true,"tenantId":"string","e164":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"},"422":{"description":"Fehlgeschlagen — `error` nennt den Grund, etwa `anfrage_unbekannt` oder `bereits_erledigt`."}},"operationId":"postAdminVoice-numbersRequestsByIdFulfil","tags":["admin","voice"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Teilt eine beschaffte Rufnummer dem anfragenden Mandanten zu","description":"Erledigt eine Anfrage: beschaffte Nummer dem anfragenden Mandanten zuteilen und die Anfrage auf provisioned setzen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,15}$"},"label":{"type":"string","maxLength":120}},"required":["e164"]}}}}}},"/admin/ai-monitoring/usage":{"get":{"responses":{"200":{"description":"Summen und Zeitreihe. `degraded: true` heisst: nichts gelesen, nicht „nichts da\".","content":{"application/json":{"schema":{"type":"object","properties":{"period":{"type":"string","enum":["day","week","month"],"description":"Der ausgewertete Zeitraum, zurueckgespiegelt"},"totalInputTokens":{"type":"number"},"totalOutputTokens":{"type":"number"},"totalEur":{"type":"number","description":"Auf zwei Nachkommastellen gerundet"},"series":{"type":"array","items":{"type":"object","properties":{"bucket":{"type":"string","description":"Beginn des Zeitschritts als Zeitstempel"},"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"cacheTokens":{"type":"number","description":"Nur aus dem alten Kassenbuch — alles ausser input/output"},"eur":{"type":"number"}},"required":["bucket","inputTokens","outputTokens","cacheTokens","eur"]},"description":"Aufsteigend nach Zeitschritt; Schritte ohne Verbrauch fehlen ganz"},"degraded":{"type":"boolean","const":true}},"required":["period","totalInputTokens","totalOutputTokens","totalEur","series"]},"example":{"period":"day","totalInputTokens":0,"totalOutputTokens":0,"totalEur":0,"series":[{"bucket":"string","inputTokens":0,"outputTokens":0,"cacheTokens":0,"eur":0}],"degraded":true}}}},"400":{"description":"Unbekannter `period`-Wert."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`)."}},"operationId":"getAdminAi-monitoringUsage","tags":["admin","ai"],"parameters":[],"summary":"Aggregate AI token usage + EUR cost over a time period","description":"Fasst Token und Kosten ueber einen Zeitraum zusammen und liefert dazu eine Zeitreihe, gestuft nach Tag, Woche oder Monat — ueber ALLE Mandanten hinweg, nicht je Mandant.\n\nGelesen werden BEIDE Kassenbuecher: `public.cost_events` (nur Ereignisse `anthropic.%`, Betrag schon in EUR-Cent) und `public.ai_cost_events` (Betrag in USD, einmal umgerechnet). Ein Zeitpunkt kann also Betraege aus beiden Buechern enthalten.\n\n`period` ist `day`, `week` oder `month`, ohne Angabe `week`; jeder andere Wert ergibt 400 mit der Liste der erlaubten.\n\nBei fehlender Datenbank ODER einem Fehler in der ersten Abfrage kommt 200 mit Nullen, leerer Reihe und `degraded: true`. Ist nur das ZWEITE Buch unlesbar, fehlt dessen Anteil still und `degraded` bleibt weg — die Zahlen sind dann zu niedrig, ohne dass es die Antwort sagt. Nur fuer Plattform-Betreiber (`super_admin`): die `/api/admin`-App ist als Ganzes so verriegelt."}},"/admin/ai-monitoring/top-tools":{"get":{"responses":{"200":{"description":"Die teuersten Werkzeuge mit Aufrufzahl und Kosten in Euro. `degraded: true` heisst: nichts gelesen, nicht „nichts da\".","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"calls":{"type":"integer"},"eur":{"type":"number"},"tool":{"type":"string"}},"required":["calls","eur","tool"]}},"degraded":{"type":"boolean","const":true}},"required":["items"]},"example":{"items":[{"calls":0,"eur":0,"tool":"string"}],"degraded":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."}},"operationId":"getAdminAi-monitoringTop-tools","tags":["admin"],"parameters":[],"summary":"Teuerste KI-Werkzeuge","description":"Rangliste der Werkzeuge nach Kosten, teuerstes zuerst — ueber ALLE Mandanten hinweg, nicht je Mandant.\n\nDie Liste liest aus ZWEI Buechern: dem alten (`metadata->>'tool'`, ersatzweise `tool_name` oder `route`) und dem neuen (`task_type`). Ein Werkzeug, das unter beiden Namen gebucht wurde, erscheint deshalb zusammengefasst — aber nur, wenn die Schluessel woertlich uebereinstimmen.\n\n`limit` ist auf 1 bis 100 begrenzt (Standard 10); Werte ausserhalb werden stillschweigend auf die Grenze gezogen, es gibt keinen 400. Ein nicht-numerischer Wert ergibt `NaN` und damit die Untergrenze 1.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt.\n\nBei fehlender Datenbank ODER einem Fehler in der Abfrage kommt 200 mit leerer Liste UND `degraded: true`. Im Erfolgsfall fehlt der Schluessel ganz — eine leere Liste ohne `degraded` heisst also wirklich „keine Aufrufe erfasst\"."}},"/admin/ai-monitoring/top-users":{"get":{"responses":{"200":{"description":"Die teuersten Nutzer, Kennung verkuerzt.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"calls":{"type":"integer"},"eur":{"type":"number"},"userIdHash":{"type":"string"}},"required":["calls","eur","userIdHash"]}},"degraded":{"type":"boolean","const":true}},"required":["items"]},"example":{"items":[{"calls":0,"eur":0,"userIdHash":"string"}],"degraded":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."}},"operationId":"getAdminAi-monitoringTop-users","tags":["admin"],"parameters":[],"summary":"Nutzer mit den hoechsten KI-Kosten (anonymisiert)","description":"Rangliste der Nutzer nach Kosten, ueber alle Mandanten hinweg.\n\nDIE KENNUNGEN SIND VERKUERZT und kommen als `userIdHash` heraus: die ersten acht Zeichen plus die Gesamtlaenge in Klammern, etwa `a1b2c3d4…(36)`. Das ist der Grund, warum diese Liste ueberhaupt gezeigt werden darf. Die Sammelwerte `(anonymous)` und `(unknown)` bleiben unveraendert stehen — sie sind keine Kennungen.\n\nAcht Zeichen einer UUID sind nicht garantiert eindeutig: zwei Nutzer koennen theoretisch denselben `userIdHash` tragen. Fuer eine Rangliste reicht das; als Schluessel taugt er nicht.\n\nWie bei den Werkzeugen aus zwei Buechern gelesen — neu `user_id` als eigene Spalte, alt `metadata->>'user_id'`. `limit` 1 bis 100, Standard 10.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt.\n\nBei fehlender Datenbank ODER einem Fehler in der Abfrage kommt 200 mit leerer Liste UND `degraded: true`. Im Erfolgsfall fehlt der Schluessel ganz — eine leere Liste ohne `degraded` heisst also wirklich „keine Aufrufe erfasst\"."}},"/admin/ai-monitoring/quota":{"get":{"responses":{"200":{"description":"Das Budget. `configured: false` heisst: Voreinstellung, keine gespeicherte Zeile — dann fehlen die drei Verwaltungsfelder.","content":{"application/json":{"schema":{"type":"object","properties":{"tenant_id":{"type":"string"},"daily_budget_eur":{"type":"number"},"alert_at_pct":{"type":"integer"},"block_at_pct":{"type":"integer"},"emergency_override":{"type":"boolean"},"configured":{"type":"boolean"},"override_until":{"type":["string","null"]},"updated_by":{"type":["string","null"]},"updated_at":{"type":"string"}},"required":["tenant_id","daily_budget_eur","alert_at_pct","block_at_pct","emergency_override","configured"]},"example":{"tenant_id":"string","daily_budget_eur":0,"alert_at_pct":0,"block_at_pct":0,"emergency_override":true,"configured":true,"override_until":"string","updated_by":"string","updated_at":"string"}}}},"400":{"description":"`tenant_id` fehlt in der Abfrage.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"getAdminAi-monitoringQuota","tags":["admin"],"parameters":[],"summary":"Tagesbudget eines Mandanten lesen","description":"Das KI-Tagesbudget des per `tenant_id` genannten Mandanten.\n\nGIBT ES KEINE ZEILE, KOMMEN ERFUNDENE WERTE — aber ehrlich beschriftet: `configured: false` sagt, dass 5 EUR am Tag, Warnung bei 80 % und Sperre bei 100 % die Voreinstellung sind und nicht die Wahl des Betreibers. Bei einer echten Zeile steht dort `true`. Wer das Feld ignoriert, haelt eine Voreinstellung fuer eine Entscheidung.\n\nIm nicht konfigurierten Fall FEHLEN ausserdem `override_until`, `updated_by` und `updated_at` — der Rumpf ist kuerzer als bei einer echten Zeile.\n\nSEIT 17.08.2026 GIBT ES KEINE FREIGABE OHNE ABLAUF MEHR: `POST /quota` setzt bei fehlender Dauer 24 Stunden. Ein `override_until: null` bei gesetztem `emergency_override` stammt daher nur noch aus Altbestand.\n\nFrueher galt: `override_until: null` bei gesetztem `emergency_override` heisst OHNE ABLAUF: die Ausnahme gilt, bis sie jemand von Hand zuruecknimmt. Das passiert, wenn beim Setzen keine Stundenzahl mitgegeben wurde.\n\nDIESE ROUTE HAT KEINEN FEHLERFANG. Anders als die beiden Ranglisten faengt sie Datenbankfehler nicht ab — ein Fehler beim Anlegen der Tabelle oder bei der Abfrage schlaegt als 500 durch.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."},"post":{"responses":{"200":{"description":"Geschrieben. Der Rumpf ist `{ \"ok\": true }`, sonst nichts.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]},"example":{"ok":true}}}},"400":{"description":"Rumpf ungueltig — `tenant_id` fehlt, ein Prozentwert liegt ausserhalb seiner Grenzen, oder `override_hours` ist keine ganze Zahl zwischen 1 und 168."},"401":{"description":"Nicht angemeldet."},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"500":{"description":"Schreibfehler in der Datenbank — ungefangen, siehe oben."},"503":{"description":"Keine Datenbankverbindung (`{ error: \"database_unavailable\" }`)."}},"operationId":"postAdminAi-monitoringQuota","tags":["admin"],"parameters":[],"summary":"Tagesbudget eines Mandanten setzen","description":"Schreibt das KI-Tagesbudget des per `tenant_id` genannten Mandanten nach `public.ai_quotas` — eine Zeile je Mandant, angelegt oder ueberschrieben (`INSERT … ON CONFLICT DO UPDATE`).\n\nES IST KEIN TEIL-UPDATE. Das Schema setzt fuer jedes fehlende Feld eine Voreinstellung ein, und der Upsert schreibt danach ALLE Spalten. Wer nur `tenant_id` und `daily_budget_eur` schickt, setzt damit zugleich `alert_at_pct` auf 80, `block_at_pct` auf 100 und `emergency_override` auf false zurueck — auch wenn dort vorher etwas anderes stand. Zuvor lesen (`GET /quota`) und den ganzen Satz zurueckschicken.\n\nGRENZEN: `daily_budget_eur` 0 bis 10000 (0 heisst „kein Verbrauch erlaubt\", nicht „unbegrenzt\"), `alert_at_pct` 0 bis 100, `block_at_pct` 0 bis 200 (ueber 100, um bewusst ueber das Budget hinaus laufen zu lassen).\n\nNOTFALL-FREIGABE: `emergency_override: true` hebt die Sperre auf. `override_hours` sagt fuer wie lange (1 bis 168 Stunden); FEHLT DER WERT, GELTEN 24 STUNDEN. Eine Freigabe ohne Ablauf gibt es seit dem 17.08.2026 nicht mehr, `0` ist nicht mehr erlaubt. Bei `emergency_override: false` wird `override_until` auf NULL gesetzt, eine laufende Freigabe also sofort beendet.\n\n`updated_by` traegt die Benutzer-ID des Aufrufers, `updated_at` den Zeitpunkt. Die Route gibt NUR `{ \"ok\": true }` zurueck — nicht die geschriebene Zeile; zum Nachlesen `GET /quota?tenant_id=…`.\n\nDIESE ROUTE HAT KEINEN FEHLERFANG. Wie ihr Gegenstueck `GET /quota` faengt sie Datenbankfehler nicht ab — ein Fehler beim Anlegen der Tabelle oder beim Schreiben schlaegt als 500 durch.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenant_id":{"type":"string","minLength":1},"daily_budget_eur":{"type":"number","minimum":0,"maximum":10000},"alert_at_pct":{"type":"integer","minimum":0,"maximum":100,"default":80},"block_at_pct":{"type":"integer","minimum":0,"maximum":200,"default":100},"emergency_override":{"type":"boolean","default":false},"override_hours":{"type":"integer","minimum":1,"maximum":168,"description":"Wie lange die Notfall-Freigabe gilt, in Stunden (1 bis 168). Fehlt der Wert, gelten 24 Stunden — eine Freigabe OHNE Ablauf gibt es seit dem 17.08.2026 nicht mehr. Frueher war `0` erlaubt und bedeutete unbegrenzt."}},"required":["tenant_id","daily_budget_eur"]},"example":{"tenant_id":"string","daily_budget_eur":0,"alert_at_pct":0,"block_at_pct":0,"emergency_override":true,"override_hours":1}}}}}},"/admin/ai-monitoring/pricing":{"get":{"responses":{"200":{"description":"Alle hinterlegten Modelle plus die Preise des heutigen Standardmodells (`claude-sonnet-4-6`).","content":{"application/json":{"schema":{"type":"object","properties":{"models":{"type":"array","items":{"type":"object","additionalProperties":{}}},"current_default":{}},"required":["models"]},"example":{"models":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."}},"operationId":"getAdminAi-monitoringPricing","tags":["admin"],"parameters":[],"summary":"Hinterlegte Modellpreise","description":"Die Preistabelle, mit der alle Euro-Betraege dieser Oberflaeche gerechnet werden — je Modell die Kosten fuer Ein- und Ausgabe.\n\nDIE WERTE STEHEN IM QUELLTEXT, nicht in der Datenbank und nicht beim Anbieter. Sie sind der Stand bei der letzten Aenderung dieser Tabelle; aendert Anthropic seine Preise, aendert sich hier nichts von selbst. Deshalb ist diese Route der ehrlichste Ort, um zu pruefen, womit die Kostenzahlen ueberhaupt zustande kommen.\n\nReine Auskunft, keine Datenbank — antwortet immer mit 200.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."}},"/admin/ai-audit":{"get":{"responses":{"200":{"description":"Die gefundenen Ereignisse. Steht `note` dabei, ist die Liste leer, WEIL die Tabelle fehlt — nicht, weil es nichts gab.","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","additionalProperties":{}}},"total":{"type":"integer"},"note":{"type":"string"}},"required":["events","total"]},"example":{"events":[{}],"total":0,"note":"string"}}}},"400":{"description":"Abfrageparameter ungueltig (z. B. `limit` groesser als 1000)."},"401":{"description":"Nicht angemeldet."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"503":{"description":"Keine Datenbankverbindung (`{ error: \"DB not available\" }`)."}},"operationId":"getAdminAi-audit","tags":["admin","ai"],"parameters":[{"in":"query","name":"userId","schema":{"type":"string"}},{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":1000,"default":100}},{"in":"query","name":"tenantId","schema":{"type":"string"}}],"summary":"KI-Protokoll ueber alle Mandanten lesen","description":"Liest das KI-Aufrufprotokoll aus `public.ai_cost_events` — je Zeile ein Modellaufruf mit Mandant, Benutzer, Modell, Token-Zahlen, Kosten und Zeitpunkt.\n\nOHNE FILTER LIEST DIESE ROUTE UEBER ALLE MANDANTEN HINWEG. `tenantId` grenzt auf einen ein, `userId` auf einen Benutzer, `from`/`to` auf einen Zeitraum (ISO-Zeitstempel; `from` ist einschliesslich, `to` ausschliesslich). `limit` begrenzt auf 1 bis 1000 Zeilen, Voreinstellung 100 — es gibt KEINE Blaetterung und keine Gesamtzahl: `total` zaehlt nur die zurueckgegebenen Zeilen, nicht die vorhandenen.\n\nDIE FELDER KOMMEN ROH AUS DER DATENBANK, also in Unterstrich-Schreibweise (`tenant_id`, `input_tokens`, `cost_usd`, `created_at`). Die Mandanten-Route `/api/v1/tenant/ai/audit` benennt dieselben Werte in camelCase um — wer beide anspricht, bekommt zwei Formen.\n\n`cost_usd` kommt als Zeichenkette (NUMERIC), nicht als Zahl.\n\nACHTUNG, EINE LEERE LISTE HEISST ZWEIERLEI: entweder es gibt keine Ereignisse, oder `public.ai_cost_events` fehlt. Der Handler faengt jeden Abfragefehler ab und antwortet trotzdem mit 200 und leerer Liste — unterscheidbar allein am zusaetzlichen Feld `note`. Nur eine fehlende Datenbankverbindung ergibt 503.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429). Ein Mandanten-Admin kommt hier NICHT durch."}},"/admin/ai-audit/right-to-erasure":{"post":{"responses":{"200":{"description":"Geloescht. `tables` nennt die beiden geleerten Tabellen, `note` erinnert an die CloudWatch-/S3-Protokolle.","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"erased":{"type":"boolean"},"tables":{"type":"array","items":{"type":"string"}},"timestamp":{"type":"string"},"note":{"type":"string"}},"required":["userId","erased","tables","timestamp","note"]},"example":{"userId":"string","erased":true,"tables":["string"],"timestamp":"string","note":"string"}}}},"400":{"description":"Validierungsfehler: `userId` fehlt, oder `reason` ist laenger als 500 Zeichen. Der frueher hier beschriebene fehlende Mandantenkontext ist seit dem 30.08.2026 kein Fall mehr."},"401":{"description":"Nicht angemeldet."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"404":{"description":"Der Benutzer gehoert nicht zu diesem Mandanten — es wurde nichts geloescht (`{ \"error\": \"User not found in this tenant\" }`)."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"500":{"description":"Loeschung fehlgeschlagen; die Transaktion ist zurueckgerollt, es ist nichts halb geloescht (`{ \"error\": \"Erasure failed — tables may not exist yet\" }`)."},"503":{"description":"Keine Datenbankverbindung (`{ error: \"DB not available\" }`)."}},"operationId":"postAdminAi-auditRight-to-erasure","tags":["admin","ai"],"parameters":[],"summary":"Loeschrecht DSGVO Art. 17 — KI-Protokoll eines Benutzers loeschen","description":"Loescht das KI-Protokoll EINES Benutzers endgueltig.\n\nWAS GELOESCHT WIRD — zwei Tabellen, beide vollstaendig fuer diesen Benutzer, ohne Mengenbegrenzung:\n- `public.ai_cost_events` — jeder Modellaufruf (Token, Kosten, Zeitpunkt)\n- `public.ai_user_feedback` — jede Rueckmeldung des Benutzers zur KI\n\nBeide Loeschungen laufen in EINER Transaktion (`sql.begin`): entweder beide oder keine. Es gibt keine Obergrenze und keinen Stapelbetrieb — sind es hunderttausend Zeilen, gehen hunderttausend Zeilen.\n\nES IST EIN ECHTES `DELETE`, NICHT UMKEHRBAR. Kein Papierkorb, kein `deleted_at`, keine Kopie. Nach der Antwort sind die Daten fort.\n\nES GIBT KEINEN TROCKENLAUF. Das Rumpf-Schema kennt genau zwei Felder — `userId` (Pflicht) und `reason` (frei, hoechstens 500 Zeichen, wandert nur in die Server-Warnung). Ein mitgeschicktes `dryRun`/`preview` wird stillschweigend verworfen und loescht trotzdem. Wer vorher wissen will, wie viel betroffen ist, zaehlt es mit `GET /admin/ai-audit?userId=…` ab — diese Route zaehlt nicht vor.\n\nNICHT GELOESCHT WERDEN die CloudWatch-/S3-Protokolle; die muss jemand von Hand ueber die AWS-CLI entfernen. Das Antwortfeld `note` sagt es nochmals. Die Loeschung selbst wird als `console.warn` festgehalten.\n\nDER MANDANT KOMMT AUS DER SITZUNG, NICHT AUS DEM RUMPF. Beide `DELETE` sind zusaetzlich auf `tenant_id` eingegrenzt, und vorab prueft der Handler, ob der Benutzer ueberhaupt zu diesem Mandanten gehoert (sonst 404). Einen Parameter, um den Mandanten zu waehlen, gibt es bewusst nicht.\n\nBIS ZUM 30.08.2026 ANTWORTETE DIESE ROUTE IMMER MIT 400 und loeschte nie etwas: sie las `c.get('tenant')`, und den Mandantenkontext setzt allein `tenantMiddleware`, die nur an der `/api/v1`-Sub-App haengt — nicht an der admin-Sub-App. Wer sich auf die Loeschung verliess, erfuellte Art. 17 NICHT.\n\nSEITHER WIRD DER MANDANT AUS DEM BENUTZER ABGELEITET (`public.users.tenant_id`). Er kann damit weder fehlen noch vom Aufrufer gewaehlt werden — ein Mandant im Rumpf haette mandantenuebergreifendes Loeschen erlaubt. Ist zusaetzlich ein Sitzungsmandant gesetzt, MUSS er zum Benutzer passen, sonst 404.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429). Ein Mandanten-Admin kommt hier NICHT durch.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","minLength":1},"reason":{"type":"string","maxLength":500}},"required":["userId"]},"example":{"userId":"string","reason":"string"}}}}}},"/admin/ai-costs":{"get":{"responses":{"200":{"description":"Kosten je Mandant. `month` fehlt, wenn die Anfrage keinen Monat nannte. Steht `note` dabei, ist die Liste leer, WEIL die Tabelle fehlt.","content":{"application/json":{"schema":{"type":"object","properties":{"month":{"type":"string"},"tenants":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"requestCount":{"type":"integer"},"costUsd":{"type":"number"}},"required":["tenantId","requestCount","costUsd"]}},"note":{"type":"string"}},"required":["tenants"]},"example":{"month":"string","tenants":[{"tenantId":"string","requestCount":0,"costUsd":0}],"note":"string"}}}},"400":{"description":"`month` passt nicht auf `JJJJ-MM`."},"401":{"description":"Nicht angemeldet."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"503":{"description":"Keine Datenbankverbindung (`{ error: \"DB not available\" }`)."}},"operationId":"getAdminAi-costs","tags":["admin","ai","costs"],"parameters":[{"in":"query","name":"month","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$"}},{"in":"query","name":"tenantId","schema":{"type":"string"}}],"summary":"KI-Kosten je Mandant fuer einen Monat","description":"Summiert `public.ai_cost_events` eines Kalendermonats und gibt je Mandant die Zahl der Aufrufe und die Kosten zurueck, absteigend nach Kosten.\n\n`month` waehlt den Monat im Format `JJJJ-MM`; ohne Angabe rechnet die Route den laufenden Monat (UTC). `tenantId` grenzt auf einen Mandanten ein — ohne das Feld sind ALLE Mandanten dabei.\n\nHOECHSTENS 200 MANDANTEN. Die Abfrage endet auf `LIMIT 200`; es gibt keine Blaetterung und keinen Hinweis darauf, dass abgeschnitten wurde. Bei mehr als 200 Mandanten mit Verbrauch fehlen die guenstigsten stillschweigend.\n\nDAS FELD `month` IM RUMPF SPIEGELT NUR DIE ANFRAGE. Es traegt den uebergebenen Wert, nicht den gerechneten Monat — wer `month` weglaesst, bekommt eine Antwort OHNE `month`, obwohl ueber den laufenden Monat gerechnet wurde. Die Mandanten-Route `/api/v1/tenant/ai/costs` macht es anders herum und gibt den gerechneten Monat zurueck.\n\nDIE BETRAEGE SIND US-DOLLAR. Die Spalte heisst `cost_usd`, das Feld `costUsd`; auf vier Nachkommastellen gerundet. Nicht mit den Euro-Betraegen der KI-Ueberwachung (`/admin/ai-monitoring/*`) verwechseln.\n\nEINE LEERE LISTE HEISST ZWEIERLEI: keine Kosten, oder `public.ai_cost_events` fehlt. Der Handler faengt jeden Abfragefehler ab und antwortet trotzdem mit 200 — unterscheidbar allein am zusaetzlichen Feld `note`. Nur eine fehlende Datenbankverbindung ergibt 503.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429)."}},"/admin/flags":{"get":{"responses":{"200":{"description":"Flags list — the raw rows, in store order.","content":{"application/json":{"schema":{"type":"object","properties":{"flags":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"rolloutPercent":{"type":"number"},"targetingRules":{"type":"array","items":{"type":"object","properties":{"attribute":{"type":"string","enum":["plan","industry","region","tenantId","userId"]},"operator":{"type":"string","enum":["equals","in","notIn","startsWith","contains"]},"values":{"type":"array","items":{"type":"string"}},"result":{"type":"boolean"}},"required":["attribute","operator","values"],"additionalProperties":false}},"updatedAt":{"type":"string"}},"required":["name","enabled","rolloutPercent","targetingRules","updatedAt"],"additionalProperties":false}}},"required":["flags"],"additionalProperties":false},"example":{"flags":[{"name":"string","enabled":true,"rolloutPercent":0,"targetingRules":[{"attribute":"plan","operator":"equals","values":["string"],"result":true}],"updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin role required"}},"operationId":"getAdminFlags","tags":["flags","admin"],"parameters":[],"description":"List all feature flags (admin only). Returns the stored flag rows — name, master switch, rollout percentage, targeting rules and last change — NOT the decisions for any tenant; for those use `GET /api/v1/flags/eval`. The list is global, not scoped to a tenant, and comes without paging or filters. The role check is a step ladder, so `super_admin` passes it too.","summary":"List all feature flags (admin only)","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"200":{"description":"Flag upserted — the stored row as it now stands, create and update alike.","content":{"application/json":{"schema":{"type":"object","properties":{"flag":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"rolloutPercent":{"type":"number"},"targetingRules":{"type":"array","items":{"type":"object","properties":{"attribute":{"type":"string","enum":["plan","industry","region","tenantId","userId"]},"operator":{"type":"string","enum":["equals","in","notIn","startsWith","contains"]},"values":{"type":"array","items":{"type":"string"}},"result":{"type":"boolean"}},"required":["attribute","operator","values"],"additionalProperties":false}},"updatedAt":{"type":"string"}},"required":["name","enabled","rolloutPercent","targetingRules","updatedAt"],"additionalProperties":false}},"required":["flag"],"additionalProperties":false},"example":{"flag":{"name":"string","enabled":true,"rolloutPercent":0,"targetingRules":[{"attribute":"plan","operator":"equals","values":["string"],"result":true}],"updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin role required"},"422":{"description":"Invalid body"}},"operationId":"postAdminFlags","tags":["flags","admin"],"parameters":[],"description":"Upsert a feature flag (admin only). This is a FULL write, not a patch: `name` is the only required field, and every field left out is reset — a missing `enabled` stores `false`, a missing `rolloutPercent` stores 0, and missing or malformed `targetingRules` store an empty list. Rules that lack a string `attribute`/`operator` or an array `values` are dropped silently, so a typo costs you the rule without an error. Answers 200 on both create and update — there is no 201.","summary":"Upsert a feature flag (admin only)","x-nemix-summary-source":"description:first-sentence"}},"/admin/flags/{name}":{"delete":{"responses":{"200":{"description":"Deleted — the requested name, echoed back. Nothing else.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin role required"}},"operationId":"deleteAdminFlagsByName","tags":["flags","admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"description":"Delete a feature flag by name (admin only). The row is removed from the store; there is no soft delete and no way back. The call answers 200 even when no flag by that name existed — it never returns 404, so the echoed name is not proof that anything was removed. Afterwards `GET /api/v1/flags/eval` reports the name as `value: false` with reason `unknown`, and subscribers of the SSE stream receive a `flag.delete` event.","summary":"Delete a feature flag by name (admin only)","x-nemix-summary-source":"description:first-sentence"}},"/admin/migrations":{"get":{"responses":{"200":{"description":"Die Migrationen mit Einstufung — ODER, bei gesetztem `error`, ein FEHLSCHLAG mit leerer Liste.","content":{"application/json":{"schema":{"type":"object","properties":{"migrations":{"type":"array","items":{"type":"object","properties":{"safetyClass":{"type":"string"},"reasons":{"type":"array","items":{"type":"string"}},"affectedTables":{"type":"array","items":{"type":"string"}},"requiresApproval":{"type":"boolean"},"version":{"type":"string"},"description":{"type":"string"},"sqlPreview":{"type":"string"},"status":{"type":"string"},"appliedAt":{"type":["string","null"]}},"required":["safetyClass","reasons","affectedTables","requiresApproval","version","description","sqlPreview","status","appliedAt"]}},"error":{"type":"string"}},"required":["migrations"]},"example":{"migrations":[{"safetyClass":"string","reasons":["string"],"affectedTables":["string"],"requiresApproval":true,"version":"string","description":"string","sqlPreview":"string","status":"string","appliedAt":"string"}],"error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"operationId":"getAdminMigrations","tags":["admin"],"parameters":[],"summary":"Alle Migrationen mit Sicherheitseinstufung auflisten","description":"Listet jede bekannte Migration mit ihrer Einstufung: wie riskant sie ist, welche Tabellen sie anfasst, ob sie eine Freigabe braucht, und ob sie schon angewandt wurde.\n\nDIESE LESEROUTE SCHREIBT. Sie traegt die errechnete Einstufung fuer JEDE Migration in `public.schema_migrations_meta` nach — ein Upsert je Zeile, in einer Schleife. Ein GET mit Nebenwirkung; bei vielen Migrationen sind das entsprechend viele Schreibvorgaenge pro Aufruf.\n\nDIE SQL WIRD NICHT AUSGEFUEHRT, um sie einzustufen: die Migration laeuft gegen einen mitschreibenden Stellvertreter, der die Anweisungen nur einsammelt. Keine Datenbank wird dabei angefasst.\n\n`sqlPreview` ist auf 300 Zeichen gekuerzt und endet dann mit `…` — es ist eine Vorschau, keine vollstaendige Anweisung.\n\nEIN FEHLER KOMMT HIER ALS **200** ZURUECK, nicht als 500: die Antwort traegt dann eine leere Liste UND einen `error`-Schluessel. Wer nur auf den Statuscode sieht, haelt einen Fehlschlag fuer „keine Migrationen vorhanden\". Das Feld ist der einzige Unterschied.\n\nNur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"/admin/migrations/analyze":{"post":{"responses":{"200":{"description":"Der Befund: Einstufung, Begruendungen, betroffene Tabellen und ob eine Freigabe noetig waere.","content":{"application/json":{"schema":{"type":"object","properties":{"safetyClass":{"type":"string"},"reasons":{"type":"array","items":{"type":"string"}},"affectedTables":{"type":"array","items":{"type":"string"}},"requiresApproval":{"type":"boolean"}},"required":["safetyClass","reasons","affectedTables","requiresApproval"]},"example":{"safetyClass":"string","reasons":["string"],"affectedTables":["string"],"requiresApproval":true}}}},"400":{"description":"`sql` oder `version` fehlt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"operationId":"postAdminMigrationsAnalyze","tags":["admin"],"parameters":[],"summary":"Beliebige SQL auf Risiko pruefen","description":"Stuft eine mitgeschickte SQL-Anweisung ein, ohne sie auszufuehren und ohne sie irgendwo zu hinterlegen. Gedacht, um eine geplante Migration vorab zu beurteilen.\n\nDer Rumpf braucht `sql` und `version`; fehlt eines, gibt es 400. Ein RUMPF, DER KEIN JSON IST, ergibt keinen 400, sondern einen 500 — er wird ungeschuetzt gelesen. Ein Eingabeschema gibt es nicht.\n\n`version` dient nur der Beschriftung des Befunds; es wird nicht gegen die bekannten Migrationen geprueft. Ein erfundener Name ist erlaubt.\n\nDie Antwort ist der Befund SELBST, nicht in einen Umschlag gepackt.\n\nReine Auskunft: nichts wird ausgefuehrt, nichts gespeichert.\n\nNur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"/admin/migrations/{version}/approve":{"post":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."},"501":{"description":"Immer. `ok: false`, `error: \"not_implemented\"`, dazu die Kennung und eine Begruendung im Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","const":"not_implemented"},"version":{"type":"string"},"message":{"type":"string"}},"required":["ok","error","version","message"]}}}}},"operationId":"postAdminMigrationsByVersionApprove","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"version","required":true}],"summary":"Freigabe einer Migration — NICHT gebaut, antwortet 501","description":"DIESE ROUTE GIBT NICHTS FREI. Sie antwortet immer **501** mit `error: \"not_implemented\"`, weil es keine Ablage fuer Migrations-Freigaben gibt.\n\nDas ist eine bewusste Entscheidung und die Beschreibung sagt warum: bis 07.08.2026 lieferte sie `{ approved: true, approvedBy, approvedAt }` — und hinterlegte nichts davon. Der naechste Aufruf, der nach Freigaben sah, fand nichts; wer freigegeben hatte, war nicht feststellbar.\n\nBei einer Migrations-Freigabe ist das die teuerste Sorte Attrappe: sie erzeugt genau das Vier-Augen-Gefuehl, das ein Freigabeschritt geben soll, ohne dass zwei Augen nachweisbar hingesehen haetten. Ein 501 ist hier besser als ein `true`, weil es den fehlenden Schritt SICHTBAR macht, statt ihn zu ersetzen.\n\nWer eine Freigabe braucht, kann sie ueber diese API also nicht erteilen — und soll das auch nicht glauben. `version` kommt in der Antwort zurueck, damit ein Aufrufer sieht, worauf sich die Absage bezieht.\n\nNur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"/admin/rollouts/analyze":{"post":{"responses":{"200":{"description":"Analysis result. Also the answer when the database is unreachable or the analysis throws — the report then carries only the common fields and a recommendation to check by hand. There is no error status on this route.","content":{"application/json":{"schema":{"type":"object","properties":{"compatible":{"type":"boolean","description":"False when at least one tenant needs manual migration"},"breakingChanges":{"type":"array","items":{"type":"string"},"description":"One \"<tenantId>: <description>\" line per breaking change"},"affectedTenants":{"type":"integer"},"recommendation":{"type":"string"},"aiSummary":{"type":"string"},"incomingBaseVersion":{"type":"string"},"currentBaseVersion":{"type":"string"},"totalTenants":{"type":"integer"},"safeTenants":{"type":"integer"},"autoMigratableCount":{"type":"integer"},"requiresManualInterventionCount":{"type":"integer"},"tenantBreakdown":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"kundeLayerId":{"type":"string"},"breakingCount":{"type":"integer"},"deprecatedCount":{"type":"integer"},"autoMigratable":{"type":"boolean"},"breaking":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["field_removed","field_type_changed","required_field_added","api_path_changed","deprecated_field"]},"path":{"type":"string","description":"Dot-path of the affected field, e.g. \"customers.kunde_credit_limit\""},"affectedContributionIds":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"previousType":{"type":"string"},"newType":{"type":"string"}},"required":["kind","path","affectedContributionIds","description"]}}},"required":["tenantId","kundeLayerId","breakingCount","deprecatedCount","autoMigratable","breaking"]},"description":"Only the tenants whose report is not safe"}},"required":["compatible","breakingChanges","affectedTenants","recommendation","aiSummary"]},"example":{"compatible":true,"breakingChanges":["string"],"affectedTenants":0,"recommendation":"string","aiSummary":"string","incomingBaseVersion":"string","currentBaseVersion":"string","totalTenants":0,"safeTenants":0,"autoMigratableCount":0,"requiresManualInterventionCount":0,"tenantBreakdown":[{"tenantId":"string","kundeLayerId":"string","breakingCount":0,"deprecatedCount":0,"autoMigratable":true,"breaking":[{"kind":"field_removed","path":"string","affectedContributionIds":["string"],"description":"string","previousType":"string","newType":"string"}]}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postAdminRolloutsAnalyze","tags":["admin","rollouts"],"parameters":[],"summary":"Analyses breaking changes against every active tenant before a rollout","description":"Pre-rollout compatibility analysis: runs the layer-engine update-checker against every active tenant and aggregates the breaking changes."}},"/admin/rollouts":{"get":{"responses":{"200":{"description":"Liste der Rollouts samt Hinweis zur Fluechtigkeit","content":{"application/json":{"schema":{"type":"object","properties":{"rollouts":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Kennung des Rollouts — zugleich der Name des verknuepften Feature-Flags"},"stages":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Name der Stufe, z. B. \"staging\" oder \"50pct\""},"percent":{"type":"number","description":"Anteil in Prozent, 0..100"}},"required":["label","percent"]},"description":"Stufenbahn, aufsteigend; erste 0 %, letzte 100 %"},"currentStage":{"type":"number","description":"0-basierter Index in `stages`"},"automaticPromotion":{"type":"boolean","description":"Darf die Engine selbst weiterschalten?"},"criteria":{"type":"object","additionalProperties":{},"description":"Bedingungen fuer automatisches Weiterschalten"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["name","stages","currentStage","automaticPromotion","criteria"]}},"speicherFluechtig":{"type":"boolean","const":true,"description":"Solange gesetzt: der Zustand liegt nur im Arbeitsspeicher und ist nach einem Neustart weg"},"hinweis":{"type":"string","description":"Klartext-Erklaerung der Fluechtigkeit fuer die Oberflaeche"}},"required":["rollouts","speicherFluechtig","hinweis"]},"example":{"rollouts":[{"name":"string","stages":[{"label":"string","percent":0}],"currentStage":0,"automaticPromotion":true,"criteria":{},"createdAt":"string","updatedAt":"string"}],"speicherFluechtig":true,"hinweis":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Adminrolle noetig"}},"operationId":"getAdminRollouts","tags":["admin","rollouts"],"parameters":[],"description":"Listet alle Rollouts. ACHTUNG: der Zustand liegt nur im Arbeitsspeicher — die Antwort sagt das ueber `speicherFluechtig` und `hinweis`.","summary":"Listet alle Rollouts","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Angelegt — startet in Staging (0 %)","content":{"application/json":{"schema":{"type":"object","properties":{"rollout":{"type":"object","properties":{"name":{"type":"string","description":"Kennung des Rollouts — zugleich der Name des verknuepften Feature-Flags"},"stages":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Name der Stufe, z. B. \"staging\" oder \"50pct\""},"percent":{"type":"number","description":"Anteil in Prozent, 0..100"}},"required":["label","percent"]},"description":"Stufenbahn, aufsteigend; erste 0 %, letzte 100 %"},"currentStage":{"type":"number","description":"0-basierter Index in `stages`"},"automaticPromotion":{"type":"boolean","description":"Darf die Engine selbst weiterschalten?"},"criteria":{"type":"object","additionalProperties":{},"description":"Bedingungen fuer automatisches Weiterschalten"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["name","stages","currentStage","automaticPromotion","criteria"]}},"required":["rollout"]},"example":{"rollout":{"name":"string","stages":[{"label":"string","percent":0}],"currentStage":0,"automaticPromotion":true,"criteria":{},"createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"Ungueltige Eingabe oder nicht unterstuetzter Termin","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbare Kennung"},"hinweis":{"type":"string","description":"Deutsche Erklaerung fuer die Oberflaeche"},"details":{"type":"array","items":{"type":"string"}}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Adminrolle noetig"}},"operationId":"postAdminRollouts","tags":["admin","rollouts"],"parameters":[],"description":"Legt einen Rollout an (oder ersetzt ihn). Erwartet PROZENTSTUFEN, z. B. [10,50,100]; die Staging-Stufe (0 %) wird ergaenzt. Ein `scheduledAt` wird ABGELEHNT: zeitgesteuerte Rollouts sind nicht gebaut, es gibt keinen Planer.","summary":"Legt einen Rollout an (oder ersetzt ihn)","x-nemix-summary-source":"description:first-sentence"}},"/admin/rollouts/{name}/promote":{"post":{"responses":{"200":{"description":"Neuer Stand des Rollouts","content":{"application/json":{"schema":{"type":"object","properties":{"rollout":{"type":"object","properties":{"name":{"type":"string","description":"Kennung des Rollouts — zugleich der Name des verknuepften Feature-Flags"},"stages":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Name der Stufe, z. B. \"staging\" oder \"50pct\""},"percent":{"type":"number","description":"Anteil in Prozent, 0..100"}},"required":["label","percent"]},"description":"Stufenbahn, aufsteigend; erste 0 %, letzte 100 %"},"currentStage":{"type":"number","description":"0-basierter Index in `stages`"},"automaticPromotion":{"type":"boolean","description":"Darf die Engine selbst weiterschalten?"},"criteria":{"type":"object","additionalProperties":{},"description":"Bedingungen fuer automatisches Weiterschalten"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["name","stages","currentStage","automaticPromotion","criteria"]}},"required":["rollout"]},"example":{"rollout":{"name":"string","stages":[{"label":"string","percent":0}],"currentStage":0,"automaticPromotion":true,"criteria":{},"createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Adminrolle noetig"},"404":{"description":"Rollout unbekannt"},"409":{"description":"Letzte Stufe erreicht"}},"operationId":"postAdminRolloutsByNamePromote","tags":["admin","rollouts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"summary":"Schaltet einen Rollout eine Stufe weiter","description":"Schaltet den Rollout eine Stufe weiter und spiegelt den neuen Prozentsatz ins Flag."}},"/admin/rollouts/{name}/revert":{"post":{"responses":{"200":{"description":"Neuer Stand des Rollouts","content":{"application/json":{"schema":{"type":"object","properties":{"rollout":{"type":"object","properties":{"name":{"type":"string","description":"Kennung des Rollouts — zugleich der Name des verknuepften Feature-Flags"},"stages":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Name der Stufe, z. B. \"staging\" oder \"50pct\""},"percent":{"type":"number","description":"Anteil in Prozent, 0..100"}},"required":["label","percent"]},"description":"Stufenbahn, aufsteigend; erste 0 %, letzte 100 %"},"currentStage":{"type":"number","description":"0-basierter Index in `stages`"},"automaticPromotion":{"type":"boolean","description":"Darf die Engine selbst weiterschalten?"},"criteria":{"type":"object","additionalProperties":{},"description":"Bedingungen fuer automatisches Weiterschalten"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["name","stages","currentStage","automaticPromotion","criteria"]}},"required":["rollout"]},"example":{"rollout":{"name":"string","stages":[{"label":"string","percent":0}],"currentStage":0,"automaticPromotion":true,"criteria":{},"createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Adminrolle noetig"},"404":{"description":"Rollout unbekannt"},"409":{"description":"Bereits in Staging"}},"operationId":"postAdminRolloutsByNameRevert","tags":["admin","rollouts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"description":"Nimmt den Rollout eine Stufe zurueck. Unterhalb von Staging (0) wird abgelehnt.","summary":"Nimmt den Rollout eine Stufe zurueck","x-nemix-summary-source":"description:first-sentence"}},"/admin/_internal/otel-test":{"get":{"responses":{"200":{"description":"Zustand der Ablaufverfolgung; Form je nach Fall.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"enabled":{"type":"boolean","const":false},"traceId":{"type":"null"},"message":{"type":"string","description":"Gesetzt, wenn die Ausfuhr gar nicht konfiguriert ist."},"error":{"type":"string","description":"Gesetzt, wenn das Paket fehlt."}},"required":["enabled","traceId"]},{"type":"object","properties":{"enabled":{"type":"boolean","const":true},"traceId":{"type":"string","description":"Zum Nachschlagen im Trace-Betrachter."},"spanCount":{"type":"integer","description":"Erzeugte Spannen; hier immer 3."},"lookup":{"type":["string","null"],"description":"Fertige Suchzeile fuer Honeycomb; `null`, wenn keine Kennung zustande kam."}},"required":["enabled","traceId","spanCount","lookup"]}]},"example":{"enabled":false,"traceId":null,"message":"string","error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdmin_internalOtel-test","tags":["Betrieb"],"parameters":[],"summary":"Ausfuhr der Ablaufverfolgung pruefen","description":"Erzeugt drei ineinanderliegende Spannen und gibt die Trace-Kennung\nzurueck, damit man sie im Betrachter (Honeycomb, Tempo, Jaeger)\nwiederfindet. Diagnosewerkzeug, keine Fachfunktion.\n\nIMMER 200, drei Faelle:\n· Ausfuhr aus  → `enabled: false`, `traceId: null`, `message`.\n· Paket fehlt  → `enabled: false`, `traceId: null`, `error`.\n· Ausfuhr an   → `enabled: true`, `traceId`, `spanCount: 3`.\n\nBewusst kein Fehlerstatus: eine Diagnoseroute, die selbst 5xx wirft,\nist im Stoerfall nutzlos. Ob die Ausfuhr laeuft, sagt `enabled`.\n\nLiegt unter `/api/admin` und ist damit als Ganzes `requireSuperAdmin`.\nDer Kopfkommentar dieser Datei nannte bis heute den falschen Pfad\n(`/api/v1/_internal/…`) — der Mount war immer der Admin-Bereich."}},"/admin/backendherz/overview":{"get":{"responses":{"200":{"description":"Ops overview snapshot. Auch die Antwort ohne Datenbank — dann stehen alle Zahlen auf 0 und die Ampel auf rot. Ein einzelner fehlgeschlagener Teilwert faellt auf 0 zurueck, ohne die uebrige Uebersicht zu leeren.","content":{"application/json":{"schema":{"type":"object","properties":{"tenants":{"type":"object","properties":{"total":{"type":"number"},"active":{"type":"number"},"new7d":{"type":"number"},"new30d":{"type":"number"}},"required":["total","active","new7d","new30d"]},"users":{"type":"object","properties":{"total":{"type":"number"},"verified":{"type":"number"},"active30d":{"type":"number"},"new7d":{"type":"number"}},"required":["total","verified","active30d","new7d"]},"ai":{"type":"object","properties":{"costTodayEur":{"type":"number","description":"USD-Kosten des laufenden Tages, mit festem Kurs in EUR"},"costMonthEur":{"type":"number","description":"Dasselbe fuer den laufenden Monat"},"tokensToday":{"type":"number"},"callsToday":{"type":"number"}},"required":["costTodayEur","costMonthEur","tokensToday","callsToday"]},"support":{"type":"object","properties":{"unreadChats":{"type":"number"},"tenantsScanned":{"type":"number"}},"required":["unreadChats","tenantsScanned"]},"health":{"type":"object","properties":{"db":{"type":"string","enum":["up","down","unknown"]},"redis":{"type":"string","enum":["up","down","unknown"],"description":"unknown, wenn kein Redis konfiguriert ist"},"aiProvider":{"type":"string","enum":["up","down","unknown"]},"overall":{"type":"string","enum":["green","yellow","red"],"description":"rot nur bei Datenbankausfall"}},"required":["db","redis","aiProvider","overall"]},"server":{"type":"object","properties":{"uptimeSec":{"type":"number"},"rssMb":{"type":"number"},"heapUsedMb":{"type":"number"},"heapTotalMb":{"type":"number"},"heapUsedPct":{"type":"number"},"eventLoopLagMs":{"type":"number"}},"required":["uptimeSec","rssMb","heapUsedMb","heapTotalMb","heapUsedPct","eventLoopLagMs"]},"generatedAt":{"type":"string"}},"required":["tenants","users","ai","support","health","server","generatedAt"]},"example":{"tenants":{"total":0,"active":0,"new7d":0,"new30d":0},"users":{"total":0,"verified":0,"active30d":0,"new7d":0},"ai":{"costTodayEur":0,"costMonthEur":0,"tokensToday":0,"callsToday":0},"support":{"unreadChats":0,"tenantsScanned":0},"health":{"db":"up","redis":"up","aiProvider":"up","overall":"green"},"server":{"uptimeSec":0,"rssMb":0,"heapUsedMb":0,"heapTotalMb":0,"heapUsedPct":0,"eventLoopLagMs":0},"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminBackendherzOverview","tags":["admin"],"parameters":[],"summary":"Sammeluebersicht fuer Super-Admins: Mandanten, Nutzer, KI-Kosten, Betrieb","description":"Unified super-admin ops overview: tenants, users, AI cost, support, health, server — one aggregate."}},"/admin/backendherz/tenants-health":{"get":{"responses":{"200":{"description":"Hoechstens 60 aktive Mandanten, neueste zuerst. Ohne Datenbank oder bei einem Fehler kommt dieselbe Form mit leerer Liste, nicht ein Fehlerstatus; ein Mandant, dessen Teilabfragen scheitern, faellt aus der Liste, ohne die uebrigen zu verlieren.","content":{"application/json":{"schema":{"type":"object","properties":{"tenants":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"plan":{"type":["string","null"]},"status":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"users":{"type":"number","description":"Sitzplaetze — Nutzer des Mandanten ohne geloeschte"},"lastActivityMs":{"type":["number","null"],"description":"Letzte Anmeldung als Unix-Zeit in Millisekunden"}},"required":["id","slug","name","plan","status","createdAt","users","lastActivityMs"]}},"totals":{"type":"object","properties":{"users":{"type":"number"}},"required":["users"]},"count":{"type":"number"},"generatedAt":{"type":"string","description":"Fehlt in der leeren Ersatzantwort"}},"required":["tenants","totals","count"]},"example":{"tenants":[{"id":"string","slug":"string","name":"string","plan":"string","status":"string","createdAt":"string","users":0,"lastActivityMs":0}],"totals":{"users":0},"count":0,"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminBackendherzTenants-health","tags":["admin"],"parameters":[],"summary":"Betriebsuebersicht je Mandant: Plan, Status, Sitzplaetze, letzte Anmeldung","description":"Cross-tenant Betriebsuebersicht: je Mandant Plan, Status, Sitzplaetze, letzte Anmeldung. Bewusst OHNE Geschaeftszahlen der Mandanten."}},"/admin/support/inbox":{"get":{"responses":{"200":{"description":"Aggregiertes Support-Postfach. Ein Mandant, dessen Tabelle fehlt oder dessen Abfrage scheitert, wird uebersprungen — die Antwort bleibt 200 und die uebrigen Mandanten stehen drin.","content":{"application/json":{"schema":{"type":"object","properties":{"chats":{"type":"array","items":{"type":"object","properties":{"tenantSlug":{"type":"string","description":"Slug des Mandanten, aus dem der Chat stammt"},"tenantName":{"type":"string","description":"Anzeigename des Mandanten aus public.tenants"},"conversationId":{"type":"string","description":"Kennung des Gespraechs innerhalb des Mandanten"},"userId":{"type":"string","description":"Verfasser der letzten Nachricht; leer, wenn die Spalte NULL ist"},"lastMessage":{"type":"string","description":"Die letzte Nachricht, auf 200 Zeichen gekuerzt"},"role":{"type":"string","description":"Rolle des Verfassers der letzten Nachricht, Vorgabe \"user\""},"unread":{"type":"boolean","description":"true, wenn die letzte Nachricht vom Nutzer kam und noch nicht gelesen wurde"},"ageHours":{"type":"number","description":"Alter der letzten Nachricht in Stunden, auf eine Nachkommastelle gerundet; 0 bei unlesbarem Datum"},"createdAt":{"type":"string","description":"Zeitpunkt der letzten Nachricht als ISO-8601"}},"required":["tenantSlug","tenantName","conversationId","userId","lastMessage","role","unread","ageHours","createdAt"]},"description":"Ungelesene zuerst, dann die neuesten; auf `chatLimit` gekuerzt"},"unreadChats":{"type":"integer","description":"Anzahl ungelesener Chats VOR der Kuerzung auf `chatLimit`"},"tenantsScanned":{"type":"integer","description":"Anzahl der durchsuchten aktiven Mandanten"},"generatedAt":{"type":"string","description":"Zeitpunkt dieser Auskunft als ISO-8601"}},"required":["chats","unreadChats","tenantsScanned","generatedAt"]},"example":{"chats":[{"tenantSlug":"string","tenantName":"string","conversationId":"string","userId":"string","lastMessage":"string","role":"string","unread":true,"ageHours":0,"createdAt":"string"}],"unreadChats":0,"tenantsScanned":0,"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getAdminSupportInbox","tags":["admin"],"parameters":[],"description":"Cross-tenant Support-Postfach: die Chats, die Mandanten-Nutzer an uns schreiben. Bewusst OHNE die Tickets der Mandanten.","summary":"Cross-tenant Support-Postfach: die Chats, die Mandanten-Nutzer an uns schreiben","x-nemix-summary-source":"description:first-sentence"}},"/admin/server/metrics":{"get":{"responses":{"200":{"description":"Server metrics snapshot for ONE container — never fleet-wide, and every number resets when this process restarts. `cloudwatch.enabled` is false in this version; `cloudwatch.reason` names what is missing. The three `http` values are `null` when the prom-client registry holds no matching metric — `null` means „not measured\", a 0 would claim „no traffic\"; `p95LatencyMs` is never filled today. Nothing is cached: `generatedAt` is the moment of the request.","content":{"application/json":{"schema":{"type":"object","properties":{"process":{"type":"object","properties":{"uptimeSec":{"type":"number"},"pid":{"type":"number"},"nodeVersion":{"type":"string"},"rssMb":{"type":"number"},"heapUsedMb":{"type":"number"},"heapTotalMb":{"type":"number"},"externalMb":{"type":"number"},"heapUsedPct":{"type":"number"},"eventLoopLagMeanMs":{"type":"number"},"eventLoopLagP99Ms":{"type":"number"},"eventLoopLagMaxMs":{"type":"number"}},"required":["uptimeSec","pid","nodeVersion","rssMb","heapUsedMb","heapTotalMb","externalMb","heapUsedPct","eventLoopLagMeanMs","eventLoopLagP99Ms","eventLoopLagMaxMs"],"additionalProperties":false},"http":{"type":"object","properties":{"totalRequests":{"type":["number","null"]},"avgLatencyMs":{"type":["number","null"]},"p95LatencyMs":{"type":["number","null"]}},"required":["totalRequests","avgLatencyMs","p95LatencyMs"],"additionalProperties":false},"cloudwatch":{"type":"object","properties":{"enabled":{"type":"boolean"},"reason":{"type":"string"}},"required":["enabled","reason"],"additionalProperties":false},"generatedAt":{"type":"string"}},"required":["process","http","cloudwatch","generatedAt"],"additionalProperties":false},"example":{"process":{"uptimeSec":0,"pid":0,"nodeVersion":"string","rssMb":0,"heapUsedMb":0,"heapTotalMb":0,"externalMb":0,"heapUsedPct":0,"eventLoopLagMeanMs":0,"eventLoopLagP99Ms":0,"eventLoopLagMaxMs":0},"http":{"totalRequests":0,"avgLatencyMs":0,"p95LatencyMs":0},"cloudwatch":{"enabled":true,"reason":"string"},"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"server_metrics_unavailable"}},"operationId":"getAdminServerMetrics","tags":["admin"],"parameters":[],"summary":"Auslastung dieses API-Containers: Speicher, Laufzeit, HTTP","description":"In-process server load for this API container (uptime, memory, event-loop lag, HTTP summary)."}},"/admin/ai/kill":{"get":{"responses":{"200":{"description":"Alle gesetzten Schalter samt Kurzform fuer den globalen Not-Aus","content":{"application/json":{"schema":{"type":"object","properties":{"globalHalted":{"type":"boolean"},"data":{"type":"array","items":{"type":"object","properties":{"scope":{"type":"string"},"tenantId":{"type":"string"},"enabled":{"type":"boolean"},"reason":{"type":["string","null"]},"actor":{"type":["string","null"]},"updatedAt":{}},"required":["scope","tenantId","enabled","reason","actor"],"additionalProperties":false}}},"required":["globalHalted","data"],"additionalProperties":false},"example":{"globalHalted":true,"data":[{"scope":"string","tenantId":"string","enabled":true,"reason":"string","actor":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getAdminAiKill","tags":["admin","ai","kill-switch"],"parameters":[],"summary":"Liest den aktuellen Zustand des KI-Not-Aus (global + pro Mandant)","description":"Liefert ALLE Zeilen aus `public.ai_governance_switch` — den globalen Schalter und die je Mandant gesetzten, sortiert nach Bereich und Mandant. `globalHalted` ist die Kurzform: true, sobald der globale Schalter auf `enabled: false` steht. Der globale Schalter hat Vorrang vor jedem Mandanten-Schalter. Fehlt eine Zeile ganz, ist der betreffende Bereich NICHT gestoppt. Nur fuer Plattform-Administratoren erreichbar."},"post":{"responses":{"200":{"description":"Der geschriebene Schalter, so wie er jetzt in der Tabelle steht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"halted":{"type":"boolean"},"switch":{"type":"object","properties":{"scope":{"type":"string"},"tenantId":{"type":"string"},"enabled":{"type":"boolean"},"reason":{"type":["string","null"]},"actor":{"type":["string","null"]},"updatedAt":{}},"required":["scope","tenantId","enabled","reason","actor"],"additionalProperties":false}},"required":["ok","halted","switch"],"additionalProperties":false},"example":{"ok":true,"halted":true,"switch":{"scope":"string","tenantId":"string","enabled":true,"reason":"string","actor":"string"}}}}},"400":{"description":"Validierungsfehler — etwa `scope: \"tenant\"` ohne `tenantId`"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"},"503":{"description":"Datenbank nicht erreichbar — nichts geschaltet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postAdminAiKill","tags":["admin","ai","kill-switch"],"parameters":[],"summary":"Setzt den KI-Not-Aus (enabled=false ⇒ KI-Ausführung gestoppt)","description":"Schreibt einen Schalter fuer `scope: \"global\"` oder fuer einen einzelnen Mandanten (`scope: \"tenant\"` verlangt dann `tenantId`); eine vorhandene Zeile wird ueberschrieben, nicht ergaenzt. `enabled: false` haelt die KI-Ausfuehrung an, `true` gibt sie wieder frei. Der Zwischenspeicher DIESES Prozesses wird sofort verworfen, andere Prozesse ziehen innerhalb ihrer eigenen kurzen Haltezeit nach. Wer geschaltet hat, wird zusaetzlich im KI-Protokoll vermerkt; scheitert dieser Eintrag, aendert das am Schaltvorgang nichts. Nur fuer Plattform-Administratoren erreichbar.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"string","enum":["global","tenant"]},"tenantId":{"type":"string","minLength":1,"maxLength":200},"enabled":{"type":"boolean"},"reason":{"type":"string","maxLength":1000}},"required":["scope","enabled"]},"example":{"scope":"global","tenantId":"string","enabled":true,"reason":"string"}}}}}},"/api/admin/metrics":{"get":{"responses":{"200":{"description":"Metrics snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"tenants":{"type":"object","properties":{"total":{"type":"integer"},"active":{"type":"integer"},"newThisMonth":{"type":"integer"}},"required":["total","active","newThisMonth"]},"users":{"type":"object","properties":{"total":{"type":"integer"},"verifiedEmails":{"type":"integer"}},"required":["total","verifiedEmails"]},"revenue":{"type":"object","properties":{"currentMonth":{"type":"number"},"lastMonth":{"type":"number"}},"required":["currentMonth","lastMonth"]},"system":{"type":"object","properties":{"dbConnected":{"type":"boolean"},"apiVersion":{"type":"string"}},"required":["dbConnected","apiVersion"]}},"required":["tenants","users","revenue","system"]},"example":{"tenants":{"total":0,"active":0,"newThisMonth":0},"users":{"total":0,"verifiedEmails":0},"revenue":{"currentMonth":0,"lastMonth":0},"system":{"dbConnected":true,"apiVersion":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminMetrics","tags":["admin"],"parameters":[],"summary":"Kennzahlen fuers Admin-Dashboard: Mandanten, Nutzer, Umsatz","description":"Mischt zwei Bezugsgroeszen in EINER Antwort: Mandanten- und Nutzerzahlen gelten PLATTFORMWEIT, der Umsatz dagegen nur fuer den Mandanten des Aufrufers. Gezaehlt werden alle Mandanten, die aktiven und die im laufenden Kalendermonat angelegten, dazu alle Nutzer und die mit bestaetigter Adresse. Der Umsatz summiert die als bezahlt gekennzeichneten Rechnungen des laufenden und des Vormonats, nach Anlagedatum, nicht nach Zahlungsdatum. Der Endpunkt scheitert nie: ohne Datenbank oder bei einem Abfragefehler kommen NULLEN mit `system.dbConnected: false` — dieses Feld unterscheidet „nichts vorhanden\" von „nicht gemessen\"."}},"/api/admin/tenants/{id}/packs/{pack}":{"post":{"responses":{"200":{"description":"Paket installiert — Antwort nennt die Zahl angelegter Entitaeten/Felder"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant oder Paket nicht gefunden"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"postApiAdminTenantsByIdPacksByPack","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"pack","required":true}],"summary":"Aktiviert ein Branchenpaket fuer einen Mandanten","description":"Aktiviert ein Branchenpaket fuer einen Mandanten (legt dessen Entitaeten + Felder an). Verwalter-Weg zu POST /industry-packs/{slug}/install."}},"/api/admin/audit/verify":{"get":{"responses":{"200":{"description":"Verification result","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"brokenAt":{"type":["integer","null"]},"rows":{"type":"integer"},"mode":{"type":"string","const":"demo"},"error":{"type":"string"}},"required":["valid","brokenAt"]},"example":{"valid":true,"brokenAt":0,"rows":0,"mode":"demo","error":"string"}}}},"400":{"description":"No tenant context"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage des Audit-Logs fehlgeschlagen"},"503":{"description":"Verifier not ready"}},"operationId":"getApiAdminAuditVerify","tags":["admin"],"parameters":[],"summary":"Verify the HMAC hash-chain of the tenant audit_log","description":"Liest das Audit-Log des EIGENEN Mandanten vollstaendig — ohne Blaetterung, aeltester Eintrag zuerst — und rechnet die HMAC-Kette nach. `brokenAt` ist die null-basierte Position der ersten Zeile, deren gespeicherte Signatur nicht zur gerechneten passt; null heiszt „kein Bruch gefunden\". ACHTUNG bei der Deutung: verglichen wird nur, wo eine Zeile ueberhaupt eine Signatur traegt — Zeilen ohne Signatur werden durchgereicht, ein `valid: true` beweist also nicht, dass alle Eintraege signiert sind. Fehlt der Schluessel AUDIT_HMAC_KEY oder ist er kuerzer als 32 Byte, LEHNT der Endpunkt mit 503 ab, statt mit einem Ersatzschluessel ein falsches Gruen zu erzeugen. Ohne Datenbank kommt 200 mit mode=\"demo\" — dann wurde nichts geprueft. Rein lesend."}},"/api/admin/organizations":{"get":{"responses":{"200":{"description":"Organisationen — hoechstens 100, `total` zaehlt die gelieferten","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"name":{},"slug":{},"country":{},"legalForm":{},"vatId":{},"parentOrg":{},"plan":{},"billingMode":{},"activePacks":{},"aiQuotaMonthly":{},"stripeCustomerId":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminOrganizations","tags":["organizations"],"parameters":[],"description":"Listet Organisationen. ACHTUNG: entgegen dem Dateikopf filtert dieser Aufruf NICHT nach Rolle — er gibt alle Organisationen zurueck (hoechstens 100, optional per `search` eingegrenzt). Der Zugang ist allein dadurch begrenzt, dass die gesamte Admin-Anwendung `requireSuperAdmin` vorgeschaltet hat.","summary":"Listet Organisationen","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Organisation angelegt","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"slug":{},"country":{},"legalForm":{},"vatId":{},"parentOrg":{},"plan":{},"billingMode":{},"activePacks":{},"aiQuotaMonthly":{},"stripeCustomerId":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"409":{"description":"Kuerzel bereits vergeben (text/plain)"}},"operationId":"postApiAdminOrganizations","tags":["organizations"],"parameters":[],"description":"Legt eine Organisation an. `slug` ist der Schluessel und muss frei sein — ist er vergeben, bricht der Aufruf mit 409 ab, BEVOR etwas geschrieben wird; erlaubt sind 2 bis 63 Zeichen aus Kleinbuchstaben, Ziffern und Bindestrich. Ohne Angabe gelten `country: DE`, `plan: enterprise` und `billingMode: central`. Nur fuer `super_admin` oder `owner`, sonst 403. Die Antwort traegt die angelegte Organisation nackt, ohne Huelle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]*[a-z0-9]$","minLength":2,"maxLength":63},"country":{"type":"string","minLength":2,"maxLength":2,"default":"DE"},"legalForm":{"type":"string","maxLength":16},"vatId":{"type":"string","maxLength":32},"parentOrg":{"type":"string","format":"uuid"},"plan":{"type":"string","enum":["enterprise","professional","starter"],"default":"enterprise"},"billingMode":{"type":"string","enum":["central","decentral","hybrid"],"default":"central"},"aiQuotaMonthly":{"type":"integer","exclusiveMinimum":0}},"required":["name","slug"]},"example":{"name":"string","slug":"00000000-0000-4000-8000-000000000000","country":"st","legalForm":"string","vatId":"string","parentOrg":"00000000-0000-4000-8000-000000000000","plan":"enterprise","billingMode":"central","aiQuotaMonthly":1}}}},"summary":"Legt eine Organisation an","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/organizations/me/tenants":{"get":{"responses":{"200":{"description":"Die sichtbaren Mandanten — die Zeilen des Repositorys unveraendert, ohne Serialisierer. Der Umfang haengt an der Rolle des Aufrufers.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[],"total":0}}}},"401":{"description":"Nicht angemeldet (text/plain)"}},"operationId":"getApiAdminOrganizationsMeTenants","tags":["organizations"],"parameters":[],"summary":"Mandanten auflisten, die der angemeldete Nutzer sehen darf","description":"Die Reichweite haengt an der Rolle (Festlegung vom 10.09.2026): `super_admin` sieht ALLE Mandanten des Systems; wer in `organization_users` Eigentuemer oder Admin einer Organisation ist (Global Admin), sieht alle Mandanten dieser Organisation(en); alle anderen sehen die Mandanten, auf die sie ueber `organization_tenant_access` einen ausdruecklichen Zugriff haben. Ohne Anmeldung 401. Keine Blaetterung, keine Obergrenze. ACHTUNG: Sichtbarkeit ist nicht Erlaubnis — der Wechsel selbst prueft erneut (POST /:tenantIdOrSlug/switch), und fuer den Super Admin ist er dort noch nicht freigegeben."}},"/api/admin/organizations/{tenantIdOrSlug}/switch":{"post":{"responses":{"200":{"description":"Gewechselt. Nebenwirkung: der httpOnly-Cookie `nemix-active-tenant-slug` wird gesetzt (24 h) — DER entscheidet ab jetzt, welchen Mandanten nachfolgende Aufrufe sehen, nicht die Antwort hier.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"role":{}},"required":["ok","tenantId","tenantSlug"],"additionalProperties":false},"example":{"ok":true,"tenantId":"string","tenantSlug":"string"}}}},"400":{"description":"Kennung fehlt oder laenger als 80 Zeichen (text/plain)"},"401":{"description":"Nicht angemeldet (text/plain)"},"403":{"description":"Kein Zugriff auf diesen Mandanten (text/plain)"}},"operationId":"postApiAdminOrganizationsByTenantIdOrSlugSwitch","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantIdOrSlug","required":true}],"summary":"Wechselt den Mandanten-Kontext serverseitig","description":"Setzt den httpOnly-Cookie `nemix-active-tenant-slug` nach Pruefung. Fuer `super_admin` ist JEDER Mandant erlaubt (Festlegung vom 10.09.2026: „ersteinmal ohne genehmigung und protokoll\"); fuer alle anderen gilt weiterhin `organization_tenant_access`, sonst 403. Der Wechsel schreibt KEIN Protokoll — das wird gebaut, wenn es beauftragt ist."}},"/api/admin/organizations/{id}":{"get":{"responses":{"200":{"description":"Organisation — nackt, ohne Huelle","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"slug":{},"country":{},"legalForm":{},"vatId":{},"parentOrg":{},"plan":{},"billingMode":{},"activePacks":{},"aiQuotaMonthly":{},"stripeCustomerId":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Organisation nicht gefunden (text/plain)"}},"operationId":"getApiAdminOrganizationsById","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest eine Organisation ueber ihre ID. Der Aufruf ist an keine Rolle gebunden — es gilt allein die Wache der Admin-Anwendung davor. Gibt es die ID nicht, kommt 404 als text/plain, nicht als JSON.","summary":"Liest eine Organisation ueber ihre ID","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Geaendert — die vollstaendige Organisation nach der Aenderung","content":{"application/json":{"schema":{"type":"object","properties":{"id":{},"name":{},"slug":{},"country":{},"legalForm":{},"vatId":{},"parentOrg":{},"plan":{},"billingMode":{},"activePacks":{},"aiQuotaMonthly":{},"stripeCustomerId":{},"createdAt":{},"updatedAt":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"404":{"description":"Organisation nicht gefunden (text/plain)"}},"operationId":"patchApiAdminOrganizationsById","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Aendert einzelne Stammdaten einer Organisation: Name, Rechtsform, USt-IdNr., Tarif, Abrechnungsmodus, KI-Monatskontingent, Stripe-Kundennummer und den freien `settings`-Block. Alle Felder sind einzeln optional; was nicht im Rumpf steht, bleibt unveraendert. `slug` und `parentOrg` sind hier NICHT aenderbar. Nur fuer `super_admin`, sonst 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"legalForm":{"type":"string","maxLength":16},"vatId":{"type":"string","maxLength":32},"plan":{"type":"string","enum":["enterprise","professional","starter"]},"billingMode":{"type":"string","enum":["central","decentral","hybrid"]},"aiQuotaMonthly":{"type":"integer","exclusiveMinimum":0},"stripeCustomerId":{"type":"string"},"settings":{"type":"object","additionalProperties":{}}}},"example":{"name":"string","legalForm":"string","vatId":"string","plan":"enterprise","billingMode":"central","aiQuotaMonthly":1,"stripeCustomerId":"string","settings":{}}}}},"summary":"Aendert einzelne Stammdaten einer Organisation","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/organizations/{id}/tenants":{"get":{"responses":{"200":{"description":"Mandanten dieser Organisation — die Zeilen des Repositorys unveraendert, ohne Serialisierer und ohne Obergrenze","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminOrganizationsByIdTenants","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Listet die Mandanten, die an dieser Organisation haengen. Ohne Filter, ohne Blaetterung und ohne Obergrenze — bei einer grossen Organisation kommt alles auf einmal. Eine unbekannte Organisations-ID ergibt hier KEIN 404, sondern eine leere Liste.","summary":"Listet die Mandanten, die an dieser Organisation haengen","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/organizations/{id}/tenants/{tid}":{"post":{"responses":{"200":{"description":"Angehaengt. Eine Zeile wurde geaendert.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"organizationId":{"type":"string"},"tenantId":{"type":"string"}},"required":["ok","organizationId","tenantId"],"additionalProperties":false},"example":{"ok":true,"organizationId":"string","tenantId":"string"}}}},"400":{"description":"Rumpf ungueltig (`invalid_body`)"},"401":{"description":"Nicht angemeldet"},"403":{"description":"Keine super_admin-Rolle"},"404":{"description":"Die Mandanten-ID trifft keine Zeile"}},"operationId":"postApiAdminOrganizationsByIdTenantsByTid","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"tid","required":true}],"description":"Haengt einen bereits bestehenden Mandanten an eine Organisation. Organisation und Mandant stehen im Pfad. Der Rumpf ist optional, wird aber GEPRUEFT: `groupId` und `parentTenant` muessen UUIDs sein, `companyType` einer von parent, subsidiary, branch, standalone; ein unbekanntes Feld wird abgewiesen (400). Trifft die Mandanten-ID keine Zeile, antwortet die Route 404 statt 200. NUR fuer `super_admin`: Wer zu einer Organisation gehoert, ist die Reichweite des Global Admin selbst und darf nicht von ihm gesetzt werden.","summary":"Haengt einen bereits bestehenden Mandanten an eine Organisation","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/organizations/{id}/users":{"post":{"responses":{"201":{"description":"Nutzer angelegt, Mandanten-Zugriffe erteilt. `grantedTenants` zaehlt die angefragten — und stimmt, weil ein Fehler beim Erteilen den ganzen Aufruf abbrechen laesst. Ein Zwischenzustand „201, aber nur die Haelfte erteilt\" ist nicht moeglich; ein Abbruch NACH dem Anlegen des Nutzers schon (dann existiert der Nutzer ohne Zugriffe).","content":{"application/json":{"schema":{"type":"object","properties":{"organizationUserId":{},"grantedTenants":{"type":"number"}},"required":["grantedTenants"],"additionalProperties":false},"example":{"grantedTenants":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"409":{"description":"Die Aenderung liesse die Organisation ohne Eigentuemer (`last_owner`)"}},"operationId":"postApiAdminOrganizationsByIdUsers","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Nimmt einen bestehenden Nutzer in die Organisation auf — Rolle `owner`, `admin`, `member` (Vorgabe) oder `auditor`, dazu das Kennzeichen `isConsolidationUser`. Die Mitgliedschaft ist wiederholbar: ein zweiter Aufruf fuer denselben Nutzer legt nichts doppelt an, sondern schreibt Rolle und Kennzeichen neu. Optional erteilt `tenantAccess` in einem Zug Zugriff auf einzelne Mandanten; die Zugriffe werden nacheinander gesetzt, nicht in einer Transaktion. Nur fuer `super_admin`, `owner`, sonst 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","minLength":1},"role":{"type":"string","enum":["owner","admin","member","auditor"],"default":"member"},"isConsolidationUser":{"type":"boolean","default":false},"tenantAccess":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid"},"role":{"type":"string","default":"user"}},"required":["tenantId"]}}},"required":["userId"]},"example":{"userId":"string","role":"owner","isConsolidationUser":true,"tenantAccess":[{"tenantId":"00000000-0000-4000-8000-000000000000","role":"string"}]}}}},"summary":"Nimmt einen bestehenden Nutzer in die Organisation auf","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/organizations/{id}/groups":{"get":{"responses":{"200":{"description":"Untergruppen (Region/Sparte/Abteilung) — die Zeilen des Repositorys unveraendert, ohne Serialisierer","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminOrganizationsByIdGroups","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Listet die Untergruppen einer Organisation — Regionen, Sparten, Abteilungen oder eigene Zuschnitte, nach Name sortiert. Die Liste ist flach: eine Gruppe kann laut `parentGroup` unter einer anderen haengen, aufgebaut wird der Baum hier aber nicht. Ohne Filter und ohne Blaetterung.","summary":"Listet die Untergruppen einer Organisation","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Gruppe angelegt — die Antwort traegt NUR die neue ID, nicht die Gruppe. Wer Name oder Typ zurueckbraucht, muss die Liste neu lesen.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{}},"additionalProperties":false}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"}},"operationId":"postApiAdminOrganizationsByIdGroups","tags":["organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Legt eine Untergruppe in der Organisation an. `groupType` ist `region` (Vorgabe), `sector`, `department` oder `custom`; `parentGroup` haengt die neue Gruppe unter eine bestehende. Der Name wird nicht auf Eindeutigkeit geprueft — derselbe Name zweimal ergibt zwei Gruppen. Nur fuer `super_admin`, sonst 403.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"groupType":{"type":"string","enum":["region","sector","department","custom"],"default":"region"},"parentGroup":{"type":"string","format":"uuid"}},"required":["name"]},"example":{"name":"string","groupType":"region","parentGroup":"00000000-0000-4000-8000-000000000000"}}}},"summary":"Legt eine Untergruppe in der Organisation an","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/organizations/{orgId}/tree":{"get":{"responses":{"200":{"description":"Org tree","content":{"application/json":{"schema":{"type":"object","properties":{"orgId":{"type":"string","description":"Die Organisation aus dem Pfad, unveraendert zurueckgegeben"},"groups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"groupType":{"type":"string","enum":["region","sector","department","custom"]},"parentGroup":{"type":["string","null"],"description":"Uebergeordnete Gruppe; `null` auf oberster Ebene"}},"required":["id","name","groupType","parentGroup"]}},"memberships":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"groupId":{"type":"string"}},"required":["tenantId","groupId"]},"description":"Eine Zeile JE ZUGEHOERIGKEIT — ein Mandant kann mehrfach vorkommen"}},"required":["orgId","groups","memberships"]},"example":{"orgId":"string","groups":[{"id":"string","name":"string","groupType":"region","parentGroup":"string"}],"memberships":[{"tenantId":"string","groupId":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminOrganizationsByOrgIdTree","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true}],"summary":"Tree-view of an organization — groups plus tenant memberships","description":"Liest `public.tenant_groups` der Organisation (Kennung, Name, Gruppenart, uebergeordnete Gruppe) und dazu aus `public.tenant_membership` je Zugehoerigkeit EINE Zeile mit Mandanten- und Gruppenkennung — ein Mandant, der in mehreren Gruppen steht, erscheint entsprechend mehrfach. Die Verknuepfung der beiden Listen macht der Aufrufer.\n\nEs gibt weder Blaetterung noch Obergrenze noch Filter; die Antwort enthaelt immer den vollstaendigen Baum. Voraussetzung ist mindestens die Organisationsrolle `org_member` — fehlt sie, antwortet der Endpunkt mit 403 und `code: ORG_ROLE_REQUIRED`, ohne Anmeldung mit 401."}},"/api/admin/organizations/{orgId}/groups/{groupId}/members":{"post":{"responses":{"201":{"description":"Membership added","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiAdminOrganizationsByOrgIdGroupsByGroupIdMembers","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true},{"schema":{"type":"string"},"in":"path","name":"groupId","required":true}],"summary":"Attach a tenant to a tenant-group (M:N membership)","description":"Schreibt eine Zeile nach `public.tenant_membership` und verbindet damit den Mandanten aus dem Rumpf (`tenantId`) mit der Gruppe aus dem Pfad. Der Schreibvorgang ist `ON CONFLICT DO NOTHING`: ein zweiter Aufruf mit derselben Paarung aendert nichts und antwortet trotzdem mit 201. Die Antwort ist `{ ok: true }` und nennt die entstandene Mitgliedschaft nicht.\n\nGeprueft wird die Organisationsrolle `org_admin` auf `orgId` — fehlt sie, 403 mit `code: ORG_ROLE_REQUIRED`, ohne Anmeldung 401. Nicht geprueft wird, ob `groupId` ueberhaupt zu `orgId` gehoert; die Einfuegung nimmt Mandanten- und Gruppenkennung unveraendert.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid"}},"required":["tenantId"]},"example":{"tenantId":"00000000-0000-4000-8000-000000000000"}}}}}},"/api/admin/organizations/{tenantId}/resolve":{"get":{"responses":{"200":{"description":"Resolved","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"groupIds":{"type":"array","items":{"type":"string"},"description":"Alle Gruppen, in denen der Mandant steht"},"organization":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"parentOrg":{"type":["string","null"]}},"required":["id","name","slug","parentOrg"]},"ancestors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"parentOrg":{"type":["string","null"]}},"required":["id","name","slug","parentOrg"]},"description":"Die uebergeordneten Organisationen, oberste zuerst"},"role":{"type":["string","null"],"description":"Hoechste Organisationsrolle des Aufrufers; `null`, wenn keine hinterlegt ist"}},"required":["tenantId","groupIds","organization","ancestors","role"]},"example":{"tenantId":"string","groupIds":["string"],"organization":{"id":"string","name":"string","slug":"string","parentOrg":"string"},"ancestors":[{"id":"string","name":"string","slug":"string","parentOrg":"string"}],"role":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not in any org"}},"operationId":"getApiAdminOrganizationsByTenantIdResolve","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"summary":"Resolve a tenant to its org chain","description":"Laeuft von `public.tenant_membership` ueber `public.tenant_groups` zu `public.organizations` und von dort die `parent_org`-Kette nach oben (hoechstens 32 Stufen, Zyklen werden abgebrochen). Die Antwort nennt `tenantId`, alle `groupIds` des Mandanten, die unmittelbare `organization`, die `ancestors` mit der obersten zuerst und die Rolle des Aufrufers. Es wird nichts geschrieben.\n\nGehoert der Mandant zu keiner Organisation, ist die Antwort 404. Denselben 404 bekommt, wer die Organisationsrolle `org_member` nicht hat — der Fall wird bewusst nicht von „unbekannt\" unterschieden, damit sich fremde Zugehoerigkeiten nicht erraten lassen. Ohne Anmeldung 401."}},"/api/admin/organizations/{orgId}/shared/{entityType}":{"get":{"responses":{"200":{"description":"Records","content":{"application/json":{"schema":{"type":"object","properties":{"records":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"orgId":{"type":"string"},"entityType":{"type":"string","enum":["customer","product","supplier","contact"]},"externalId":{"type":["string","null"]},"payload":{"type":"object","additionalProperties":{},"description":"Die Nutzdaten des Satzes, unveraendert aus JSONB"},"orgShared":{"type":"boolean"}},"required":["id","orgId","entityType","externalId","payload","orgShared"]}}},"required":["records"]},"example":{"records":[{"id":"string","orgId":"string","entityType":"customer","externalId":"string","payload":{},"orgShared":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminOrganizationsByOrgIdSharedByEntityType","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true},{"schema":{"type":"string"},"in":"path","name":"entityType","required":true}],"summary":"List shared master-data records for an entity type within the org","description":"Liest `public.shared_master_data` fuer diese Organisation und die im Pfad genannte Art, neueste Aenderung zuerst. Zeilen mit `org_shared = false` bleiben aussen vor — wer eine Zeile privat gestellt hat, findet sie hier nicht mehr. Je Zeile kommen Kennung, `orgId`, `entityType`, `externalId`, `payload` und `orgShared`.\n\nEs gibt weder Blaetterung noch Obergrenze; alle Zeilen kommen in einer Antwort. Der Pfadwert `entityType` wird nicht gegen die vier erlaubten Arten geprueft (`customer`, `product`, `supplier`, `contact`) — ein anderer Wert liefert eine leere Liste statt eines Fehlers. Voraussetzung ist die Organisationsrolle `org_member`: fehlt sie, 403 mit `code: ORG_ROLE_REQUIRED`, ohne Anmeldung 401."}},"/api/admin/organizations/{orgId}/shared":{"post":{"responses":{"201":{"description":"Upserted","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"example":{"id":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiAdminOrganizationsByOrgIdShared","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true}],"summary":"Upsert a shared master-data record across the org","description":"Schreibt einen Satz nach `public.shared_master_data`. Der Schluessel ist das Tripel aus Organisation, `entityType` und `externalId`: gibt es ihn schon, werden `payload`, `orgShared` und der Aenderungszeitpunkt ueberschrieben — sonst entsteht eine neue Zeile. Beide Faelle antworten mit 201 und nur der Kennung (`{ id }`); ob angelegt oder ersetzt wurde, sagt die Antwort nicht. Ohne `externalId` gilt `null` als Schluesselteil, es kann also nur EINE solche Zeile je Art geben.\n\n`orgShared: false` nimmt den Satz aus der Verteilung an die Mandanten heraus; die Zeile bleibt erhalten, verschwindet aber aus der Liste und aus `materialise`.\n\nVerlangt wird die Organisationsrolle `org_admin`. Anders als bei den uebrigen Endpunkten dieses Routers wird die Ablehnung hier NICHT in ein 403 uebersetzt: fehlt die Rolle, antwortet der Endpunkt mit 500. Ohne Anmeldung 401.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entityType":{"type":"string","enum":["customer","product","supplier","contact"]},"externalId":{"type":["string","null"]},"payload":{"type":"object","additionalProperties":{}},"orgShared":{"type":"boolean","default":true}},"required":["entityType","payload"]},"example":{"entityType":"customer","externalId":"string","payload":{},"orgShared":true}}}}}},"/api/admin/organizations/{orgId}/shared/{entityType}/materialise":{"get":{"responses":{"200":{"description":"Materialised rows","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"recordId":{"type":"string"},"entityType":{"type":"string","enum":["customer","product","supplier","contact"]},"payload":{"type":"object","additionalProperties":{}}},"required":["tenantId","recordId","entityType","payload"]}}},"required":["rows"]},"example":{"rows":[{"tenantId":"string","recordId":"string","entityType":"customer","payload":{}}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminOrganizationsByOrgIdSharedByEntityTypeMaterialise","tags":["admin","organizations"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"orgId","required":true},{"schema":{"type":"string"},"in":"path","name":"entityType","required":true}],"summary":"Materialise shared master-data into per-tenant rows","description":"Rechnet die Verteilung nur AUS und gibt sie zurueck — es wird nichts in die Mandanten-Schemata geschrieben. Dazu werden die geteilten Saetze der Art (`org_shared = true`) und die Mandanten aller Gruppen der Organisation gelesen und ueber Kreuz gestellt: je Mandant und Satz eine Zeile mit `tenantId`, `recordId`, `entityType` und `payload`. Die Antwortlaenge ist damit Saetze mal Mandanten; Blaetterung oder Obergrenze gibt es nicht.\n\nDer Pfadwert `entityType` wird nicht gegen die vier erlaubten Arten geprueft — ein anderer Wert liefert eine leere Liste. Voraussetzung ist die Organisationsrolle `org_member`: fehlt sie, 403 mit `code: ORG_ROLE_REQUIRED`, ohne Anmeldung 401."}},"/api/admin/tenants/clone":{"post":{"responses":{"201":{"description":"Der Klon steht. `tablesCloned` und `rowsCopied` sagen, wie viel tatsaechlich kopiert wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"tenantId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"tenantNumber":{"type":["string","null"]},"tablesCloned":{"type":"integer"},"rowsCopied":{"type":"integer"}},"required":["ok","tenantId","slug","name","tenantNumber","tablesCloned","rowsCopied"]},"example":{"ok":true,"tenantId":"string","slug":"string","name":"string","tenantNumber":"string","tablesCloned":0,"rowsCopied":0}}}},"400":{"description":"Quelle nicht bestimmbar (`source_required`), Slug ungueltig (`invalid_slug`), oder der Rumpf haelt das Schema nicht ein (`newName` fehlt, `newSlug` passt nicht auf das Muster)."},"401":{"description":"Keine Benutzer-ID im Kontext (`{ \"error\": \"unauthorized\" }`)."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"404":{"description":"Quell-Mandant nicht gefunden (`source_missing`)."},"409":{"description":"Slug oder Ziel-Schema existiert bereits (`slug_taken`) — es wurde nichts angelegt und nichts zusammengefuehrt."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"500":{"description":"Klon fehlgeschlagen (`clone_failed`); die Transaktion ist zurueckgerollt, es bleibt kein halber Mandant zurueck."},"503":{"description":"Keine Datenbankverbindung (`db_unavailable`)."}},"operationId":"postApiAdminTenantsClone","tags":["admin","tenants"],"parameters":[],"summary":"Mandanten als vollstaendige Dublette klonen","description":"Legt einen NEUEN, eigenstaendigen Mandanten an, der eine vollstaendige Dublette eines bestehenden ist — Struktur UND Daten. Gedacht fuer Test-/Experimentiermandanten.\n\nWIE VIEL ANGELEGT WIRD — es ist kein Auszug, es ist alles:\n- eine neue Zeile in `public.tenants` (eigene Mandantennummer, Status `active`, Tarif/Pakete/Einstellungen/KI-Konfiguration/Branding/Steuernummer von der Quelle uebernommen)\n- ein neues Postgres-Schema `tenant_<newSlug>`\n- JEDE Basistabelle des Quell-Schemas, angelegt per `CREATE TABLE (LIKE … INCLUDING ALL)` (Spalten, Vorgaben, Pruefregeln, Indizes — KEINE Fremdschluessel)\n- JEDE ZEILE dieser Tabellen, kopiert per `INSERT … SELECT` ohne `WHERE` und ohne `LIMIT`\n\nES GIBT KEINE OBERGRENZE. Keine Zeilen-, Tabellen- oder Groessenschranke, keine Vorabpruefung, kein Trockenlauf. Ein Mandant mit Millionen Zeilen wird mit Millionen Zeilen kopiert; die Antwort nennt hinterher `tablesCloned` und `rowsCopied`. Alles laeuft in EINER Transaktion (DDL eingeschlossen) — bricht etwas ab, ist nichts angelegt, aber bis dahin haelt der Vorgang Sperren und Plattenplatz.\n\nNEBENWIRKUNGEN AUF DIE QUELLE UND AUF DEN AUFRUFER: der Handler sichert den Zugang ueber die Organisationsebene. Hat der Quell-Mandant keine Organisation, wird eine angelegt (`<Name> (Gruppe)`, Tarif `enterprise`). Der aufrufende Benutzer wird Mitglied (`owner`) und bekommt Zugriff auf den NEUEN UND den QUELL-Mandanten. Anschliessend wird `organization_id` bei beiden Mandanten gesetzt, falls sie noch leer war — der Quell-Mandant wird also mit veraendert.\n\nQUELLE: `sourceSlug` nennt den Quell-Mandanten. Fehlt er, greift der Handler auf den Mandanten der Sitzung zurueck — den setzt aber nur `tenantMiddleware`, und die haengt nicht an der admin-Sub-App. AN DIESEN PFADEN IST `sourceSlug` DAHER IN DER PRAXIS PFLICHT; ohne ihn kommt 400 `source_required`. An der Laufzeit nachgemessen (30.08.2026).\n\nZIEL-SLUG: `newSlug` ist optional. Ohne Angabe wird er aus `newName` abgeleitet und um `-test-<vier Zeichen>` ergaenzt (Zeitstempel zur Basis 36). Erlaubt ist `^[a-z][a-z0-9-]{1,40}$`.\n\nNICHT WIEDERHOLBAR, KEIN ZUSAMMENFUEHREN: existiert der Slug oder das Ziel-Schema schon, endet der Aufruf mit 409 — es wird nichts ergaenzt und nichts ueberschrieben.\n\nBEKANNTER KOMPROMISS: `LIKE INCLUDING DEFAULTS` uebernimmt SERIAL-Vorgaben als Verweis auf die SEQUENZ DER QUELLE. Der Klon zaehlt an dieser Stelle also mit der Quelle mit. Fuer Experimentiermandanten hingenommen, fuer einen produktiven Zwilling nicht geeignet.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sourceSlug":{"type":"string","minLength":2,"maxLength":63},"newName":{"type":"string","minLength":1,"maxLength":255},"newSlug":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"}},"required":["newName"]},"example":{"sourceSlug":"string","newName":"string"}}}}}},"/api/admin/tenants/stats":{"get":{"responses":{"200":{"description":"Zaehlung nach Status. Ohne Datenbank kommen ebenfalls 200 und lauter Nullen — eine 0 heisst hier also „keiner\" ODER „nicht messbar\".","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"number"},"active":{"type":"number"},"trial":{"type":"number"},"suspended":{"type":"number"},"cancelled":{"type":"number"}},"required":["total","active","trial","suspended"]},"example":{"total":0,"active":0,"trial":0,"suspended":0,"cancelled":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminTenantsStats","tags":["admin","tenants"],"parameters":[],"description":"Zaehlt die Mandanten nach Status. Eine einzige Abfrage auf `public.tenants` liefert total, active, trial, suspended und cancelled ueber COUNT(*) FILTER; Nutzer-, Tarif- oder Verbrauchsdaten werden dafuer nicht gelesen. Filter oder Blaetterung gibt es nicht. Der Endpunkt ist bewusst VOR `/:id` registriert, sonst laese der Router „stats\" als Mandanten-Id.","summary":"Zaehlt die Mandanten nach Status","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/tenants":{"get":{"responses":{"200":{"description":"Eine Seite der Mandantenliste. `total` zaehlt mit denselben Bedingungen wie die Liste. Ohne Datenbank kommt `{data: [], total: 0}` — ebenfalls mit 200 und ohne `page`/`limit`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"slug":{},"name":{},"tenantNumber":{},"status":{},"createdAt":{},"updatedAt":{},"plan":{},"planPrice":{},"userCount":{"type":"number"}},"required":["userCount"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"},"error":{"type":"string"}},"required":["data","total"]},"example":{"data":[{"userCount":0}],"total":0,"page":0,"limit":0,"error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — die Antwort traegt eine LEERE Liste UND den rohen Fehlertext im Feld `error`","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"slug":{},"name":{},"tenantNumber":{},"status":{},"createdAt":{},"updatedAt":{},"plan":{},"planPrice":{},"userCount":{"type":"number"}},"required":["userCount"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"},"error":{"type":"string"}},"required":["data","total"]}}}}},"operationId":"getApiAdminTenants","tags":["admin","tenants"],"parameters":[],"description":"Liest eine Seite aus `public.tenants`. Je Zeile kommen Tarifname und Monatspreis aus `plans` sowie die Anzahl der nicht geloeschten Nutzer aus `users` dazu. `page` beginnt bei 1, `limit` liegt zwischen 1 und 100 (Vorgabe 50); gefiltert wird ueber `status`, `plan` (Tarifname) und `search` (Teiltreffer in Name oder Slug). Sortiert nach Anlagedatum, neueste zuerst. `total` zaehlt mit denselben Bedingungen wie die Liste — die Zahl passt also zur Filterung.","summary":"Liest eine Seite aus `public.tenants`","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Mandant samt Verwalter-Konto angelegt. ACHTUNG: `tempPassword` steht im Klartext in dieser Antwort — einmalig, aber ungeschuetzt. Sie gehoert nicht in Protokolle oder Zwischenspeicher; das Passwort ist ueber einen sicheren Kanal weiterzugeben. Eine Willkommensmail geht nebenher raus, ihr Scheitern aendert die Antwort NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{},"userId":{},"slug":{"type":"string"},"companyName":{"type":"string"},"plan":{"type":"string"},"status":{"type":"string","const":"trial"},"loginUrl":{"type":"string"},"tempPassword":{"type":"string"}},"required":["slug","companyName","plan","status","loginUrl","tempPassword"],"additionalProperties":false},"example":{"slug":"string","companyName":"string","plan":"string","status":"trial","loginUrl":"string","tempPassword":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"E-Mail bereits vergeben (`duplicate_email`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Anlegen fehlgeschlagen (`create_failed`) — `message` traegt den Grund","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiAdminTenants","tags":["admin","tenants"],"parameters":[],"description":"Legt Mandant UND erstes Verwalter-Konto in einem Aufruf an. Der Slug entsteht aus dem Firmennamen und wird bei Kollision mit `-1`, `-2` … eindeutig gemacht (nach 99 Versuchen bricht der Aufruf ab). Der Mandant startet mit Status `trial` und dem Paket `core`; ist der genannte Tarif unbekannt, faellt die Zuordnung auf `trial` zurueck. Das Konto bekommt die Rolle `admin` und ein einmalig zurueckgegebenes Zufallspasswort — scheitert seine Anlage, wird die eben erzeugte Mandantenzeile wieder entfernt. Eine Willkommensmail geht nebenher raus; ihr Scheitern aendert das Ergebnis nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string","minLength":2,"maxLength":255},"firstName":{"type":"string","minLength":1,"maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email"},"plan":{"type":"string","enum":["starter","professional","enterprise"],"default":"starter"},"sitzland":{"type":"string","pattern":"^[A-Z]{2}$"},"profil":{"type":"object","properties":{"branchen":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":200,"default":[]},"groesse":{"type":"string","maxLength":32,"default":""},"laender":{"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"maxItems":200,"default":[]},"sprachen":{"type":"array","items":{"type":"string","minLength":2,"maxLength":8},"maxItems":200,"default":[]}}}},"required":["companyName","firstName","lastName","email","sitzland"]}}}},"summary":"Legt Mandant UND erstes Verwalter-Konto in einem Aufruf an","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/tenants/{id}":{"get":{"responses":{"200":{"description":"Ein Mandant — die ganze Tabellenzeile (`t.*`) plus Plan-Name, Preis, Nutzer- und Speichergrenze und die gezaehlten Nutzer. Welche Spalten `t.*` umfasst, bestimmt das Tabellenschema, nicht diese Route.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiAdminTenantsById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest einen Mandanten ueber seine Id. Zur vollstaendigen Tabellenzeile kommen Tarifname, Monatspreis, Nutzer- und Speichergrenze aus `plans` sowie die Zahl der nicht geloeschten Nutzer. Ein Zugriff ueber den Slug ist hier nicht vorgesehen. Eine unbekannte Id ergibt 404, nicht eine leere Zeile.","summary":"Liest einen Mandanten ueber seine Id","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Geaendert — die Antwort traegt NUR id, name und status zurueck, nicht den ganzen Mandanten. Ein unbekannter Planname wird still uebergangen: kam daneben ein anderes Feld, meldet der Aufruf 200, obwohl der Plan unveraendert blieb; kam nur der Plan, meldet er 400 `no_changes`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Kein aenderbares Feld im Rumpf (`no_changes`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Aenderung fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchApiAdminTenantsById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Namen, Tarif oder Status eines Mandanten aendern","description":"Aendert Name, Tarif und/oder Status eines Mandanten; nicht mitgeschickte Felder bleiben unberuehrt. Der Tarif wird ueber seinen Namen in `plans` aufgeloest — ein unbekannter Name wird still uebergangen und aendert nichts. Bleibt danach kein einziges Feld zum Schreiben uebrig, antwortet der Aufruf 400 `no_changes`. `updated_at` wird bei jeder echten Aenderung mitgezogen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":255},"plan":{"type":"string","enum":["starter","professional","enterprise","trial"]},"status":{"type":"string","enum":["active","trial","suspended","cancelled"]}}},"example":{"name":"string","plan":"starter","status":"active"}}}}},"delete":{"responses":{"200":{"description":"Geloescht. Die Antwort sagt, was wirklich fiel.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"schemaGeloescht":{"type":"string","description":"Name des verworfenen Mandanten-Schemas"},"organisationGeloescht":{"type":"boolean","description":"true, wenn die Gruppe leer zurueckblieb"},"kundennummer":{"type":["string","null"],"description":"Die sechsstellige Nummer der Prozessdatenbank, die die Oberflaeche noch wegraeumen muss. null, wenn der Mandant keine Nummer trug."}},"required":["ok","tenantId","slug","name","schemaGeloescht","organisationGeloescht","kundennummer"]},"example":{"ok":true,"tenantId":"string","slug":"string","name":"string","schemaGeloescht":"string","organisationGeloescht":true,"kundennummer":"string"}}}},"400":{"description":"Bestaetigung fehlt oder passt nicht zum Slug (`confirmation_mismatch`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"404":{"description":"Mandant nicht gefunden (`not_found`)"},"409":{"description":"Ein Fremdschluessel haelt den Mandanten (`in_use`) — `message` nennt die Einschraenkung. Es wurde NICHTS geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)"}},"operationId":"deleteApiAdminTenantsById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mandanten endgueltig loeschen, mit allen Daten","description":"Verwirft das Mandanten-Schema `tenant_<slug>` samt Inhalt und loescht die Zeile in `public.tenants`. Per ON DELETE CASCADE fallen mit: Benutzer (und daran Sitzungen und Konten), API-Schluessel, `organization_tenant_access` und Ebenen. NICHT MIT FALLEN `support_sessions` und `support_session_events`: das Protokoll haelt fest, wer vom Hersteller in diesen Mandanten gesehen hat, und ein Nachweis, der mit seinem Gegenstand verschwindet, ist keiner (Fremdschluessel geloest in 20260910163000_support_protokoll_ueberlebt_den_mandanten.sql). Die Organisation faellt NUR, wenn kein anderer Mandant mehr an ihr haengt. BEIDES IN EINER TRANSAKTION: entweder alles oder nichts. NICHT UMKEHRBAR — es gibt kein Wiederherstellen. Verlangt `?bestaetigung=<slug>`; stimmt sie nicht, 400 und es passiert nichts. Die Prozessdatenbank der Oberflaeche liegt im Dateisystem und faellt NICHT mit; die Antwort nennt ihre Nummer. Nur fuer `super_admin`, sonst 403."}},"/api/admin/tenants/{id}/suspend":{"post":{"responses":{"200":{"description":"Gesperrt (`status=suspended`). Die Antwort traegt nur id, name und status. Laufende Sitzungen des Mandanten beendet dieser Aufruf NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiAdminTenantsByIdSuspend","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt den Status des Mandanten auf `suspended` und zieht `updated_at` mit. Mehr passiert nicht: Daten, Tarif und Pakete bleiben unberuehrt, und bereits laufende Sitzungen des Mandanten beendet der Aufruf NICHT. Rueckgaengig ueber `POST /api/admin/tenants/{id}/activate`. Eine unbekannte Id ergibt 404.","summary":"Setzt den Status des Mandanten auf `suspended` und zieht `updated_at` mit","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/tenants/{id}/activate":{"post":{"responses":{"200":{"description":"Freigeschaltet (`status=active`). Die Antwort traegt nur id, name und status.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiAdminTenantsByIdActivate","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt den Status des Mandanten auf `active` und zieht `updated_at` mit — die Gegenrichtung zu `suspend`. Der vorherige Status wird nicht geprueft: der Aufruf wirkt auf einen gesperrten wie auf einen Test-Mandanten und laesst einen bereits aktiven unveraendert aktiv. Tarif, Pakete und Grenzen ruehrt er nicht an. Eine unbekannte Id ergibt 404.","summary":"Setzt den Status des Mandanten auf `active` und zieht `updated_at` mit","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/tenants/{id}/reset-password":{"post":{"responses":{"200":{"description":"ACHTUNG: es wurde KEINE E-Mail verschickt. Der Aufruf sucht bis zu zehn Nutzer des Mandanten, schreibt sie ins Serverprotokoll und meldet „Password-Reset-E-Mail wurde ausgeloest.\" — der Versand ueber Better Auth fehlt noch. `users` sind die Konten, die sie bekommen WUERDEN.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"versandGebaut":{"type":"boolean"},"users":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{}},"additionalProperties":false}}},"required":["message","versandGebaut","users"],"additionalProperties":false},"example":{"message":"string","versandGebaut":true,"users":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Der Mandant hat keine Nutzer (`no_users`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiAdminTenantsByIdReset-password","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Sucht bis zu zehn Nutzer des Mandanten und gibt sie zurueck. WICHTIG: es wird derzeit KEINE E-Mail verschickt — der Versand ueber Better Auth fehlt noch, der Aufruf schreibt die betroffenen Adressen nur ins Serverprotokoll. Auch die Einschraenkung auf Verwalter-Konten ist noch nicht wirksam: es kommen alle Nutzer zurueck, unabhaengig von der Rolle. Hat der Mandant gar keine Nutzer, antwortet der Aufruf 404 `no_users`.","summary":"Sucht bis zu zehn Nutzer des Mandanten und gibt sie zurueck","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/tenants/{id}/users":{"get":{"responses":{"200":{"description":"Alle Nutzer des Mandanten, neueste zuerst — ohne Obergrenze und ohne Blaettern. Geloeschte Konten sind NICHT ausgenommen (anders als in der Nutzerzaehlung der Liste, die `deleted_at IS NULL` fordert). Ohne Datenbank kommt `{data: []}` mit 200 und ohne `total`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{},"role":{},"emailVerified":{},"lastLoginAt":{},"createdAt":{}},"additionalProperties":false}},"total":{"type":"number"}},"required":["data"],"additionalProperties":false},"example":{"data":[{}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiAdminTenantsByIdUsers","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest alle Nutzer mit dieser `tenant_id`, neueste zuerst — ohne Obergrenze und ohne Blaettern. Je Konto kommen id, E-Mail, Name, Rolle, Bestaetigungsstatus, letzte Anmeldung und Anlagedatum; Passwortdaten nicht. Geloeschte Konten sind hier NICHT ausgenommen, anders als bei der Nutzerzaehlung der Mandantenliste, die `deleted_at IS NULL` fordert — die beiden Zahlen koennen deshalb auseinandergehen.","summary":"Liest alle Nutzer mit dieser `tenant_id`, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/tenants/{id}/usage":{"get":{"responses":{"200":{"description":"Umfang eines Mandanten. Von den vier Zahlen ist NUR `userCount` gemessen. `invoiceCount` steht seit dem 03.08.2026 fest auf 0 (die Rechnungszahl ist Geschaeftsvolumen und geht die Verwaltung nichts an), `storageUsedGb` und `apiCallsThisMonth` sind Platzhalter fuer eine Messwert-Tabelle, die es noch nicht gibt. Eine 0 heisst hier „wird nicht erhoben\", nicht „ist null\". ZWEITE FORM: fehlt die Datenbank, kommt ebenfalls 200 — dann aber mit `storage`/`apiCalls` statt `storageUsedGb`/`apiCallsThisMonth`. Wer nur die langen Namen liest, bekommt `undefined` und merkt den Ausfall nicht.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"tenantId":{"type":"string"},"slug":{"type":"string"},"userCount":{"type":"number"},"invoiceCount":{"type":"number"},"storageUsedGb":{"type":"number"},"apiCallsThisMonth":{"type":"number"}},"required":["tenantId","slug","userCount","invoiceCount","storageUsedGb","apiCallsThisMonth"],"additionalProperties":false},{"type":"object","properties":{"storage":{"type":"number"},"apiCalls":{"type":"number"},"invoiceCount":{"type":"number"}},"required":["storage","apiCalls","invoiceCount"],"additionalProperties":false}]},"example":{"tenantId":"string","slug":"string","userCount":0,"invoiceCount":0,"storageUsedGb":0,"apiCallsThisMonth":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiAdminTenantsByIdUsage","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Umfangs-Momentaufnahme eines Mandanten fuer die Verwaltungsuebersicht. GEMESSEN wird davon nur `userCount` (alle Konten mit dieser `tenant_id`); `storageUsedGb` und `apiCallsThisMonth` sind Platzhalter fuer eine Messwert-Tabelle, die es noch nicht gibt, und `invoiceCount` steht seit dem 03.08.2026 bewusst fest auf 0, weil die Rechnungszahl Geschaeftsvolumen ist. Eine 0 heisst hier also „wird nicht erhoben\", nicht „ist null\". Eine unbekannte Id ergibt 404.","summary":"Umfangs-Momentaufnahme eines Mandanten fuer die Verwaltungsuebersicht","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/tenants/{id}/support":{"get":{"responses":{"200":{"description":"Konfiguration und Umbauten des Mandanten. Die drei Listen sind gekappt (Felder 500, Umbauten und Bau-Doku je 200) und jede Quelle ist einzeln abgesichert: fehlt eine Tabelle, kommt SIE leer und der Rest trotzdem. Ob eine leere Liste „nichts angepasst\" oder „nicht lesbar\" bedeutet, sagen `customFieldsLesbar` / `umbautenLesbar` / `bauDokuLesbar`; der Grund steht in `quellenFehler`.","content":{"application/json":{"schema":{"type":"object","properties":{"mandant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"],"additionalProperties":false},"customFields":{"type":"array","items":{}},"umbauten":{"type":"array","items":{}},"bauDoku":{"type":"array","items":{}},"customFieldsLesbar":{"type":"boolean"},"umbautenLesbar":{"type":"boolean"},"bauDokuLesbar":{"type":"boolean"},"quellenFehler":{"type":"array","items":{"type":"object","properties":{"quelle":{"type":"string"},"meldung":{"type":"string"}},"required":["quelle","meldung"],"additionalProperties":false}},"generatedAt":{"type":"string"}},"required":["mandant","customFields","umbauten","bauDoku","customFieldsLesbar","umbautenLesbar","bauDokuLesbar","quellenFehler","generatedAt"],"additionalProperties":false},"example":{"mandant":{"id":"string","slug":"string","name":"string"},"customFields":[],"umbauten":[],"bauDoku":[],"customFieldsLesbar":true,"umbautenLesbar":true,"bauDokuLesbar":true,"quellenFehler":[{"quelle":"string","meldung":"string"}],"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`tenant_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`database_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiAdminTenantsByIdSupport","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Support-Ansicht eines Mandanten, ohne Geschaeftsdaten","description":"Support-Ansicht: eigene Felder, Umbauten, Bau-Doku eines Mandanten (ohne Geschaeftsdaten)"}},"/api/admin/tenants/{id}/umbauten/zurueckrollen":{"post":{"responses":{"200":{"description":"Zurueckgerollt — die erfassten Werte bleiben erhalten","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"zurueckgerollt":{"type":"object","properties":{"kind":{"type":"string","enum":["custom_field","custom_entity","validation_rule","ui_config","relation","action","workflow"]},"entity":{"type":"string"},"artifactId":{"type":"string"}},"required":["kind","entity","artifactId"]},"geistBeseitigt":{"type":"boolean"},"hinweis":{"type":"string"}},"required":["ok","zurueckgerollt","hinweis"]},"example":{"ok":true,"zurueckgerollt":{"kind":"custom_field","entity":"string","artifactId":"string"},"geistBeseitigt":true,"hinweis":"string"}}}},"400":{"description":"Ungueltige Eingabe, unerlaubte Entitaet, oder eine Art ohne Rueckweg — die Antwort traegt dann `grund` im Klartext"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant oder Umbau nicht gefunden"},"503":{"description":"Datenbank nicht verfuegbar — ODER der Umbau wurde entfernt, das Manifest liess sich aber nicht stilllegen (`manifest_nicht_stillgelegt`)"}},"operationId":"postApiAdminTenantsByIdUmbautenZurueckrollen","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Umbau eines Mandanten zurueckrollen (Support)","description":"Nimmt einen Umbau eines Mandanten zurueck. Einen echten Rueckweg haben eigenes Feld (custom_field) und eigenes Modul (custom_entity); die uebrigen Arten antworten mit 400 UND dem Grund, warum es fuer sie keinen gibt. Fehlt der Umbau schon, ist aber im Manifest noch aktiv („Geist\"), wird der Manifest-Eintrag stillgelegt statt 404 zu melden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["custom_field","custom_entity","validation_rule","ui_config","relation","action","workflow"]},"entity":{"type":"string","minLength":1,"maxLength":64},"artifactId":{"type":"string","minLength":1,"maxLength":128},"grund":{"type":"string","maxLength":500}},"required":["kind","entity","artifactId"]},"example":{"kind":"custom_field","entity":"string","artifactId":"string","grund":"string"}}}}}},"/api/admin/tenants/{id}/anwenderdoku":{"get":{"responses":{"200":{"description":"Nach Entitaet gruppierte Anwenderdoku — ausschliesslich Einrichtung, keine Geschaeftsdaten des Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"tenant":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"]},"abschnitte":{"type":"array","items":{"type":"object","properties":{"entitaet":{"type":["string","null"],"description":"null = uebergreifend, ohne Entitaet"},"titel":{"type":"string"},"felder":{"type":"array","items":{"type":"object","properties":{"fieldId":{"type":"string"},"label":{"type":"string","description":"Fehlt das Label, steht hier der humanisierte Feld-Slug"},"typ":{"type":"string","description":"Datentyp in deutschen Worten"},"pflicht":{"type":"boolean"},"inListe":{"type":"boolean","description":"Ob das Feld in der Listenansicht als Spalte erscheint"},"erklaerung":{"type":["string","null"],"description":"Wunsch des Anwenders aus der Bau-Doku, sonst der Hilfetext des Feldes"},"seitWann":{"type":["string","null"]},"version":{"type":["number","null"],"description":"Versionsstand aus dem Manifest; null = kein Eintrag"},"stand":{"type":["string","null"]}},"required":["fieldId","label","typ","pflicht","inListe","erklaerung","seitWann","version","stand"]}},"automatiken":{"type":"array","items":{"type":"object","properties":{"art":{"type":"string"},"beschreibung":{"type":"string"},"erklaerung":{"type":["string","null"]},"seitWann":{"type":["string","null"]},"urheber":{"type":["string","null"]}},"required":["art","beschreibung","erklaerung","seitWann","urheber"]}}},"required":["entitaet","titel","felder","automatiken"]},"description":"Nach deutschem Titel sortiert; das Uebergreifende steht am Ende"},"erzeugtAm":{"type":"string","format":"date-time"}},"required":["tenant","abschnitte","erzeugtAm"]},"example":{"tenant":{"id":"string","name":"string","slug":"string"},"abschnitte":[{"entitaet":"string","titel":"string","felder":[{"fieldId":"string","label":"string","typ":"string","pflicht":true,"inListe":true,"erklaerung":"string","seitWann":"string","version":0,"stand":"string"}],"automatiken":[{"art":"string","beschreibung":"string","erklaerung":"string","seitWann":"string","urheber":"string"}]}],"erzeugtAm":"2026-01-01T12:00:00.000Z"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Super-Administratoren"},"404":{"description":"Mandant nicht gefunden"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getApiAdminTenantsByIdAnwenderdoku","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Lesbare Anwenderdoku eines Mandanten: eigene Felder und Automatiken","description":"Lesbare Anwenderdoku eines Mandanten: eigene Felder je Entitaet mit Begruendung und Versionsstand plus Automatiken — ausschliesslich Einrichtung, keine Geschaeftsdaten."}},"/api/admin/tenants/{id}/gesundheit":{"get":{"responses":{"200":{"description":"Befundliste (leer = keine technischen Stolpersteine). War eine Quelle nicht lesbar, steht das als eigener Befund DRIN — die Liste ist dann nicht leer, und die uebrigen Pruefungen sagen nichts ueber diese eine aus.","content":{"application/json":{"schema":{"type":"object","properties":{"tenant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"}},"required":["id","slug"]},"befunde":{"type":"array","items":{"type":"object","properties":{"schwere":{"type":"string","description":"kritisch | warnung | info — in dieser Reihenfolge sortiert"},"titel":{"type":"string"},"erklaerung":{"type":"string","description":"Deutscher Klartext fuer den Support"},"quelle":{"type":"string","description":"Woher der Befund stammt, damit man nachsehen kann"}},"required":["schwere","titel","erklaerung","quelle"]},"description":"Leer = keine technischen Stolpersteine gefunden"},"geprueftAm":{"type":"string","description":"Zeitpunkt DIESER Pruefung — nichts wird zwischengespeichert"}},"required":["tenant","befunde","geprueftAm"]},"example":{"tenant":{"id":"string","slug":"string"},"befunde":[{"schwere":"string","titel":"string","erklaerung":"string","quelle":"string"}],"geprueftAm":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden — auch bei einer Id, die keine UUID ist"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getApiAdminTenantsByIdGesundheit","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Technische Stolpersteine eines Mandanten","description":"Technische Stolpersteine EINES Mandanten: fehlende Kerntabellen, KI-Kontingent, Einrichtungs-Aktivitaet, auseinanderlaufende Feld-Speicher (Registry vs. Manifest), Wissensbasis mit Platzhalter-Vektoren. Enthaelt keine Geschaeftsdaten des Mandanten."}},"/api/admin/customers/bulk/plan":{"post":{"responses":{"200":{"description":"Bulk update result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"},"targetPlan":{"type":"string","enum":["starter","professional","enterprise","trial"]}},"required":["updated","targetPlan"]},"example":{"updated":0,"targetPlan":"starter"}}}},"400":{"description":"Unknown plan name"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Bulk update failed"},"503":{"description":"Database unavailable"}},"operationId":"postApiAdminCustomersBulkPlan","tags":["admin","customers","bulk"],"parameters":[],"summary":"Bulk plan upgrade/downgrade for multiple tenants","description":"Repoints `tenants.plan_id` for 1…500 tenant ids in a single UPDATE and reports how many rows it actually hit — ids that match no tenant are silently skipped, so `updated` can be lower than the list sent. Unlike the single-tenant plan change this writes NO proration record. A `bulk.plan_change` entry goes to public.admin_audit_log best-effort. 400 when the plan name is unknown; no tenant is touched then.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"targetPlan":{"type":"string","enum":["starter","professional","enterprise","trial"]}},"required":["tenantIds","targetPlan"]},"example":{"tenantIds":["00000000-0000-4000-8000-000000000000"],"targetPlan":"starter"}}}}}},"/api/admin/customers/bulk/suspend":{"post":{"responses":{"200":{"description":"Bulk suspend result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}},"required":["updated"]},"example":{"updated":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Bulk suspend failed"},"503":{"description":"Database unavailable"}},"operationId":"postApiAdminCustomersBulkSuspend","tags":["admin","customers","bulk"],"parameters":[],"summary":"Bulk suspend tenants (login blocked)","description":"Sets `tenants.status` to \"suspended\" for 1…500 ids in one UPDATE — regardless of the previous status, so an already suspended tenant counts as updated too. `updated` reports the rows actually hit; unknown ids are skipped without an error. The optional `reason` is not stored on the tenant, it only travels into the best-effort `bulk.suspend` entry in public.admin_audit_log. Reversible via POST /bulk/activate.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"reason":{"type":"string","maxLength":500}},"required":["tenantIds"]},"example":{"tenantIds":["00000000-0000-4000-8000-000000000000"],"reason":"string"}}}}}},"/api/admin/customers/bulk/activate":{"post":{"responses":{"200":{"description":"Bulk activate result","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}},"required":["updated"]},"example":{"updated":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Bulk activate failed"},"503":{"description":"Database unavailable"}},"operationId":"postApiAdminCustomersBulkActivate","tags":["admin","customers","bulk"],"parameters":[],"summary":"Bulk reactivate suspended/trial tenants","description":"Sets `tenants.status` to \"active\" for 1…500 ids in one UPDATE. The previous status is not checked, so a trial tenant loses its trial marker here as well. `updated` reports the rows actually hit; unknown ids are skipped without an error. Writes a best-effort `bulk.activate` entry to public.admin_audit_log.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500}},"required":["tenantIds"]},"example":{"tenantIds":["00000000-0000-4000-8000-000000000000"]}}}}}},"/api/admin/customers/bulk/email":{"post":{"responses":{"200":{"description":"Empfaenger ermittelt — nichts verschickt","content":{"application/json":{"schema":{"type":"object","properties":{"versandBereit":{"type":"integer"},"versendet":{"type":"number","const":0},"hinweis":{"type":"string"},"recipients":{"type":"array","items":{"type":"string"}}},"required":["versandBereit","versendet","hinweis","recipients"]},"example":{"versandBereit":0,"versendet":0,"hinweis":"string","recipients":["string"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Empfaenger konnten nicht ermittelt werden"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"postApiAdminCustomersBulkEmail","tags":["admin","customers","bulk"],"parameters":[],"description":"Ermittelt zu jedem Mandanten die E-Mail des Haupt-Administrators: je Mandant genau ein Nutzer, Rolle \"admin\" zuerst, sonst der aelteste; geloeschte Nutzer bleiben auszen vor. Mandanten ohne passenden Nutzer fallen still heraus, `versandBereit` kann darum kleiner sein als die Zahl der gesendeten IDs. Es wird NICHTS verschickt: ein Versender ist nicht gebaut. `subject`, `body` und `fromName` werden geprueft, aber nur der Betreff landet im Audit-Eintrag `bulk.email` — der Rumpf wird nirgends abgelegt. Die Antwort nennt die Empfaengerzahl (versandBereit), versendet=0 und einen Hinweis.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"subject":{"type":"string","minLength":2,"maxLength":200},"body":{"type":"string","minLength":2,"maxLength":20000},"fromName":{"type":"string","maxLength":100}},"required":["tenantIds","subject","body"]},"example":{"tenantIds":["00000000-0000-4000-8000-000000000000"],"subject":"string","body":"string","fromName":"string"}}}},"summary":"Ermittelt zu jedem Mandanten die E-Mail des Haupt-Administrators","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/customers/clone":{"post":{"responses":{"201":{"description":"Der Klon steht. `tablesCloned` und `rowsCopied` sagen, wie viel tatsaechlich kopiert wurde.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"tenantId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"tenantNumber":{"type":["string","null"]},"tablesCloned":{"type":"integer"},"rowsCopied":{"type":"integer"}},"required":["ok","tenantId","slug","name","tenantNumber","tablesCloned","rowsCopied"]},"example":{"ok":true,"tenantId":"string","slug":"string","name":"string","tenantNumber":"string","tablesCloned":0,"rowsCopied":0}}}},"400":{"description":"Quelle nicht bestimmbar (`source_required`), Slug ungueltig (`invalid_slug`), oder der Rumpf haelt das Schema nicht ein (`newName` fehlt, `newSlug` passt nicht auf das Muster)."},"401":{"description":"Keine Benutzer-ID im Kontext (`{ \"error\": \"unauthorized\" }`)."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"404":{"description":"Quell-Mandant nicht gefunden (`source_missing`)."},"409":{"description":"Slug oder Ziel-Schema existiert bereits (`slug_taken`) — es wurde nichts angelegt und nichts zusammengefuehrt."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"500":{"description":"Klon fehlgeschlagen (`clone_failed`); die Transaktion ist zurueckgerollt, es bleibt kein halber Mandant zurueck."},"503":{"description":"Keine Datenbankverbindung (`db_unavailable`)."}},"operationId":"postApiAdminCustomersClone","tags":["admin","tenants"],"parameters":[],"summary":"Mandanten als vollstaendige Dublette klonen","description":"Legt einen NEUEN, eigenstaendigen Mandanten an, der eine vollstaendige Dublette eines bestehenden ist — Struktur UND Daten. Gedacht fuer Test-/Experimentiermandanten.\n\nWIE VIEL ANGELEGT WIRD — es ist kein Auszug, es ist alles:\n- eine neue Zeile in `public.tenants` (eigene Mandantennummer, Status `active`, Tarif/Pakete/Einstellungen/KI-Konfiguration/Branding/Steuernummer von der Quelle uebernommen)\n- ein neues Postgres-Schema `tenant_<newSlug>`\n- JEDE Basistabelle des Quell-Schemas, angelegt per `CREATE TABLE (LIKE … INCLUDING ALL)` (Spalten, Vorgaben, Pruefregeln, Indizes — KEINE Fremdschluessel)\n- JEDE ZEILE dieser Tabellen, kopiert per `INSERT … SELECT` ohne `WHERE` und ohne `LIMIT`\n\nES GIBT KEINE OBERGRENZE. Keine Zeilen-, Tabellen- oder Groessenschranke, keine Vorabpruefung, kein Trockenlauf. Ein Mandant mit Millionen Zeilen wird mit Millionen Zeilen kopiert; die Antwort nennt hinterher `tablesCloned` und `rowsCopied`. Alles laeuft in EINER Transaktion (DDL eingeschlossen) — bricht etwas ab, ist nichts angelegt, aber bis dahin haelt der Vorgang Sperren und Plattenplatz.\n\nNEBENWIRKUNGEN AUF DIE QUELLE UND AUF DEN AUFRUFER: der Handler sichert den Zugang ueber die Organisationsebene. Hat der Quell-Mandant keine Organisation, wird eine angelegt (`<Name> (Gruppe)`, Tarif `enterprise`). Der aufrufende Benutzer wird Mitglied (`owner`) und bekommt Zugriff auf den NEUEN UND den QUELL-Mandanten. Anschliessend wird `organization_id` bei beiden Mandanten gesetzt, falls sie noch leer war — der Quell-Mandant wird also mit veraendert.\n\nQUELLE: `sourceSlug` nennt den Quell-Mandanten. Fehlt er, greift der Handler auf den Mandanten der Sitzung zurueck — den setzt aber nur `tenantMiddleware`, und die haengt nicht an der admin-Sub-App. AN DIESEN PFADEN IST `sourceSlug` DAHER IN DER PRAXIS PFLICHT; ohne ihn kommt 400 `source_required`. An der Laufzeit nachgemessen (30.08.2026).\n\nZIEL-SLUG: `newSlug` ist optional. Ohne Angabe wird er aus `newName` abgeleitet und um `-test-<vier Zeichen>` ergaenzt (Zeitstempel zur Basis 36). Erlaubt ist `^[a-z][a-z0-9-]{1,40}$`.\n\nNICHT WIEDERHOLBAR, KEIN ZUSAMMENFUEHREN: existiert der Slug oder das Ziel-Schema schon, endet der Aufruf mit 409 — es wird nichts ergaenzt und nichts ueberschrieben.\n\nBEKANNTER KOMPROMISS: `LIKE INCLUDING DEFAULTS` uebernimmt SERIAL-Vorgaben als Verweis auf die SEQUENZ DER QUELLE. Der Klon zaehlt an dieser Stelle also mit der Quelle mit. Fuer Experimentiermandanten hingenommen, fuer einen produktiven Zwilling nicht geeignet.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sourceSlug":{"type":"string","minLength":2,"maxLength":63},"newName":{"type":"string","minLength":1,"maxLength":255},"newSlug":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"}},"required":["newName"]},"example":{"sourceSlug":"string","newName":"string"}}}}}},"/api/admin/customers/stats":{"get":{"responses":{"200":{"description":"Zaehlung nach Status. Ohne Datenbank kommen ebenfalls 200 und lauter Nullen — eine 0 heisst hier also „keiner\" ODER „nicht messbar\".","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"number"},"active":{"type":"number"},"trial":{"type":"number"},"suspended":{"type":"number"},"cancelled":{"type":"number"}},"required":["total","active","trial","suspended"]},"example":{"total":0,"active":0,"trial":0,"suspended":0,"cancelled":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminCustomersStats","tags":["admin","tenants"],"parameters":[],"description":"Zaehlt die Mandanten nach Status. Eine einzige Abfrage auf `public.tenants` liefert total, active, trial, suspended und cancelled ueber COUNT(*) FILTER; Nutzer-, Tarif- oder Verbrauchsdaten werden dafuer nicht gelesen. Filter oder Blaetterung gibt es nicht. Der Endpunkt ist bewusst VOR `/:id` registriert, sonst laese der Router „stats\" als Mandanten-Id.","summary":"Zaehlt die Mandanten nach Status","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/customers":{"get":{"responses":{"200":{"description":"Eine Seite der Mandantenliste. `total` zaehlt mit denselben Bedingungen wie die Liste. Ohne Datenbank kommt `{data: [], total: 0}` — ebenfalls mit 200 und ohne `page`/`limit`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"slug":{},"name":{},"tenantNumber":{},"status":{},"createdAt":{},"updatedAt":{},"plan":{},"planPrice":{},"userCount":{"type":"number"}},"required":["userCount"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"},"error":{"type":"string"}},"required":["data","total"]},"example":{"data":[{"userCount":0}],"total":0,"page":0,"limit":0,"error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — die Antwort traegt eine LEERE Liste UND den rohen Fehlertext im Feld `error`","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"slug":{},"name":{},"tenantNumber":{},"status":{},"createdAt":{},"updatedAt":{},"plan":{},"planPrice":{},"userCount":{"type":"number"}},"required":["userCount"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"},"error":{"type":"string"}},"required":["data","total"]}}}}},"operationId":"getApiAdminCustomers","tags":["admin","tenants"],"parameters":[],"description":"Liest eine Seite aus `public.tenants`. Je Zeile kommen Tarifname und Monatspreis aus `plans` sowie die Anzahl der nicht geloeschten Nutzer aus `users` dazu. `page` beginnt bei 1, `limit` liegt zwischen 1 und 100 (Vorgabe 50); gefiltert wird ueber `status`, `plan` (Tarifname) und `search` (Teiltreffer in Name oder Slug). Sortiert nach Anlagedatum, neueste zuerst. `total` zaehlt mit denselben Bedingungen wie die Liste — die Zahl passt also zur Filterung.","summary":"Liest eine Seite aus `public.tenants`","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Mandant samt Verwalter-Konto angelegt. ACHTUNG: `tempPassword` steht im Klartext in dieser Antwort — einmalig, aber ungeschuetzt. Sie gehoert nicht in Protokolle oder Zwischenspeicher; das Passwort ist ueber einen sicheren Kanal weiterzugeben. Eine Willkommensmail geht nebenher raus, ihr Scheitern aendert die Antwort NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{},"userId":{},"slug":{"type":"string"},"companyName":{"type":"string"},"plan":{"type":"string"},"status":{"type":"string","const":"trial"},"loginUrl":{"type":"string"},"tempPassword":{"type":"string"}},"required":["slug","companyName","plan","status","loginUrl","tempPassword"],"additionalProperties":false},"example":{"slug":"string","companyName":"string","plan":"string","status":"trial","loginUrl":"string","tempPassword":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"409":{"description":"E-Mail bereits vergeben (`duplicate_email`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Anlegen fehlgeschlagen (`create_failed`) — `message` traegt den Grund","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiAdminCustomers","tags":["admin","tenants"],"parameters":[],"description":"Legt Mandant UND erstes Verwalter-Konto in einem Aufruf an. Der Slug entsteht aus dem Firmennamen und wird bei Kollision mit `-1`, `-2` … eindeutig gemacht (nach 99 Versuchen bricht der Aufruf ab). Der Mandant startet mit Status `trial` und dem Paket `core`; ist der genannte Tarif unbekannt, faellt die Zuordnung auf `trial` zurueck. Das Konto bekommt die Rolle `admin` und ein einmalig zurueckgegebenes Zufallspasswort — scheitert seine Anlage, wird die eben erzeugte Mandantenzeile wieder entfernt. Eine Willkommensmail geht nebenher raus; ihr Scheitern aendert das Ergebnis nicht.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string","minLength":2,"maxLength":255},"firstName":{"type":"string","minLength":1,"maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email"},"plan":{"type":"string","enum":["starter","professional","enterprise"],"default":"starter"},"sitzland":{"type":"string","pattern":"^[A-Z]{2}$"},"profil":{"type":"object","properties":{"branchen":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":200,"default":[]},"groesse":{"type":"string","maxLength":32,"default":""},"laender":{"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"maxItems":200,"default":[]},"sprachen":{"type":"array","items":{"type":"string","minLength":2,"maxLength":8},"maxItems":200,"default":[]}}}},"required":["companyName","firstName","lastName","email","sitzland"]}}}},"summary":"Legt Mandant UND erstes Verwalter-Konto in einem Aufruf an","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/customers/{id}":{"get":{"responses":{"200":{"description":"Ein Mandant — die ganze Tabellenzeile (`t.*`) plus Plan-Name, Preis, Nutzer- und Speichergrenze und die gezaehlten Nutzer. Welche Spalten `t.*` umfasst, bestimmt das Tabellenschema, nicht diese Route.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiAdminCustomersById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest einen Mandanten ueber seine Id. Zur vollstaendigen Tabellenzeile kommen Tarifname, Monatspreis, Nutzer- und Speichergrenze aus `plans` sowie die Zahl der nicht geloeschten Nutzer. Ein Zugriff ueber den Slug ist hier nicht vorgesehen. Eine unbekannte Id ergibt 404, nicht eine leere Zeile.","summary":"Liest einen Mandanten ueber seine Id","x-nemix-summary-source":"description:first-sentence"},"patch":{"responses":{"200":{"description":"Geaendert — die Antwort traegt NUR id, name und status zurueck, nicht den ganzen Mandanten. Ein unbekannter Planname wird still uebergangen: kam daneben ein anderes Feld, meldet der Aufruf 200, obwohl der Plan unveraendert blieb; kam nur der Plan, meldet er 400 `no_changes`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"400":{"description":"Kein aenderbares Feld im Rumpf (`no_changes`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Aenderung fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"patchApiAdminCustomersById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Namen, Tarif oder Status eines Mandanten aendern","description":"Aendert Name, Tarif und/oder Status eines Mandanten; nicht mitgeschickte Felder bleiben unberuehrt. Der Tarif wird ueber seinen Namen in `plans` aufgeloest — ein unbekannter Name wird still uebergangen und aendert nichts. Bleibt danach kein einziges Feld zum Schreiben uebrig, antwortet der Aufruf 400 `no_changes`. `updated_at` wird bei jeder echten Aenderung mitgezogen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":255},"plan":{"type":"string","enum":["starter","professional","enterprise","trial"]},"status":{"type":"string","enum":["active","trial","suspended","cancelled"]}}},"example":{"name":"string","plan":"starter","status":"active"}}}}},"delete":{"responses":{"200":{"description":"Geloescht. Die Antwort sagt, was wirklich fiel.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"schemaGeloescht":{"type":"string","description":"Name des verworfenen Mandanten-Schemas"},"organisationGeloescht":{"type":"boolean","description":"true, wenn die Gruppe leer zurueckblieb"},"kundennummer":{"type":["string","null"],"description":"Die sechsstellige Nummer der Prozessdatenbank, die die Oberflaeche noch wegraeumen muss. null, wenn der Mandant keine Nummer trug."}},"required":["ok","tenantId","slug","name","schemaGeloescht","organisationGeloescht","kundennummer"]},"example":{"ok":true,"tenantId":"string","slug":"string","name":"string","schemaGeloescht":"string","organisationGeloescht":true,"kundennummer":"string"}}}},"400":{"description":"Bestaetigung fehlt oder passt nicht zum Slug (`confirmation_mismatch`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Keine super_admin-Rolle"},"404":{"description":"Mandant nicht gefunden (`not_found`)"},"409":{"description":"Ein Fremdschluessel haelt den Mandanten (`in_use`) — `message` nennt die Einschraenkung. Es wurde NICHTS geloescht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)"}},"operationId":"deleteApiAdminCustomersById","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Mandanten endgueltig loeschen, mit allen Daten","description":"Verwirft das Mandanten-Schema `tenant_<slug>` samt Inhalt und loescht die Zeile in `public.tenants`. Per ON DELETE CASCADE fallen mit: Benutzer (und daran Sitzungen und Konten), API-Schluessel, `organization_tenant_access` und Ebenen. NICHT MIT FALLEN `support_sessions` und `support_session_events`: das Protokoll haelt fest, wer vom Hersteller in diesen Mandanten gesehen hat, und ein Nachweis, der mit seinem Gegenstand verschwindet, ist keiner (Fremdschluessel geloest in 20260910163000_support_protokoll_ueberlebt_den_mandanten.sql). Die Organisation faellt NUR, wenn kein anderer Mandant mehr an ihr haengt. BEIDES IN EINER TRANSAKTION: entweder alles oder nichts. NICHT UMKEHRBAR — es gibt kein Wiederherstellen. Verlangt `?bestaetigung=<slug>`; stimmt sie nicht, 400 und es passiert nichts. Die Prozessdatenbank der Oberflaeche liegt im Dateisystem und faellt NICHT mit; die Antwort nennt ihre Nummer. Nur fuer `super_admin`, sonst 403."}},"/api/admin/customers/{id}/suspend":{"post":{"responses":{"200":{"description":"Gesperrt (`status=suspended`). Die Antwort traegt nur id, name und status. Laufende Sitzungen des Mandanten beendet dieser Aufruf NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiAdminCustomersByIdSuspend","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt den Status des Mandanten auf `suspended` und zieht `updated_at` mit. Mehr passiert nicht: Daten, Tarif und Pakete bleiben unberuehrt, und bereits laufende Sitzungen des Mandanten beendet der Aufruf NICHT. Rueckgaengig ueber `POST /api/admin/tenants/{id}/activate`. Eine unbekannte Id ergibt 404.","summary":"Setzt den Status des Mandanten auf `suspended` und zieht `updated_at` mit","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/customers/{id}/activate":{"post":{"responses":{"200":{"description":"Freigeschaltet (`status=active`). Die Antwort traegt nur id, name und status.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{},"name":{},"status":{}},"additionalProperties":false}},"required":["data"],"additionalProperties":false},"example":{"data":{}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiAdminCustomersByIdActivate","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Setzt den Status des Mandanten auf `active` und zieht `updated_at` mit — die Gegenrichtung zu `suspend`. Der vorherige Status wird nicht geprueft: der Aufruf wirkt auf einen gesperrten wie auf einen Test-Mandanten und laesst einen bereits aktiven unveraendert aktiv. Tarif, Pakete und Grenzen ruehrt er nicht an. Eine unbekannte Id ergibt 404.","summary":"Setzt den Status des Mandanten auf `active` und zieht `updated_at` mit","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/customers/{id}/reset-password":{"post":{"responses":{"200":{"description":"ACHTUNG: es wurde KEINE E-Mail verschickt. Der Aufruf sucht bis zu zehn Nutzer des Mandanten, schreibt sie ins Serverprotokoll und meldet „Password-Reset-E-Mail wurde ausgeloest.\" — der Versand ueber Better Auth fehlt noch. `users` sind die Konten, die sie bekommen WUERDEN.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"versandGebaut":{"type":"boolean"},"users":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{}},"additionalProperties":false}}},"required":["message","versandGebaut","users"],"additionalProperties":false},"example":{"message":"string","versandGebaut":true,"users":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Der Mandant hat keine Nutzer (`no_users`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`db_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"postApiAdminCustomersByIdReset-password","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Sucht bis zu zehn Nutzer des Mandanten und gibt sie zurueck. WICHTIG: es wird derzeit KEINE E-Mail verschickt — der Versand ueber Better Auth fehlt noch, der Aufruf schreibt die betroffenen Adressen nur ins Serverprotokoll. Auch die Einschraenkung auf Verwalter-Konten ist noch nicht wirksam: es kommen alle Nutzer zurueck, unabhaengig von der Rolle. Hat der Mandant gar keine Nutzer, antwortet der Aufruf 404 `no_users`.","summary":"Sucht bis zu zehn Nutzer des Mandanten und gibt sie zurueck","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/customers/{id}/users":{"get":{"responses":{"200":{"description":"Alle Nutzer des Mandanten, neueste zuerst — ohne Obergrenze und ohne Blaettern. Geloeschte Konten sind NICHT ausgenommen (anders als in der Nutzerzaehlung der Liste, die `deleted_at IS NULL` fordert). Ohne Datenbank kommt `{data: []}` mit 200 und ohne `total`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{},"role":{},"emailVerified":{},"lastLoginAt":{},"createdAt":{}},"additionalProperties":false}},"total":{"type":"number"}},"required":["data"],"additionalProperties":false},"example":{"data":[{}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiAdminCustomersByIdUsers","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Liest alle Nutzer mit dieser `tenant_id`, neueste zuerst — ohne Obergrenze und ohne Blaettern. Je Konto kommen id, E-Mail, Name, Rolle, Bestaetigungsstatus, letzte Anmeldung und Anlagedatum; Passwortdaten nicht. Geloeschte Konten sind hier NICHT ausgenommen, anders als bei der Nutzerzaehlung der Mandantenliste, die `deleted_at IS NULL` fordert — die beiden Zahlen koennen deshalb auseinandergehen.","summary":"Liest alle Nutzer mit dieser `tenant_id`, neueste zuerst","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/customers/{id}/usage":{"get":{"responses":{"200":{"description":"Umfang eines Mandanten. Von den vier Zahlen ist NUR `userCount` gemessen. `invoiceCount` steht seit dem 03.08.2026 fest auf 0 (die Rechnungszahl ist Geschaeftsvolumen und geht die Verwaltung nichts an), `storageUsedGb` und `apiCallsThisMonth` sind Platzhalter fuer eine Messwert-Tabelle, die es noch nicht gibt. Eine 0 heisst hier „wird nicht erhoben\", nicht „ist null\". ZWEITE FORM: fehlt die Datenbank, kommt ebenfalls 200 — dann aber mit `storage`/`apiCalls` statt `storageUsedGb`/`apiCallsThisMonth`. Wer nur die langen Namen liest, bekommt `undefined` und merkt den Ausfall nicht.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"tenantId":{"type":"string"},"slug":{"type":"string"},"userCount":{"type":"number"},"invoiceCount":{"type":"number"},"storageUsedGb":{"type":"number"},"apiCallsThisMonth":{"type":"number"}},"required":["tenantId","slug","userCount","invoiceCount","storageUsedGb","apiCallsThisMonth"],"additionalProperties":false},{"type":"object","properties":{"storage":{"type":"number"},"apiCalls":{"type":"number"},"invoiceCount":{"type":"number"}},"required":["storage","apiCalls","invoiceCount"],"additionalProperties":false}]},"example":{"tenantId":"string","slug":"string","userCount":0,"invoiceCount":0,"storageUsedGb":0,"apiCallsThisMonth":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"500":{"description":"Abfrage fehlgeschlagen — `error` traegt den rohen Fehlertext","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiAdminCustomersByIdUsage","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"description":"Umfangs-Momentaufnahme eines Mandanten fuer die Verwaltungsuebersicht. GEMESSEN wird davon nur `userCount` (alle Konten mit dieser `tenant_id`); `storageUsedGb` und `apiCallsThisMonth` sind Platzhalter fuer eine Messwert-Tabelle, die es noch nicht gibt, und `invoiceCount` steht seit dem 03.08.2026 bewusst fest auf 0, weil die Rechnungszahl Geschaeftsvolumen ist. Eine 0 heisst hier also „wird nicht erhoben\", nicht „ist null\". Eine unbekannte Id ergibt 404.","summary":"Umfangs-Momentaufnahme eines Mandanten fuer die Verwaltungsuebersicht","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/customers/{id}/support":{"get":{"responses":{"200":{"description":"Konfiguration und Umbauten des Mandanten. Die drei Listen sind gekappt (Felder 500, Umbauten und Bau-Doku je 200) und jede Quelle ist einzeln abgesichert: fehlt eine Tabelle, kommt SIE leer und der Rest trotzdem. Ob eine leere Liste „nichts angepasst\" oder „nicht lesbar\" bedeutet, sagen `customFieldsLesbar` / `umbautenLesbar` / `bauDokuLesbar`; der Grund steht in `quellenFehler`.","content":{"application/json":{"schema":{"type":"object","properties":{"mandant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"],"additionalProperties":false},"customFields":{"type":"array","items":{}},"umbauten":{"type":"array","items":{}},"bauDoku":{"type":"array","items":{}},"customFieldsLesbar":{"type":"boolean"},"umbautenLesbar":{"type":"boolean"},"bauDokuLesbar":{"type":"boolean"},"quellenFehler":{"type":"array","items":{"type":"object","properties":{"quelle":{"type":"string"},"meldung":{"type":"string"}},"required":["quelle","meldung"],"additionalProperties":false}},"generatedAt":{"type":"string"}},"required":["mandant","customFields","umbauten","bauDoku","customFieldsLesbar","umbautenLesbar","bauDokuLesbar","quellenFehler","generatedAt"],"additionalProperties":false},"example":{"mandant":{"id":"string","slug":"string","name":"string"},"customFields":[],"umbauten":[],"bauDoku":[],"customFieldsLesbar":true,"umbautenLesbar":true,"bauDokuLesbar":true,"quellenFehler":[{"quelle":"string","meldung":"string"}],"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant nicht gefunden (`tenant_not_found`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}},"503":{"description":"Datenbank nicht verfuegbar (`database_unavailable`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"retryAfter":{"type":"number"}},"required":["error"]}}}}},"operationId":"getApiAdminCustomersByIdSupport","tags":["admin","tenants"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Support-Ansicht eines Mandanten, ohne Geschaeftsdaten","description":"Support-Ansicht: eigene Felder, Umbauten, Bau-Doku eines Mandanten (ohne Geschaeftsdaten)"}},"/api/admin/customers/{id}/umbauten/zurueckrollen":{"post":{"responses":{"200":{"description":"Zurueckgerollt — die erfassten Werte bleiben erhalten","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"zurueckgerollt":{"type":"object","properties":{"kind":{"type":"string","enum":["custom_field","custom_entity","validation_rule","ui_config","relation","action","workflow"]},"entity":{"type":"string"},"artifactId":{"type":"string"}},"required":["kind","entity","artifactId"]},"geistBeseitigt":{"type":"boolean"},"hinweis":{"type":"string"}},"required":["ok","zurueckgerollt","hinweis"]},"example":{"ok":true,"zurueckgerollt":{"kind":"custom_field","entity":"string","artifactId":"string"},"geistBeseitigt":true,"hinweis":"string"}}}},"400":{"description":"Ungueltige Eingabe, unerlaubte Entitaet, oder eine Art ohne Rueckweg — die Antwort traegt dann `grund` im Klartext"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Mandant oder Umbau nicht gefunden"},"503":{"description":"Datenbank nicht verfuegbar — ODER der Umbau wurde entfernt, das Manifest liess sich aber nicht stilllegen (`manifest_nicht_stillgelegt`)"}},"operationId":"postApiAdminCustomersByIdUmbautenZurueckrollen","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Umbau eines Mandanten zurueckrollen (Support)","description":"Nimmt einen Umbau eines Mandanten zurueck. Einen echten Rueckweg haben eigenes Feld (custom_field) und eigenes Modul (custom_entity); die uebrigen Arten antworten mit 400 UND dem Grund, warum es fuer sie keinen gibt. Fehlt der Umbau schon, ist aber im Manifest noch aktiv („Geist\"), wird der Manifest-Eintrag stillgelegt statt 404 zu melden.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["custom_field","custom_entity","validation_rule","ui_config","relation","action","workflow"]},"entity":{"type":"string","minLength":1,"maxLength":64},"artifactId":{"type":"string","minLength":1,"maxLength":128},"grund":{"type":"string","maxLength":500}},"required":["kind","entity","artifactId"]},"example":{"kind":"custom_field","entity":"string","artifactId":"string","grund":"string"}}}}}},"/api/admin/customers/{id}/plan":{"post":{"responses":{"200":{"description":"Plan changed","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"targetPlan":{"type":"string","enum":["starter","professional","enterprise","trial"]},"planId":{"type":"string"},"monthlyPriceEur":{"type":["string","null"]}},"required":["tenantId","targetPlan","planId","monthlyPriceEur"]},"example":{"tenantId":"string","targetPlan":"starter","planId":"string","monthlyPriceEur":"string"}}}},"400":{"description":"Unknown plan name"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not found"},"500":{"description":"Plan change failed"},"503":{"description":"Database unavailable"}},"operationId":"postApiAdminCustomersByIdPlan","tags":["admin","customers","billing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Change tenant plan (upgrade/downgrade), with optional proration record","description":"Looks the target plan up by name in public.plans, then repoints `tenants.plan_id` and bumps `tenants.updated_at`. A row is appended to public.tenant_plan_changes carrying `prorate` and `effectiveAt` — but only best-effort: if that table is missing the plan change still stands, only the proration record is lost. Same for the `billing.plan_change` entry in public.admin_audit_log. Nothing is charged or refunded here; the endpoint only records the change. 400 when the plan name is unknown, 404 when the tenant does not exist.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"targetPlan":{"type":"string","enum":["starter","professional","enterprise","trial"]},"effectiveAt":{"type":"string","format":"date-time"},"prorate":{"type":"boolean","default":true}},"required":["targetPlan"]},"example":{"targetPlan":"starter","effectiveAt":"2026-01-01T12:00:00.000Z","prorate":true}}}}}},"/api/admin/customers/{id}/billing":{"get":{"responses":{"200":{"description":"Billing summary","content":{"application/json":{"schema":{"type":"object","properties":{"tenant":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"plan_name":{"type":["string","null"]},"price_eur_monthly":{"type":["string","null"]},"status":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","slug","name","plan_name","price_eur_monthly","status","created_at"]},"invoices":{"type":"array","items":{"type":"object","properties":{"id":{"anyOf":[{"type":"string"},{"type":"number"}]},"period_start":{"type":["string","null"]},"period_end":{"type":["string","null"]},"amount_eur":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"status":{"type":["string","null"]},"paid_at":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","period_start","period_end","amount_eur","status","paid_at","created_at"]}},"paymentMethod":{"type":"null"},"paymentMethodQuelle":{"type":"string","const":"nicht_erhoben"},"mrrEur":{"type":"number"}},"required":["tenant","invoices","paymentMethod","paymentMethodQuelle","mrrEur"]},"example":{"tenant":{"id":"string","slug":"string","name":"string","plan_name":"string","price_eur_monthly":"string","status":"string","created_at":"string"},"invoices":[{"id":"string","period_start":"string","period_end":"string","amount_eur":"string","status":"string","paid_at":"string","created_at":"string"}],"paymentMethod":null,"paymentMethodQuelle":"nicht_erhoben","mrrEur":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not found"},"500":{"description":"Billing summary could not be read"},"503":{"description":"Database unavailable"}},"operationId":"getApiAdminCustomersByIdBilling","tags":["admin","customers","billing"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Tenant billing overview: current plan, invoice history, payment method","description":"Joins the tenant against its plan and appends up to 24 rows from public.tenant_invoices, newest billing period first; if that table is missing the list stays empty instead of failing. `mrrEur` is simply the plan's monthly price as a number. The payment method is deliberately NOT reported: it lives at Stripe and is not mirrored here, so `paymentMethod` is always null and `paymentMethodQuelle` says \"nicht_erhoben\" — that means \"not collected\", not \"the customer has none\". 404 when the tenant does not exist."}},"/api/admin/customers/{id}/audit-log":{"get":{"responses":{"200":{"description":"Audit log entries, newest first — `source` names the table that answered","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"anyOf":[{"type":"string"},{"type":"number"}]},"action":{"type":"string"},"actorUserId":{"type":["string","null"]},"targetTenantId":{"type":["string","null"]},"payload":{},"createdAt":{"type":"string"}},"required":["id","action","actorUserId","targetTenantId","createdAt"]}},"source":{"type":"string","enum":["admin_audit_log","audit_log","none"]},"message":{"type":"string"}},"required":["data"]},"example":{"data":[{"id":"string","action":"string","actorUserId":"string","targetTenantId":"string","createdAt":"string"}],"source":"admin_audit_log","message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Unexpected error while reading the audit log"}},"operationId":"getApiAdminCustomersByIdAudit-log","tags":["admin","customers","audit"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Last 100 audit entries for a tenant, across admin and tenant actions","description":"Reads public.admin_audit_log newest first; if that table is missing the endpoint silently falls back to public.audit_log with the same column aliases, and if neither exists it answers 200 with an empty list and `source: \"none\"`. The `source` field names which table actually answered. Page size comes from the `limit` query parameter — default 100, clamped to 1…500. Read-only; no table is created."}},"/api/admin/customers/{id}/notes":{"get":{"responses":{"200":{"description":"Notes — pinned first, then newest first, at most 200","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"pinned":{"type":"boolean"},"visibility":{"type":"string"},"authorUserId":{"type":["string","null"]},"authorName":{"type":["string","null"]},"authorEmail":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","body","pinned","visibility","authorUserId","authorName","authorEmail","createdAt","updatedAt"]}},"message":{"type":"string"}},"required":["data"]},"example":{"data":[{"id":"string","body":"string","pinned":true,"visibility":"string","authorUserId":"string","authorName":"string","authorEmail":"string","createdAt":"string","updatedAt":"string"}],"message":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminCustomersByIdNotes","tags":["admin","customers","notes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Internal support notes for a tenant (NOT visible to tenant)","description":"Reads public.admin_tenant_notes for the tenant, pinned entries first, then newest first, capped at 200 rows — there is no paging. Each row is joined to public.users to carry the author name and e-mail. Answers 200 with an empty list even when the notes table does not exist; the `message` field then says so."},"post":{"responses":{"201":{"description":"Note created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"pinned":{"type":"boolean"},"visibility":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","body","pinned","visibility","createdAt"]}},"required":["data"]},"example":{"data":{"id":"string","body":"string","pinned":true,"visibility":"string","createdAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Insert failed — e.g. the notes table does not exist"},"503":{"description":"Database unavailable"}},"operationId":"postApiAdminCustomersByIdNotes","tags":["admin","customers","notes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Add an internal support note to a tenant","description":"Inserts one row into public.admin_tenant_notes. `body` is required (1…5000 characters); `pinned` defaults to false and `visibility` to \"internal\". The author is taken from the calling admin session, not from the body. The response carries only the columns of the INSERT … RETURNING, so author and updatedAt are absent here — read them back via the list endpoint.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":5000},"pinned":{"type":"boolean","default":false},"visibility":{"type":"string","enum":["internal","support"],"default":"internal"}},"required":["body"]},"example":{"body":"string","pinned":true,"visibility":"internal"}}}}}},"/api/admin/customers/{id}/notes/{noteId}":{"delete":{"responses":{"200":{"description":"Note deleted (or nothing matched)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Delete failed"},"503":{"description":"Database unavailable"}},"operationId":"deleteApiAdminCustomersByIdNotesByNoteId","tags":["admin","customers","notes"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"noteId","required":true}],"summary":"Delete a tenant support note","description":"Removes the row permanently — a hard DELETE, no soft-delete and no undo. The statement is scoped to both the note id and the tenant id, so a note of another tenant is never hit. A note id that matches nothing is not an error: the endpoint answers 200 either way."}},"/api/admin/customers/{id}/limits":{"get":{"responses":{"200":{"description":"Effective limits — `data` is absent when no database connection exists","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"apiRateLimitPerMinute":{"anyOf":[{"type":"number"},{"type":"string"}]},"storageCapGb":{"anyOf":[{"type":"number"},{"type":"string"}]},"aiTokensPerMonth":{"anyOf":[{"type":"number"},{"type":"string"}]},"maxUsers":{"anyOf":[{"type":"number"},{"type":"string"}]},"planName":{"type":["string","null"]}},"required":["id","apiRateLimitPerMinute","storageCapGb","aiTokensPerMonth","maxUsers","planName"]}}},"example":{"data":{"id":"string","apiRateLimitPerMinute":0,"storageCapGb":0,"aiTokensPerMonth":0,"maxUsers":0,"planName":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not found"},"500":{"description":"Limits could not be read"}},"operationId":"getApiAdminCustomersByIdLimits","tags":["admin","customers","limits"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Tenant limits/quotas (rate-limit, storage, AI tokens, users)","description":"Returns the EFFECTIVE limits, resolved per value in three steps: the admin override under `tenants.settings.limits`, then the column of the tenant's plan, then a hard-coded default (600 requests/minute, 10 GB, 1,000,000 AI tokens, 5 users). `planName` names the joined plan and is null when the tenant has none. Read-only. 404 when no tenant carries the given id; on a query error the raw database message stays in the log and the client only sees `limits_unavailable`."},"patch":{"responses":{"200":{"description":"Limits updated — `limits` is the merged state","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"limits":{"type":"object","properties":{"apiRateLimitPerMinute":{"type":"integer","exclusiveMinimum":0,"maximum":100000},"storageCapGb":{"type":"number","exclusiveMinimum":0,"maximum":100000},"aiTokensPerMonth":{"type":"integer","exclusiveMinimum":0,"maximum":100000000},"maxUsers":{"type":"integer","exclusiveMinimum":0,"maximum":100000}},"additionalProperties":true}},"required":["ok","limits"]},"example":{"ok":true,"limits":{"apiRateLimitPerMinute":1,"storageCapGb":1,"aiTokensPerMonth":1,"maxUsers":1}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Database unavailable"}},"operationId":"patchApiAdminCustomersByIdLimits","tags":["admin","customers","limits"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Override tenant limits/quotas (stored under tenants.settings.limits)","description":"Partial update: only the keys present in the body are written, the remaining stored overrides survive. The merged object replaces `tenants.settings.limits` and `tenants.updated_at` is bumped; the plan itself is not touched, so removing a key here is not possible via this endpoint. Writes a `limits.update` entry to public.admin_audit_log — a failed audit write is swallowed and does not abort the update. Answers 200 even when the id matches no tenant (the UPDATE then hits zero rows).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"apiRateLimitPerMinute":{"type":"integer","exclusiveMinimum":0,"maximum":100000},"storageCapGb":{"type":"number","exclusiveMinimum":0,"maximum":100000},"aiTokensPerMonth":{"type":"integer","exclusiveMinimum":0,"maximum":100000000},"maxUsers":{"type":"integer","exclusiveMinimum":0,"maximum":100000}}},"example":{"apiRateLimitPerMinute":1,"storageCapGb":1,"aiTokensPerMonth":1,"maxUsers":1}}}}}},"/api/admin/customers/{id}/stammdaten":{"patch":{"responses":{"200":{"description":"Updated — `stammdaten` is the merged state","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"stammdaten":{"type":"object","properties":{"legalName":{"type":"string","maxLength":255},"vatId":{"type":"string","maxLength":50},"legalForm":{"type":"string","maxLength":50},"industry":{"type":"string","maxLength":100},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"street":{"type":"string","maxLength":255},"zip":{"type":"string","maxLength":20},"city":{"type":"string","maxLength":100},"country":{"type":"string","minLength":2,"maxLength":2},"contactName":{"type":"string","maxLength":200},"contactEmail":{"type":"string","format":"email"},"contactPhone":{"type":"string","maxLength":50},"logoUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]}},"additionalProperties":true}},"required":["ok","stammdaten"]},"example":{"ok":true,"stammdaten":{"legalName":"string","vatId":"string","legalForm":"string","industry":"string","website":"https://example.com","street":"string","zip":"string","city":"string","country":"st","contactName":"string","contactEmail":"beispiel@example.com","contactPhone":"string","logoUrl":"https://example.com"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Update failed"},"503":{"description":"Database unavailable"}},"operationId":"patchApiAdminCustomersByIdStammdaten","tags":["admin","customers","stammdaten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Update tenant master data (anschrift, USt-ID, contact person)","description":"Partial update: only the keys present in the body are written, everything already stored under `tenants.settings.stammdaten` survives — there is no way to clear a key here, and `tenants.name` itself is NOT changed by a `legalName` in the body. Bumps `tenants.updated_at` and writes a `stammdaten.update` entry to public.admin_audit_log; a failed audit write is swallowed. Answers 200 even when the id matches no tenant, because the UPDATE then simply hits zero rows.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"legalName":{"type":"string","maxLength":255},"vatId":{"type":"string","maxLength":50},"legalForm":{"type":"string","maxLength":50},"industry":{"type":"string","maxLength":100},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"street":{"type":"string","maxLength":255},"zip":{"type":"string","maxLength":20},"city":{"type":"string","maxLength":100},"country":{"type":"string","minLength":2,"maxLength":2},"contactName":{"type":"string","maxLength":200},"contactEmail":{"type":"string","format":"email"},"contactPhone":{"type":"string","maxLength":50},"logoUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]}}},"example":{"legalName":"string","vatId":"string","legalForm":"string","industry":"string","website":"https://example.com","street":"string","zip":"string","city":"string","country":"st","contactName":"string","contactEmail":"beispiel@example.com","contactPhone":"string","logoUrl":"https://example.com"}}}}},"get":{"responses":{"200":{"description":"Master data block — `data` is absent when no database connection exists","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"legalName":{"type":"string"},"vatId":{"type":"string","maxLength":50},"legalForm":{"type":"string","maxLength":50},"industry":{"type":"string","maxLength":100},"website":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]},"street":{"type":"string","maxLength":255},"zip":{"type":"string","maxLength":20},"city":{"type":"string","maxLength":100},"country":{"type":"string","minLength":2,"maxLength":2},"contactName":{"type":"string","maxLength":200},"contactEmail":{"type":"string","format":"email"},"contactPhone":{"type":"string","maxLength":50},"logoUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}]}},"required":["legalName"],"additionalProperties":true}}},"example":{"data":{"legalName":"string","vatId":"string","legalForm":"string","industry":"string","website":"https://example.com","street":"string","zip":"string","city":"string","country":"st","contactName":"string","contactEmail":"beispiel@example.com","contactPhone":"string","logoUrl":"https://example.com"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"404":{"description":"Tenant not found"},"500":{"description":"Read failed"}},"operationId":"getApiAdminCustomersByIdStammdaten","tags":["admin","customers","stammdaten"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Get tenant master data block","description":"Composes the block from two places: `legalName` always comes from the `tenants.name` column, all other fields from `tenants.settings.stammdaten`. A `legalName` stored in the settings therefore overrides the column value in this answer. Never fails on missing master data — an unset block yields just `legalName`. 404 when no tenant carries the given id."}},"/api/admin/customers/{tenantId}/impersonate":{"post":{"responses":{"200":{"description":"Impersonation token issued — returned as cookie, body carries the target and redirect data","content":{"application/json":{"schema":{"type":"object","properties":{"expiresInSec":{"type":"integer"},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"tenantName":{"type":"string"},"targetUserId":{"type":"string"},"targetEmail":{"type":"string"},"redirectUrl":{"type":"string"}},"required":["expiresInSec","tenantId","tenantSlug","tenantName","targetUserId","targetEmail","redirectUrl"]},"example":{"expiresInSec":0,"tenantId":"string","tenantSlug":"string","tenantName":"string","targetUserId":"string","targetEmail":"string","redirectUrl":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden — not a hersteller-admin"},"404":{"description":"Tenant or admin user not found"},"503":{"description":"Database unavailable"}},"operationId":"postApiAdminCustomersByTenantIdImpersonate","tags":["admin","customers","impersonate"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"summary":"Hersteller-Admin: issue short-lived impersonation JWT for tenant primary admin","description":"Resolves the tenant and its primary admin user (users with role \"admin\" first, then oldest by created_at) and signs an HS256 JWT that is valid for 15 minutes. The token is NOT part of the response body: it is delivered only as the HttpOnly/Secure/SameSite=Lax cookie `__Secure-nemix_impersonation`, scoped to the shared parent domain so it travels to the tenant sub-domain named in `redirectUrl`. Writes an `impersonate.start` row into public.admin_audit_log (table created on demand); a failed audit write is logged but does not abort the request. Callers must be role \"system\"/\"admin\" or layerType \"hersteller\", otherwise 403. 404 when the tenant does not exist or has no non-deleted user."}},"/api/admin/customers/{tenantId}/impersonate/end":{"post":{"responses":{"200":{"description":"Impersonation ended — cookie cleared","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"]},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiAdminCustomersByTenantIdImpersonateEnd","tags":["admin","customers","impersonate"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"summary":"End an active impersonation session","description":"Deletes the `__Secure-nemix_impersonation` cookie (same path/domain/flags as on set — otherwise the browser keeps the cross-subdomain cookie alive) and appends an `impersonate.end` row to public.admin_audit_log. The stored JWT itself is not revoked; it simply expires after its 15-minute lifetime. Always answers 200, also when no database or no caller identity is available — then only the cookie is cleared."}},"/api/admin/customers/{id}/history":{"get":{"responses":{"200":{"description":"Seite der Aenderungshistorie, neueste zuerst","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"source":{"type":"string","enum":["ai_build_docs","ai_build_versions","tenant_activity_log","undo_log","gobd_chain","tenant_audit_log","ai_audit_log","invoice_versions","document_versions","customization_manifest"]},"refId":{"type":"string"},"occurredAt":{"type":"string"},"category":{"type":"string","enum":["ki_bau","regel","anpassung","daten_edit","loeschung","beleg_version","dokument_version","compliance","undo"]},"entity":{"type":["string","null"]},"targetLabel":{"type":"string"},"summary":{"type":"string"},"actorLabel":{"type":"string"},"actorType":{"type":"string","enum":["mensch","ki","system"]},"reason":{"type":["string","null"]},"details":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"value":{"type":"string"}},"required":["label","value"]}},"diff":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"label":{"type":"string"},"before":{},"after":{}},"required":["field","label"]}},"revertable":{"type":"boolean"},"revertRef":{"type":["object","null"],"properties":{"kind":{"type":"string","enum":["build_version","ai_undo"]},"id":{"type":"string"}},"required":["kind","id"]},"integrityVerified":{"type":"boolean"}},"required":["id","source","refId","occurredAt","category","entity","targetLabel","summary","actorLabel","actorType","reason","details","diff","revertable","revertRef"]}},"nextCursor":{"type":["string","null"]}},"required":["events","nextCursor"]},"example":{"events":[{"id":"string","source":"ai_build_docs","refId":"string","occurredAt":"string","category":"ki_bau","entity":"string","targetLabel":"string","summary":"string","actorLabel":"string","actorType":"mensch","reason":"string","details":[{"label":"string","value":"string"}],"diff":[{"field":"string","label":"string"}],"revertable":true,"revertRef":{"kind":"build_version","id":"string"},"integrityVerified":true}],"nextCursor":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminCustomersByIdHistory","tags":["admin","customers","history"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Konsolidierte Aenderungshistorie eines Mandanten","description":"Mischt die getrennten Historien-Quellen des Ziel-Mandanten (KI-Bau-Log, Feld-Versionen, activity_log, GoBD-Audit, Rechnungs- und Dokument-Versionen, Anpassungs-Manifest) zu einer nach `occurredAt` absteigend sortierten Liste. Blaettert per Keyset: `before` uebernimmt den `nextCursor` der Vorseite, `pageSize` steuert die Seitengroesse. Rein lesend — im fremden Mandanten-Schema wird keine Tabelle angelegt, fehlende Quellen werden uebersprungen. Laesst sich der Mandant nicht aufloesen oder faellt die Datenbank aus, antwortet der Endpunkt mit einer leeren Seite statt mit einem Fehler."}},"/api/admin/users":{"get":{"responses":{"200":{"description":"Nutzer der Seite plus Seitenangaben","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Kennung des Nutzers"},"email":{"type":"string","description":"E-Mail-Adresse"},"name":{"type":"string","description":"Anzeigename; leerer String wenn keiner erfasst ist"},"role":{"type":"string","description":"Rolle; `user` wenn in der Datenbank keine steht"},"tenantId":{"type":"string","description":"Mandant des Nutzers; leerer String wenn keiner zugeordnet ist"},"tenantName":{"type":"string","description":"Name des Mandanten; FEHLT, wenn keiner ermittelbar war"},"emailVerified":{"type":"boolean","description":"true nur bei ausdruecklich bestaetigter Adresse; NULL zaehlt als false"},"lastLoginAt":{"type":["string","null"],"description":"Letzte Anmeldung; null wenn nie angemeldet"},"createdAt":{"type":["string","null"],"description":"Anlagezeitpunkt"}},"required":["id","email","name","role","tenantId","emailVerified","lastLoginAt","createdAt"]},"description":"Die Nutzer der Seite, neueste zuerst"},"total":{"type":"integer","minimum":0,"description":"Anzahl aller Treffer der Filter, unabhaengig von limit/offset"},"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Angeforderte Seitengroesse"},"offset":{"type":"integer","minimum":0,"description":"Uebersprungene Eintraege"}},"required":["data","total","limit","offset"]},"example":{"data":[{"id":"string","email":"string","name":"string","role":"string","tenantId":"string","tenantName":"string","emailVerified":true,"lastLoginAt":"string","createdAt":"string"}],"total":0,"limit":1,"offset":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht verfuegbar oder Abfrage fehlgeschlagen","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"integer","minimum":0,"description":"Empfohlene Wartezeit in Sekunden"}},"required":["error","retryAfter"]}}}}},"operationId":"getApiAdminUsers","tags":["admin","users"],"parameters":[{"in":"query","name":"tenantId","schema":{"type":"string","minLength":1}},{"in":"query","name":"q","schema":{"type":"string","minLength":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"summary":"Nutzer aller Mandanten auflisten und filtern","description":"Liest `public.users` ueber ALLE Mandanten hinweg, verbunden mit `public.tenants` fuer den Mandantennamen, neueste zuerst. Geloeschte Nutzer (`deleted_at`) bleiben aussen vor. `tenantId` und `role` filtern exakt, `q` sucht als Teiltext in E-Mail ODER Name; `limit` (1-200, Vorgabe 50) und `offset` blaettern, `total` zaehlt alle Treffer der Filter. Scheitert die Abfrage, kommt 503 und KEINE leere Liste — sonst waere „kein Nutzer\" von „Abfrage kaputt\" nicht zu unterscheiden."}},"/api/admin/users/{id}":{"delete":{"responses":{"200":{"description":"Removed. The account can no longer sign in.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"id":{"type":"string"},"email":{"type":"string"}},"required":["ok","id","email"]},"example":{"ok":true,"id":"string","email":"string"}}}},"400":{"description":"Confirmation missing or not matching (`confirmation_mismatch`)"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Not a super_admin caller"},"404":{"description":"Unknown or already removed (`not_found`)"},"409":{"description":"Own account (`self`), not a super admin (`not_a_super_admin`), or the last active super admin (`last_super_admin`)"},"503":{"description":"Database unavailable"}},"operationId":"deleteApiAdminUsersById","tags":["admin","users"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Remove a super admin (soft delete)","description":"Sets `deleted_at` on a user whose role is `super_admin`. Requires `?bestaetigung=<email>` matching the target exactly; without it nothing happens. Refuses the caller's own account and refuses anyone who is not a super admin — this area manages super admins only. THE LAST ACTIVE SUPER ADMIN CANNOT BE REMOVED: a trigger on public.users refuses it and this route reports that as 409 `last_super_admin`. The trigger also covers the nine other paths that do not run through here."}},"/api/admin/layer-sync/export":{"post":{"responses":{"200":{"description":"Das signierte Buendel unter `bundle`. Seine innere Form bestimmt `@nemix/layer-engine`, nicht diese Route — deshalb hier keine Feldliste.","content":{"application/json":{"schema":{"type":"object","properties":{"bundle":{}}}}}},"400":{"description":"`layer` fehlt oder `env` ist kein gueltiger Umgebungsname."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin` (zusaetzlich zum Tor der Admin-App)."},"404":{"description":"Diese Ebene gibt es in der Quellumgebung nicht."},"500":{"description":"`LAYER_SYNC_SECRET` ist nicht gesetzt."},"503":{"description":"Adapter nicht verdrahtet — heute der Regelfall, siehe Beschreibung."}},"operationId":"postApiAdminLayer-syncExport","tags":["admin"],"parameters":[],"summary":"Eine Ebene als signiertes Buendel ausgeben","description":"Packt eine Ebene samt ihrer Beitraege in ein JSON-Buendel und signiert es mit dem Geheimnis aus `LAYER_SYNC_SECRET` (ersatzweise `LAYER_PROMOTE_SECRET`). Das Buendel ist die Transporteinheit fuer `/import` in einer anderen Umgebung.\n\nBeide Angaben stehen in der ABFRAGE, nicht im Rumpf: `layer` (Kennung) und `env` (Quellumgebung). Gueltige Umgebungsnamen sind `dev`, `sandkasten` und `produktiv` — alles andere ergibt 400.\n\nTrotz `POST` wird KEIN Rumpf gelesen — die Methode ist gewaehlt, weil signiert und protokolliert wird, nicht weil etwas mitgeschickt wird.\n\nHEUTE NICHT BENUTZBAR: der Adapter, ueber den diese Route ihre Daten holt, wird in der laufenden Anwendung nirgends gesetzt (`setLayerSyncAdapter` steht nur im Test). Die Antwort ist deshalb ein 503 — kein voruebergehender Fehler, sondern ein fehlendes Bauteil.\n\nDie `/api/admin`-App ist als Ganzes `requireSuperAdmin` (`index.ts:2615`); der zusaetzliche `admin`-Test im Handler ist ein zweiter Riegel, kein eigenes Tor."}},"/api/admin/layer-sync/import":{"post":{"responses":{"200":{"description":"Das Ergebnis unter `result`. `applied` sagt, ob wirklich geschrieben wurde — bei `dryRun` niemals.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{}}}}}},"400":{"description":"Umgebungsname ungueltig, `bundle` fehlt, Signatur oder Aufbau falsch, oder die Quellumgebung des Buendels passt nicht zu `from`."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin` — ODER `POLICY_DENIED`/`APPROVAL_REQUIRED` aus der Freigabepruefung. Zwei sehr verschiedene Faelle unter einem Code; die Meldung unterscheidet sie."},"409":{"description":"`CONFLICT` — der Bestand widerspricht, `conflictMode: \"fail\"`."},"500":{"description":"`LAYER_SYNC_SECRET` ist nicht gesetzt."},"503":{"description":"Adapter nicht verdrahtet."}},"operationId":"postApiAdminLayer-syncImport","tags":["admin"],"parameters":[],"summary":"Ein signiertes Buendel in eine Umgebung einspielen","description":"Prueft die Signatur des Buendels, vergleicht es mit dem Bestand der Zielumgebung und spielt es ein.\n\nDie Umgebungen stehen in der ABFRAGE (`from`, `to`), das Buendel im RUMPF unter `bundle`. Gueltige Umgebungsnamen sind `dev`, `sandkasten` und `produktiv` — alles andere ergibt 400. Stimmt die im Buendel vermerkte Quellumgebung nicht mit `from` ueberein, gibt es 400 — das verhindert, dass ein Buendel aus der falschen Richtung eingespielt wird.\n\nDrei weitere Felder im Rumpf, alle freiwillig:\n· `dryRun` — rechnet durch, SCHREIBT ABER NICHT. Die Antwort sieht aus wie im Ernstfall; nur `applied` verraet den Unterschied.\n· `conflictMode` — `fail` (Standard), `overwrite` oder `skip`. Der Standard ist der vorsichtige: bei einem Widerspruch bricht es mit 409 ab, statt zu ueberschreiben.\n· `approvalsGranted` — setzt eine verlangte Freigabe als erteilt. Wer das mitschickt, umgeht die Freigabe; die Route prueft NICHT, ob sie wirklich erteilt wurde.\n\nDIE FEHLERCODES TRAGEN BEDEUTUNG: 403 heisst `POLICY_DENIED` (dieser Weg zwischen den Umgebungen ist nicht erlaubt) ODER `APPROVAL_REQUIRED` (er waere erlaubt, aber jemand muss zustimmen) — die Meldung nennt den Code. 409 heisst `CONFLICT`. Alles andere aus der Pruefung ergibt 400.\n\nHEUTE NICHT BENUTZBAR: der Adapter, ueber den diese Route ihre Daten holt, wird in der laufenden Anwendung nirgends gesetzt (`setLayerSyncAdapter` steht nur im Test). Die Antwort ist deshalb ein 503 — kein voruebergehender Fehler, sondern ein fehlendes Bauteil. Bei DIESER Route schlaegt allerdings meist schon das fehlende Geheimnis vorher zu (500).\n\nDie `/api/admin`-App ist als Ganzes `requireSuperAdmin` (`index.ts:2615`); der zusaetzliche `admin`-Test im Handler ist ein zweiter Riegel, kein eigenes Tor."}},"/api/admin/layer-sync/diff":{"get":{"responses":{"200":{"description":"Der Vergleich: `summary` (Zaehlwerk), `entries` (die einzelnen Unterschiede) und `policy` (ob der Weg erlaubt waere). Die innere Form bestimmt `@nemix/layer-engine`.","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{},"entries":{"type":"array","items":{}},"policy":{}},"required":["entries"]},"example":{"entries":[]}}}},"400":{"description":"`layer` fehlt oder ein Umgebungsname ist ungueltig."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Rolle unter `admin`."},"503":{"description":"Adapter nicht verdrahtet — heute der Regelfall, siehe Beschreibung."}},"operationId":"getApiAdminLayer-syncDiff","tags":["admin"],"parameters":[],"summary":"Eine Ebene zwischen zwei Umgebungen vergleichen","description":"Zeigt, was sich zwischen zwei Umgebungen an einer Ebene unterscheidet, und ob dieser Weg ueberhaupt erlaubt waere.\n\nAlle drei Angaben stehen in der ABFRAGE: `layer`, `from`, `to`. Gueltige Umgebungsnamen sind `dev`, `sandkasten` und `produktiv` — alles andere ergibt 400.\n\nFEHLT DIE EBENE IN EINER DER BEIDEN UMGEBUNGEN, ist das KEIN Fehler: die fehlende Seite gilt als leer, und der Vergleich zeigt sie entsprechend als vollstaendigen Zugang oder Wegfall. Fehlt sie in BEIDEN, kommt ein leerer Vergleich mit 200 — nicht 404.\n\n`policy` beantwortet die zweite Frage: ob eine Uebernahme von `from` nach `to` fuer diese Ebenenart zulaessig ist. Sie schaut NICHT auf die Unterschiede, sondern nur auf Art und Richtung — eine Wegauskunft, kein Urteil ueber den Inhalt. Ist die Ebene in keiner der beiden Umgebungen vorhanden, wird ersatzweise mit der Art `hersteller` gerechnet.\n\nDiese Route SCHREIBT NICHTS und braucht kein Geheimnis — sie faellt deshalb nicht in den 500, sondern direkt in den 503.\n\nHEUTE NICHT BENUTZBAR: der Adapter, ueber den diese Route ihre Daten holt, wird in der laufenden Anwendung nirgends gesetzt (`setLayerSyncAdapter` steht nur im Test). Die Antwort ist deshalb ein 503 — kein voruebergehender Fehler, sondern ein fehlendes Bauteil.\n\nDie `/api/admin`-App ist als Ganzes `requireSuperAdmin` (`index.ts:2615`); der zusaetzliche `admin`-Test im Handler ist ein zweiter Riegel, kein eigenes Tor."}},"/api/admin/ai-usage":{"get":{"responses":{"200":{"description":"Wochenverlauf und Mandanten-Aufstellung. Alle Betraege in EUR.","content":{"application/json":{"schema":{"type":"object","properties":{"weekly":{"type":"object","properties":{"totalInputTokens":{"type":"number"},"totalOutputTokens":{"type":"number"},"totalTokens":{"type":"number"},"totalCostEur":{"type":"number"},"avgCostPerDayEur":{"type":"number"},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"costEur":{"type":"number"}},"required":["date","inputTokens","outputTokens","costEur"],"additionalProperties":false}}},"required":["totalInputTokens","totalOutputTokens","totalTokens","totalCostEur","avgCostPerDayEur","days"],"additionalProperties":false},"byTenant":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"tenantName":{"type":"string"},"tenantSlug":{"type":"string"},"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"totalTokens":{"type":"number"},"costEur":{"type":"number"},"callCount":{"type":"number"}},"required":["tenantId","tenantName","tenantSlug","inputTokens","outputTokens","totalTokens","costEur","callCount"],"additionalProperties":false}}},"required":["weekly","byTenant"],"additionalProperties":false},"example":{"weekly":{"totalInputTokens":0,"totalOutputTokens":0,"totalTokens":0,"totalCostEur":0,"avgCostPerDayEur":0,"days":[{"date":"string","inputTokens":0,"outputTokens":0,"costEur":0}]},"byTenant":[{"tenantId":"string","tenantName":"string","tenantSlug":"string","inputTokens":0,"outputTokens":0,"totalTokens":0,"costEur":0,"callCount":0}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht verfuegbar. Bewusst ein Fehler statt einer Woche voller Nullen — die waere von „nichts verbraucht\" nicht zu unterscheiden."}},"operationId":"getApiAdminAi-usage","tags":["admin","ai"],"parameters":[],"description":"KI-Kosten der letzten 7 Tage und Verteilung auf die Mandanten (Super-Admin). Gelesen wird `public.ai_cost_events` ueber ALLE Mandanten hinweg — der Endpunkt kennt keine Parameter, das Fenster ist fest auf sieben Tage gesetzt. Der Wochenverlauf ist lueckenlos: Tage ohne Ereignisse kommen mit Nullen mit, und die Tagesgrenzen zieht die Datenbank, nicht der API-Container. Die Mandanten-Aufstellung ist nach Kosten absteigend sortiert und auf 100 Zeilen gekappt. Gespeichert wird in USD, ausgewiesen in EUR — umgerechnet mit dem Spot-Kurs OHNE Sicherheitsaufschlag, dieser Wert ist also keine Abrechnungsgrundlage.","summary":"KI-Kosten der letzten 7 Tage und Verteilung auf die Mandanten (Super-Admin)","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/cost-summary":{"get":{"responses":{"200":{"description":"Wochenuebersicht","content":{"application/json":{"schema":{"type":"object","properties":{"windowStart":{"type":"string","description":"Beginn des Auswertungsfensters als ISO-8601-Zeitstempel"},"windowEnd":{"type":"string","description":"Ende des Auswertungsfensters als ISO-8601-Zeitstempel"},"perTenant":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, dem die Kosten zugeordnet sind"},"aiEurCents":{"type":"number","description":"KI-Kosten in Euro-Cent — Ereignisse mit Praefix `anthropic.`"},"infraEurCents":{"type":"number","description":"Infrastrukturkosten in Euro-Cent — Ereignisse mit Praefix `infra.`"},"totalEurCents":{"type":"number","description":"Summe ALLER Ereignisse des Mandanten, auch solcher ohne bekanntes Praefix"},"weeklyLimitEurCents":{"type":["number","null"],"description":"Monatslimit auf 7 Tage umgerechnet; null heisst unbegrenzt — heute immer null"},"utilization":{"type":["number","null"],"description":"Verbrauch geteilt durch Wochenlimit; null wenn kein Limit hinterlegt ist"},"flagged":{"type":"boolean","description":"true ab 80 % des Wochenlimits — ohne Limit nie"}},"required":["tenantId","aiEurCents","infraEurCents","totalEurCents","weeklyLimitEurCents","utilization","flagged"]},"description":"Je Mandant eine Zeile, nach Gesamtkosten absteigend"},"totalAiEurCents":{"type":"number","description":"KI-Kosten aller Mandanten in Euro-Cent"},"totalInfraEurCents":{"type":"number","description":"Infrastrukturkosten aller Mandanten in Euro-Cent"},"totalEurCents":{"type":"number","description":"Summe aus KI und Infrastruktur in Euro-Cent"},"flaggedCount":{"type":"integer","description":"Anzahl der Mandanten ueber der 80-Prozent-Schwelle"},"infraBreakdown":{"type":"object","properties":{"s3EurCents":{"type":"number","description":"Ereignisse vom Typ `infra.s3_gb`"},"dockerEurCents":{"type":"number","description":"Ereignisse vom Typ `infra.docker_mem_mb`"},"dbEurCents":{"type":"number","description":"Ereignisse vom Typ `infra.db_conn`"}},"required":["s3EurCents","dockerEurCents","dbEurCents"],"description":"Infrastruktur nach Quelle, ueber alle Mandanten. 0 heisst: nichts gesammelt"}},"required":["windowStart","windowEnd","perTenant","totalAiEurCents","totalInfraEurCents","totalEurCents","flaggedCount","infraBreakdown"]},"example":{"windowStart":"string","windowEnd":"string","perTenant":[{"tenantId":"string","aiEurCents":0,"infraEurCents":0,"totalEurCents":0,"weeklyLimitEurCents":0,"utilization":0,"flagged":true}],"totalAiEurCents":0,"totalInfraEurCents":0,"totalEurCents":0,"flaggedCount":0,"infraBreakdown":{"s3EurCents":0,"dockerEurCents":0,"dbEurCents":0}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getApiAdminCost-summary","tags":["admin","costs"],"parameters":[],"summary":"Kosten der letzten 7 Tage je Mandant, getrennt nach KI und Infrastruktur.","description":"Aggregiert `public.cost_events` ueber die letzten sieben Tage und reicht die Zeilen an `aggregateWeekly` weiter — dieselbe reine Funktion, die auch den woechentlichen Kostenbericht per Mail rechnet. Zugeordnet wird nach Praefix: `anthropic.` zaehlt als KI, `infra.` als Infrastruktur, und `infraBreakdown` schluesselt Letztere nach S3, Docker-Speicher und Datenbankverbindungen auf. Ein monatliches Kostenlimit je Mandant fuehrt heute keine Tabelle; deshalb bleiben `weeklyLimitEurCents` und `utilization` null und `flagged` false, statt eine 0 zu behaupten. Alle Betraege in Euro-Cent — die Gesamtsumme zaehlt dabei nur KI und Infrastruktur, die Mandantenzeile dagegen jedes Ereignis. Die Rollenpruefung liegt in der Admin-Subapp, nicht in diesem Handler."}},"/api/admin/ai-cost-summary":{"get":{"responses":{"200":{"description":"Monatsuebersicht","content":{"application/json":{"schema":{"type":"object","properties":{"month":{"type":"string","description":"Der ausgewertete Monat als YYYY-MM (UTC)"},"perTenant":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string","description":"Mandant, dem die Aufrufe zugeordnet sind"},"tenantName":{"type":"string","description":"Anzeigename aus public.tenants; fehlt, wenn zum Mandanten kein Eintrag existiert"},"totalEur":{"type":"number","description":"Kosten des laufenden Monats in Euro, aus USD zum festen Kurs umgerechnet"},"callCount":{"type":"number","description":"Anzahl der KI-Aufrufe im laufenden Monat"},"inputTokens":{"type":"number","description":"Summe der Eingabe-Token"},"outputTokens":{"type":"number","description":"Summe der Ausgabe-Token"},"cachedTokens":{"type":"number","description":"Immer 0 — ai_cost_events fuehrt keine Cache-Spalte"}},"required":["tenantId","totalEur","callCount","inputTokens","outputTokens","cachedTokens"]},"description":"Bis zu 100 Mandanten, nach Kosten absteigend"},"topTools":{"type":"array","items":{"type":"object","properties":{"toolId":{"type":"string","description":"Der `task_type` des Aufrufs; Zeilen ohne Zuordnung erscheinen als \"ohne Zuordnung\""},"totalEur":{"type":"number","description":"Kosten dieses Werkzeugs im laufenden Monat in Euro"},"callCount":{"type":"number","description":"Anzahl der Aufrufe dieses Werkzeugs"},"avgEur":{"type":"number","description":"Kosten je Aufruf; 0 wenn es keinen Aufruf gab"}},"required":["toolId","totalEur","callCount","avgEur"]},"description":"Bis zu 20 Werkzeuge, nach Kosten absteigend"},"grandTotalEur":{"type":"number","description":"Summe der Mandantenkosten in Euro"},"grandTotalCalls":{"type":"number","description":"Summe der Aufrufe ueber alle Mandanten"}},"required":["month","perTenant","topTools","grandTotalEur","grandTotalCalls"]},"example":{"month":"string","perTenant":[{"tenantId":"string","tenantName":"string","totalEur":0,"callCount":0,"inputTokens":0,"outputTokens":0,"cachedTokens":0}],"topTools":[{"toolId":"string","totalEur":0,"callCount":0,"avgEur":0}],"grandTotalEur":0,"grandTotalCalls":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"Datenbank nicht verfuegbar"}},"operationId":"getApiAdminAi-cost-summary","tags":["admin","ai","costs"],"parameters":[],"summary":"KI-Kosten des laufenden Monats je Mandant und je Werkzeug.","description":"Liest `public.ai_cost_events` ab Monatsanfang (UTC) und gruppiert zweimal: einmal je Mandant — verbunden mit `public.tenants` fuer den Anzeigenamen, hoechstens 100 Zeilen — und einmal je `task_type` fuer die 20 teuersten Werkzeuge. Beide Listen sind nach Kosten absteigend sortiert. Die Tabelle fuehrt Kosten in US-Dollar; die Antwort rechnet sie zu einem festen Kurs in Euro um. Zeilen ohne `task_type` erscheinen als \"ohne Zuordnung\", damit die Summe der Werkzeuge zur Gesamtsumme passt, und `cachedTokens` ist immer 0, weil die Tabelle keine Cache-Spalte hat. Die Rollenpruefung liegt in der Admin-Subapp, nicht in diesem Handler."}},"/api/admin/usage/overview":{"get":{"responses":{"200":{"description":"Tenant usage overview — one row per active or trial tenant.","content":{"application/json":{"schema":{"type":"object","properties":{"monthBucket":{"type":"string"},"tenantCount":{"type":"number"},"data":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"tenantId":{"type":"string"},"name":{"type":"string"},"plan":{"type":"string"},"monthBucket":{"type":"string"},"apiCalls":{"type":"object","properties":{"month":{"type":"number"},"today":{"type":"number"},"rpm_current":{"type":"number"},"month_limit":{"type":"number"},"rpm_limit":{"type":"number"},"month_pct":{"type":["number","null"]}},"required":["month","today","rpm_current","month_limit","rpm_limit","month_pct"],"additionalProperties":false},"degraded":{"type":"boolean"}},"required":["tenantId","name","plan","monthBucket","apiCalls","degraded"],"additionalProperties":false},{"type":"object","properties":{"tenantId":{"type":"string"},"name":{"type":"string"},"plan":{"type":["string","null"]},"monthBucket":{"type":"string"},"apiCalls":{"type":"null"},"degraded":{"type":"boolean","const":true},"error":{"type":"string"}},"required":["tenantId","name","plan","monthBucket","apiCalls","degraded","error"],"additionalProperties":false}]}}},"required":["monthBucket","tenantCount","data"],"additionalProperties":false},"example":{"monthBucket":"string","tenantCount":0,"data":[{"tenantId":"string","name":"string","plan":"string","monthBucket":"string","apiCalls":{"month":0,"today":0,"rpm_current":0,"month_limit":0,"rpm_limit":0,"month_pct":0},"degraded":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"DB unavailable"}},"operationId":"getApiAdminUsageOverview","tags":["admin","usage"],"parameters":[],"description":"All-tenant API usage overview for the current month with plan quotas. Tenants come from Postgres (status `active` or `trial` only, newest first); the call counters come from Redis and are read for every tenant in parallel. A tenant whose counter read fails still appears in the list — with `apiCalls: null`, `degraded: true` and an `error` string, so one broken row never hides the rest. `degraded` also goes true when Redis is simply unavailable, in which case zeros mean „not measured\", not „no traffic\". No paging and no filters.","summary":"All-tenant API usage overview for the current month with plan quotas","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/usage/users":{"get":{"responses":{"200":{"description":"User list — nine fields per user, plus the paging numbers.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{},"email":{},"name":{},"role":{},"tenantId":{},"tenantName":{},"emailVerified":{},"lastLoginAt":{},"createdAt":{}},"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"}},"required":["data","total","page","limit"],"additionalProperties":false},"example":{"data":[{}],"total":0,"page":0,"limit":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"DB unavailable"}},"operationId":"getApiAdminUsageUsers","tags":["admin","usage"],"parameters":[],"description":"System-wide user list across all tenants (paged, searchable). Reads `public.users` without the soft-deleted rows, newest first, and joins the tenant name. `page` starts at 1, `limit` is clamped to 1..200 (default 50); `search` matches email OR name as a substring, `role` matches exactly. `total` is counted with the same conditions as the page. Columns are listed one by one and mapped again afterwards, so `password_hash` and `consents` cannot reach the browser. Despite the path, this endpoint reports no usage counters at all.","summary":"System-wide user list across all tenants (paged, searchable)","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/usage/{tenantId}":{"get":{"responses":{"200":{"description":"Tenant usage detail — counters, limits and plan quotas side by side.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"plan":{"type":["string","null"]},"monthBucket":{"type":"string"},"apiCalls":{"type":"object","properties":{"month":{"type":"number"},"today":{"type":"number"},"rpm_current":{"type":"number"},"month_limit":{"type":"number"},"rpm_limit":{"type":"number"},"month_pct":{"type":["number","null"]}},"required":["month","today","rpm_current","month_limit","rpm_limit","month_pct"],"additionalProperties":false},"planQuotas":{"type":"object","properties":{"api_rpm":{"type":"number"},"ai_actions_per_month":{"type":["number","null"]},"storage_gb":{"type":"number"},"max_users":{"type":["number","null"]}},"required":["api_rpm","ai_actions_per_month","storage_gb","max_users"],"additionalProperties":false},"degraded":{"type":"boolean"}},"required":["tenantId","plan","monthBucket","apiCalls","planQuotas","degraded"],"additionalProperties":false},"example":{"tenantId":"string","plan":"string","monthBucket":"string","apiCalls":{"month":0,"today":0,"rpm_current":0,"month_limit":0,"rpm_limit":0,"month_pct":0},"planQuotas":{"api_rpm":0,"ai_actions_per_month":0,"storage_gb":0,"max_users":0},"degraded":true}}}},"400":{"description":"tenantId required"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminUsageByTenantId","tags":["admin","usage"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"description":"Detailed API usage and plan quotas for a single tenant. Counters (minute, day, month) come from Redis, the plan from Postgres; if the plan lookup fails the call falls back to the starter limits instead of erroring. An UNKNOWN tenant id is not rejected — it answers 200 with `plan: null` and zeros, so a response here is no proof the tenant exists. `degraded: true` means the counters could not be read: the zeros then mean „not measured\", not „no traffic\". Beyond the API counters this also returns the plan quotas for AI actions, storage and seats, where `null` means unmetered.","summary":"Detailed API usage and plan quotas for a single tenant","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/usage/report-stripe":{"post":{"responses":{"200":{"description":"Reported. `singleTenant` says which of the two shapes you get: `result` for one tenant, `summary` for the bulk run.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"singleTenant":{"type":"boolean","const":true},"result":{"type":"object","properties":{"tenantId":{"type":"string"},"monthBucket":{"type":"string"},"quantity":{"type":"number"},"ok":{"type":"boolean"},"error":{"type":"string"},"degraded":{"type":"boolean"}},"required":["tenantId","monthBucket","quantity","ok","degraded"],"additionalProperties":false}},"required":["singleTenant","result"],"additionalProperties":false},{"type":"object","properties":{"singleTenant":{"type":"boolean","const":false},"summary":{"type":"object","properties":{"reported":{"type":"number"},"skipped":{"type":"number"},"failed":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"monthBucket":{"type":"string"},"quantity":{"type":"number"},"ok":{"type":"boolean"},"error":{"type":"string"},"degraded":{"type":"boolean"}},"required":["tenantId","monthBucket","quantity","ok","degraded"],"additionalProperties":false}}},"required":["reported","skipped","failed","results"],"additionalProperties":false}},"required":["singleTenant","summary"],"additionalProperties":false}]},"example":{"singleTenant":true,"result":{"tenantId":"string","monthBucket":"string","quantity":0,"ok":true,"error":"string","degraded":true}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"500":{"description":"Reporting failure — same body as the 200 case, with `ok: false` respectively `reported: 0`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"singleTenant":{"type":"boolean","const":true},"result":{"type":"object","properties":{"tenantId":{"type":"string"},"monthBucket":{"type":"string"},"quantity":{"type":"number"},"ok":{"type":"boolean"},"error":{"type":"string"},"degraded":{"type":"boolean"}},"required":["tenantId","monthBucket","quantity","ok","degraded"],"additionalProperties":false}},"required":["singleTenant","result"],"additionalProperties":false},{"type":"object","properties":{"singleTenant":{"type":"boolean","const":false},"summary":{"type":"object","properties":{"reported":{"type":"number"},"skipped":{"type":"number"},"failed":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"monthBucket":{"type":"string"},"quantity":{"type":"number"},"ok":{"type":"boolean"},"error":{"type":"string"},"degraded":{"type":"boolean"}},"required":["tenantId","monthBucket","quantity","ok","degraded"],"additionalProperties":false}}},"required":["reported","skipped","failed","results"],"additionalProperties":false}},"required":["singleTenant","summary"],"additionalProperties":false}]}}}}},"operationId":"postApiAdminUsageReport-stripe","tags":["admin","usage"],"parameters":[],"description":"Manually trigger Stripe usage reporting for a single tenant or all tenants. A body with `tenantId` reports just that tenant and answers 500 when the report failed; without it ALL tenants are reported and the call only answers 500 when nothing at all got through (`failed > 0` AND `reported === 0`). A partly failed bulk run therefore answers 200 — the truth is in `summary.failed`. This writes to Stripe: successful reports are billable and cannot be taken back from here.","summary":"Manually trigger Stripe usage reporting for a single tenant or all tenants","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/tenant-modules/{tenantId}":{"get":{"responses":{"200":{"description":"Modul-Liste samt aufgelöstem Tarif des Mandanten.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"plan":{"type":"string","enum":["free","starter","professional","enterprise"]},"modules":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["voice","admin","customer_portal","immo"]},"label":{"type":"string"},"minPlan":{"type":["string","null"],"enum":["free","starter","professional","enterprise",null]},"superAdminOnly":{"type":"boolean"},"planAllowed":{"type":"boolean"},"defaultEnabled":{"type":"boolean"},"override":{"type":["boolean","null"]},"effective":{"type":"boolean"}},"required":["key","label","minPlan","superAdminOnly","planAllowed","defaultEnabled","override","effective"],"additionalProperties":false}}},"required":["tenantId","plan","modules"],"additionalProperties":false},"example":{"tenantId":"string","plan":"free","modules":[{"key":"voice","label":"string","minPlan":"free","superAdminOnly":true,"planAllowed":true,"defaultEnabled":true,"override":true,"effective":true}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminTenant-modulesByTenantId","tags":["admin","modules"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true}],"description":"Gateable Module + effektiver Freischalt-Zustand für einen Mandanten. Aufgeführt sind nur die freischaltbaren Module aus der festen Registry — Kern-Module stehen dort nicht und sind immer aktiv. Je Modul kommen vier Angaben, die zusammen erklären, WARUM es an oder aus ist: `planAllowed` (lässt der Tarif es zu), `defaultEnabled` (Zustand ohne Override), `override` (ausdrückliche Setzung, `null` = keine) und `effective`. `effective` wird für einen NORMALEN Nutzer des Mandanten gerechnet — Module mit `superAdminOnly` stehen deshalb in aller Regel auf false. Ist der Mandant unbekannt oder die Datenbank nicht erreichbar, rechnet der Aufruf mit dem Tarif `free` statt zu scheitern. Nur Plattform-Superadmin.","summary":"Gateable Module + effektiver Freischalt-Zustand für einen Mandanten","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/tenant-modules/{tenantId}/{moduleKey}":{"put":{"responses":{"200":{"description":"gesetzt — der Aufruf spiegelt nur zurück, was gespeichert wurde. Er nennt NICHT den daraus folgenden effektiven Zustand; den liefert `GET /api/admin/tenant-modules/{tenantId}`.","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string"},"moduleKey":{"type":"string","enum":["voice","admin","customer_portal","immo"]},"enabled":{"type":"boolean"}},"required":["tenantId","moduleKey","enabled"],"additionalProperties":false},"example":{"tenantId":"string","moduleKey":"voice","enabled":true}}}},"400":{"description":"unbekanntes Modul"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"putApiAdminTenant-modulesByTenantIdByModuleKey","tags":["admin","modules"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"tenantId","required":true},{"schema":{"type":"string"},"in":"path","name":"moduleKey","required":true}],"description":"Modul für einen Mandanten freischalten/sperren (Override). Geschrieben wird ein Eintrag in `public.tenant_modules`, der Tarif und Standardzustand übersteuert — der Tarif selbst bleibt unberührt. Ein Modul, das die Registry nicht kennt, ergibt 400 `unknown_module`; ob der Mandant existiert, prüft der Aufruf NICHT. Scheitert das Speichern, kommt 503 und es wurde nichts gesetzt. Der Override wirkt nicht gegen `superAdminOnly`: ein so markiertes Modul bleibt für normale Nutzer aus, auch mit `enabled: true`. Nur Plattform-Superadmin.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]},"example":{"enabled":true}}}},"summary":"Modul für einen Mandanten freischalten/sperren (Override)","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/voice-numbers":{"get":{"responses":{"200":{"description":"Der gesamte Vorrat samt Zaehlern","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"e164":{"type":"string"},"tenant_id":{"type":["string","null"]},"label":{"type":["string","null"]},"active":{"type":"boolean"},"assigned_at":{"type":["string","null"]}},"required":["id","e164","tenant_id","label","active","assigned_at"],"additionalProperties":false}},"frei":{"type":"number"},"zugeteilt":{"type":"number"},"total":{"type":"number"}},"required":["data","frei","zugeteilt","total"],"additionalProperties":false},"example":{"data":[{"id":"string","e164":"string","tenant_id":"string","label":"string","active":true,"assigned_at":"string"}],"frei":0,"zugeteilt":0,"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"}},"operationId":"getApiAdminVoice-numbers","tags":["admin","voice"],"parameters":[],"summary":"Nummern-Vorrat: frei und zugeteilt","description":"Liest alle Rufnummern des Vorrats aus `public.voice_phone_numbers`, die den Provider `ainemix` tragen — freie zuerst, danach die zugeteilten. `frei`, `zugeteilt` und `total` sind aus genau dieser Liste gezaehlt und nicht getrennt abgefragt. Nummern anderer Anbieter bleiben aussen vor. Ist die Datenbank nicht erreichbar, kommt eine LEERE Liste mit 200 statt eines Fehlers — leer heisst hier also nicht zwingend, dass kein Vorrat da ist."},"post":{"responses":{"201":{"description":"Aufgenommen","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"e164":{"type":"string"}},"required":["ok","e164"],"additionalProperties":false},"example":{"ok":true,"e164":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"},"422":{"description":"Ungültig — `error` nennt den Grund"}},"operationId":"postApiAdminVoice-numbers","tags":["admin","voice"],"parameters":[],"summary":"Nimmt eine bei AWS bestellte Rufnummer in den Vorrat auf","description":"Traegt eine bereits beschaffte Rufnummer in den Vorrat ein — noch ohne Mandant (201). Die Nummer muss E.164 sein (`+49…`). Hier wird NICHTS bei AWS bestellt: der Aufruf bildet nur ab, was dort schon existiert. Schlaegt das Eintragen fehl, antwortet die Route mit 422 und nennt den Grund im Feld `error`.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,15}$"},"label":{"type":"string","maxLength":120}},"required":["e164"]}}}}}},"/api/admin/voice-numbers/assign":{"post":{"responses":{"200":{"description":"Zugeteilt. `e164` ist die Nummer, die der Mandant jetzt hat — bei der idempotenten Wiederholung also seine bereits vorhandene.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"e164":{"type":"string"}},"required":["ok","tenantId","e164"],"additionalProperties":false},"example":{"ok":true,"tenantId":"string","e164":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"},"409":{"description":"Keine freie Nummer"},"422":{"description":"Zuteilung fehlgeschlagen — `error` nennt den Grund"}},"operationId":"postApiAdminVoice-numbersAssign","tags":["admin","voice"],"parameters":[],"description":"Teilt einem Mandanten eine Nummer zu. Ohne e164 wird die älteste freie genommen. Idempotent: hat der Mandant schon eine, wird diese zurückgegeben.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string","minLength":1},"e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,15}$"}},"required":["tenantId"]},"example":{"tenantId":"string"}}}},"summary":"Teilt einem Mandanten eine Nummer zu","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/voice-numbers/release":{"post":{"responses":{"200":{"description":"Freigegeben. Der Handler prueft NICHT, ob es die Nummer gab — eine unbekannte Rufnummer wird ebenso mit `ok: true` quittiert.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false},"example":{"ok":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"},"422":{"description":"Freigabe fehlgeschlagen — `error` nennt den Grund"}},"operationId":"postApiAdminVoice-numbersRelease","tags":["admin","voice"],"parameters":[],"summary":"Gibt eine Nummer zurück in den Vorrat (z. B. nach Kündigung)","description":"Setzt in `public.voice_phone_numbers` fuer die genannte Rufnummer `tenant_id` und `assigned_at` auf NULL — die Nummer steht damit wieder im Vorrat und wird beim naechsten `/assign` ohne `e164` wieder vergeben. Die Zeile selbst bleibt erhalten und behaelt `active`; geloescht wird nichts.\n\nDie Nebenwirkung reicht weiter als der Datensatz: der bisherige Mandant hat danach keine eigene Absendernummer mehr, ausgehende Anrufe fallen auf die globale Vorgabenummer zurueck.\n\nDer Handler prueft NICHT, ob es die Nummer ueberhaupt gab oder ob sie zugeteilt war: eine unbekannte Rufnummer trifft null Zeilen und wird trotzdem mit 200 und `ok: true` quittiert. 422 kommt nur, wenn das Schreiben selbst scheitert (auch ohne Datenbankverbindung — `error: db_unavailable`). Nur fuer Plattform-Admins.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,15}$"}},"required":["e164"]}}}}}},"/api/admin/voice-numbers/requests":{"get":{"responses":{"200":{"description":"Die passenden Anfragen","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"provider_pref":{"type":["string","null"]},"area_code":{"type":["string","null"]},"number_type":{"type":["string","null"]},"note":{"type":["string","null"]},"status":{"type":"string"},"assigned_e164":{"type":["string","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","tenant_id","provider_pref","area_code","number_type","note","status","assigned_e164","created_by","created_at","updated_at"],"additionalProperties":false}},"total":{"type":"number"}},"required":["data","total"],"additionalProperties":false},"example":{"data":[{"id":"string","tenant_id":"string","provider_pref":"string","area_code":"string","number_type":"string","note":"string","status":"string","assigned_e164":"string","created_by":"string","created_at":"string","updated_at":"string"}],"total":0}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"}},"operationId":"getApiAdminVoice-numbersRequests","tags":["admin","voice"],"parameters":[],"summary":"Offene Nummern-Anfragen aller Mandanten (?status=all für alle)","description":"Zeigt die Nummern-Anfragen ALLER Mandanten aus `public.voice_number_requests`, aelteste zuerst. Ohne `status` sind nur die offenen dabei (`pending`); `?status=all` liefert alle, dann neueste zuerst, und jeder andere Wert filtert genau darauf. `total` ist die Laenge der gelieferten Liste. Ist die Datenbank nicht erreichbar, kommt eine LEERE Liste mit 200 statt eines Fehlers."}},"/api/admin/voice-numbers/requests/{id}/fulfil":{"post":{"responses":{"200":{"description":"Erledigt: die Nummer ist im Vorrat, dem anfragenden Mandanten zugeteilt und die Anfrage steht auf `provisioned`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"tenantId":{"type":"string"},"e164":{"type":"string"}},"required":["ok","tenantId","e164"],"additionalProperties":false},"example":{"ok":true,"tenantId":"string","e164":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur Plattform-Admins"},"422":{"description":"Fehlgeschlagen — `error` nennt den Grund, etwa `anfrage_unbekannt` oder `bereits_erledigt`."}},"operationId":"postApiAdminVoice-numbersRequestsByIdFulfil","tags":["admin","voice"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"summary":"Teilt eine beschaffte Rufnummer dem anfragenden Mandanten zu","description":"Erledigt eine Anfrage: beschaffte Nummer dem anfragenden Mandanten zuteilen und die Anfrage auf provisioned setzen.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,15}$"},"label":{"type":"string","maxLength":120}},"required":["e164"]}}}}}},"/api/admin/ai-monitoring/usage":{"get":{"responses":{"200":{"description":"Summen und Zeitreihe. `degraded: true` heisst: nichts gelesen, nicht „nichts da\".","content":{"application/json":{"schema":{"type":"object","properties":{"period":{"type":"string","enum":["day","week","month"],"description":"Der ausgewertete Zeitraum, zurueckgespiegelt"},"totalInputTokens":{"type":"number"},"totalOutputTokens":{"type":"number"},"totalEur":{"type":"number","description":"Auf zwei Nachkommastellen gerundet"},"series":{"type":"array","items":{"type":"object","properties":{"bucket":{"type":"string","description":"Beginn des Zeitschritts als Zeitstempel"},"inputTokens":{"type":"number"},"outputTokens":{"type":"number"},"cacheTokens":{"type":"number","description":"Nur aus dem alten Kassenbuch — alles ausser input/output"},"eur":{"type":"number"}},"required":["bucket","inputTokens","outputTokens","cacheTokens","eur"]},"description":"Aufsteigend nach Zeitschritt; Schritte ohne Verbrauch fehlen ganz"},"degraded":{"type":"boolean","const":true}},"required":["period","totalInputTokens","totalOutputTokens","totalEur","series"]},"example":{"period":"day","totalInputTokens":0,"totalOutputTokens":0,"totalEur":0,"series":[{"bucket":"string","inputTokens":0,"outputTokens":0,"cacheTokens":0,"eur":0}],"degraded":true}}}},"400":{"description":"Unbekannter `period`-Wert."},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`)."}},"operationId":"getApiAdminAi-monitoringUsage","tags":["admin","ai"],"parameters":[],"summary":"Aggregate AI token usage + EUR cost over a time period","description":"Fasst Token und Kosten ueber einen Zeitraum zusammen und liefert dazu eine Zeitreihe, gestuft nach Tag, Woche oder Monat — ueber ALLE Mandanten hinweg, nicht je Mandant.\n\nGelesen werden BEIDE Kassenbuecher: `public.cost_events` (nur Ereignisse `anthropic.%`, Betrag schon in EUR-Cent) und `public.ai_cost_events` (Betrag in USD, einmal umgerechnet). Ein Zeitpunkt kann also Betraege aus beiden Buechern enthalten.\n\n`period` ist `day`, `week` oder `month`, ohne Angabe `week`; jeder andere Wert ergibt 400 mit der Liste der erlaubten.\n\nBei fehlender Datenbank ODER einem Fehler in der ersten Abfrage kommt 200 mit Nullen, leerer Reihe und `degraded: true`. Ist nur das ZWEITE Buch unlesbar, fehlt dessen Anteil still und `degraded` bleibt weg — die Zahlen sind dann zu niedrig, ohne dass es die Antwort sagt. Nur fuer Plattform-Betreiber (`super_admin`): die `/api/admin`-App ist als Ganzes so verriegelt."}},"/api/admin/ai-monitoring/top-tools":{"get":{"responses":{"200":{"description":"Die teuersten Werkzeuge mit Aufrufzahl und Kosten in Euro. `degraded: true` heisst: nichts gelesen, nicht „nichts da\".","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"calls":{"type":"integer"},"eur":{"type":"number"},"tool":{"type":"string"}},"required":["calls","eur","tool"]}},"degraded":{"type":"boolean","const":true}},"required":["items"]},"example":{"items":[{"calls":0,"eur":0,"tool":"string"}],"degraded":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."}},"operationId":"getApiAdminAi-monitoringTop-tools","tags":["admin"],"parameters":[],"summary":"Teuerste KI-Werkzeuge","description":"Rangliste der Werkzeuge nach Kosten, teuerstes zuerst — ueber ALLE Mandanten hinweg, nicht je Mandant.\n\nDie Liste liest aus ZWEI Buechern: dem alten (`metadata->>'tool'`, ersatzweise `tool_name` oder `route`) und dem neuen (`task_type`). Ein Werkzeug, das unter beiden Namen gebucht wurde, erscheint deshalb zusammengefasst — aber nur, wenn die Schluessel woertlich uebereinstimmen.\n\n`limit` ist auf 1 bis 100 begrenzt (Standard 10); Werte ausserhalb werden stillschweigend auf die Grenze gezogen, es gibt keinen 400. Ein nicht-numerischer Wert ergibt `NaN` und damit die Untergrenze 1.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt.\n\nBei fehlender Datenbank ODER einem Fehler in der Abfrage kommt 200 mit leerer Liste UND `degraded: true`. Im Erfolgsfall fehlt der Schluessel ganz — eine leere Liste ohne `degraded` heisst also wirklich „keine Aufrufe erfasst\"."}},"/api/admin/ai-monitoring/top-users":{"get":{"responses":{"200":{"description":"Die teuersten Nutzer, Kennung verkuerzt.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"calls":{"type":"integer"},"eur":{"type":"number"},"userIdHash":{"type":"string"}},"required":["calls","eur","userIdHash"]}},"degraded":{"type":"boolean","const":true}},"required":["items"]},"example":{"items":[{"calls":0,"eur":0,"userIdHash":"string"}],"degraded":true}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."}},"operationId":"getApiAdminAi-monitoringTop-users","tags":["admin"],"parameters":[],"summary":"Nutzer mit den hoechsten KI-Kosten (anonymisiert)","description":"Rangliste der Nutzer nach Kosten, ueber alle Mandanten hinweg.\n\nDIE KENNUNGEN SIND VERKUERZT und kommen als `userIdHash` heraus: die ersten acht Zeichen plus die Gesamtlaenge in Klammern, etwa `a1b2c3d4…(36)`. Das ist der Grund, warum diese Liste ueberhaupt gezeigt werden darf. Die Sammelwerte `(anonymous)` und `(unknown)` bleiben unveraendert stehen — sie sind keine Kennungen.\n\nAcht Zeichen einer UUID sind nicht garantiert eindeutig: zwei Nutzer koennen theoretisch denselben `userIdHash` tragen. Fuer eine Rangliste reicht das; als Schluessel taugt er nicht.\n\nWie bei den Werkzeugen aus zwei Buechern gelesen — neu `user_id` als eigene Spalte, alt `metadata->>'user_id'`. `limit` 1 bis 100, Standard 10.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt.\n\nBei fehlender Datenbank ODER einem Fehler in der Abfrage kommt 200 mit leerer Liste UND `degraded: true`. Im Erfolgsfall fehlt der Schluessel ganz — eine leere Liste ohne `degraded` heisst also wirklich „keine Aufrufe erfasst\"."}},"/api/admin/ai-monitoring/quota":{"get":{"responses":{"200":{"description":"Das Budget. `configured: false` heisst: Voreinstellung, keine gespeicherte Zeile — dann fehlen die drei Verwaltungsfelder.","content":{"application/json":{"schema":{"type":"object","properties":{"tenant_id":{"type":"string"},"daily_budget_eur":{"type":"number"},"alert_at_pct":{"type":"integer"},"block_at_pct":{"type":"integer"},"emergency_override":{"type":"boolean"},"configured":{"type":"boolean"},"override_until":{"type":["string","null"]},"updated_by":{"type":["string","null"]},"updated_at":{"type":"string"}},"required":["tenant_id","daily_budget_eur","alert_at_pct","block_at_pct","emergency_override","configured"]},"example":{"tenant_id":"string","daily_budget_eur":0,"alert_at_pct":0,"block_at_pct":0,"emergency_override":true,"configured":true,"override_until":"string","updated_by":"string","updated_at":"string"}}}},"400":{"description":"`tenant_id` fehlt in der Abfrage.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."},"503":{"description":"Keine Datenbankverbindung."}},"operationId":"getApiAdminAi-monitoringQuota","tags":["admin"],"parameters":[],"summary":"Tagesbudget eines Mandanten lesen","description":"Das KI-Tagesbudget des per `tenant_id` genannten Mandanten.\n\nGIBT ES KEINE ZEILE, KOMMEN ERFUNDENE WERTE — aber ehrlich beschriftet: `configured: false` sagt, dass 5 EUR am Tag, Warnung bei 80 % und Sperre bei 100 % die Voreinstellung sind und nicht die Wahl des Betreibers. Bei einer echten Zeile steht dort `true`. Wer das Feld ignoriert, haelt eine Voreinstellung fuer eine Entscheidung.\n\nIm nicht konfigurierten Fall FEHLEN ausserdem `override_until`, `updated_by` und `updated_at` — der Rumpf ist kuerzer als bei einer echten Zeile.\n\nSEIT 17.08.2026 GIBT ES KEINE FREIGABE OHNE ABLAUF MEHR: `POST /quota` setzt bei fehlender Dauer 24 Stunden. Ein `override_until: null` bei gesetztem `emergency_override` stammt daher nur noch aus Altbestand.\n\nFrueher galt: `override_until: null` bei gesetztem `emergency_override` heisst OHNE ABLAUF: die Ausnahme gilt, bis sie jemand von Hand zuruecknimmt. Das passiert, wenn beim Setzen keine Stundenzahl mitgegeben wurde.\n\nDIESE ROUTE HAT KEINEN FEHLERFANG. Anders als die beiden Ranglisten faengt sie Datenbankfehler nicht ab — ein Fehler beim Anlegen der Tabelle oder bei der Abfrage schlaegt als 500 durch.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."},"post":{"responses":{"200":{"description":"Geschrieben. Der Rumpf ist `{ \"ok\": true }`, sonst nichts.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]},"example":{"ok":true}}}},"400":{"description":"Rumpf ungueltig — `tenant_id` fehlt, ein Prozentwert liegt ausserhalb seiner Grenzen, oder `override_hours` ist keine ganze Zahl zwischen 1 und 168."},"401":{"description":"Nicht angemeldet."},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"500":{"description":"Schreibfehler in der Datenbank — ungefangen, siehe oben."},"503":{"description":"Keine Datenbankverbindung (`{ error: \"database_unavailable\" }`)."}},"operationId":"postApiAdminAi-monitoringQuota","tags":["admin"],"parameters":[],"summary":"Tagesbudget eines Mandanten setzen","description":"Schreibt das KI-Tagesbudget des per `tenant_id` genannten Mandanten nach `public.ai_quotas` — eine Zeile je Mandant, angelegt oder ueberschrieben (`INSERT … ON CONFLICT DO UPDATE`).\n\nES IST KEIN TEIL-UPDATE. Das Schema setzt fuer jedes fehlende Feld eine Voreinstellung ein, und der Upsert schreibt danach ALLE Spalten. Wer nur `tenant_id` und `daily_budget_eur` schickt, setzt damit zugleich `alert_at_pct` auf 80, `block_at_pct` auf 100 und `emergency_override` auf false zurueck — auch wenn dort vorher etwas anderes stand. Zuvor lesen (`GET /quota`) und den ganzen Satz zurueckschicken.\n\nGRENZEN: `daily_budget_eur` 0 bis 10000 (0 heisst „kein Verbrauch erlaubt\", nicht „unbegrenzt\"), `alert_at_pct` 0 bis 100, `block_at_pct` 0 bis 200 (ueber 100, um bewusst ueber das Budget hinaus laufen zu lassen).\n\nNOTFALL-FREIGABE: `emergency_override: true` hebt die Sperre auf. `override_hours` sagt fuer wie lange (1 bis 168 Stunden); FEHLT DER WERT, GELTEN 24 STUNDEN. Eine Freigabe ohne Ablauf gibt es seit dem 17.08.2026 nicht mehr, `0` ist nicht mehr erlaubt. Bei `emergency_override: false` wird `override_until` auf NULL gesetzt, eine laufende Freigabe also sofort beendet.\n\n`updated_by` traegt die Benutzer-ID des Aufrufers, `updated_at` den Zeitpunkt. Die Route gibt NUR `{ \"ok\": true }` zurueck — nicht die geschriebene Zeile; zum Nachlesen `GET /quota?tenant_id=…`.\n\nDIESE ROUTE HAT KEINEN FEHLERFANG. Wie ihr Gegenstueck `GET /quota` faengt sie Datenbankfehler nicht ab — ein Fehler beim Anlegen der Tabelle oder beim Schreiben schlaegt als 500 durch.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tenant_id":{"type":"string","minLength":1},"daily_budget_eur":{"type":"number","minimum":0,"maximum":10000},"alert_at_pct":{"type":"integer","minimum":0,"maximum":100,"default":80},"block_at_pct":{"type":"integer","minimum":0,"maximum":200,"default":100},"emergency_override":{"type":"boolean","default":false},"override_hours":{"type":"integer","minimum":1,"maximum":168,"description":"Wie lange die Notfall-Freigabe gilt, in Stunden (1 bis 168). Fehlt der Wert, gelten 24 Stunden — eine Freigabe OHNE Ablauf gibt es seit dem 17.08.2026 nicht mehr. Frueher war `0` erlaubt und bedeutete unbegrenzt."}},"required":["tenant_id","daily_budget_eur"]},"example":{"tenant_id":"string","daily_budget_eur":0,"alert_at_pct":0,"block_at_pct":0,"emergency_override":true,"override_hours":1}}}}}},"/api/admin/ai-monitoring/pricing":{"get":{"responses":{"200":{"description":"Alle hinterlegten Modelle plus die Preise des heutigen Standardmodells (`claude-sonnet-4-6`).","content":{"application/json":{"schema":{"type":"object","properties":{"models":{"type":"array","items":{"type":"object","additionalProperties":{}}},"current_default":{}},"required":["models"]},"example":{"models":[{}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."}},"operationId":"getApiAdminAi-monitoringPricing","tags":["admin"],"parameters":[],"summary":"Hinterlegte Modellpreise","description":"Die Preistabelle, mit der alle Euro-Betraege dieser Oberflaeche gerechnet werden — je Modell die Kosten fuer Ein- und Ausgabe.\n\nDIE WERTE STEHEN IM QUELLTEXT, nicht in der Datenbank und nicht beim Anbieter. Sie sind der Stand bei der letzten Aenderung dieser Tabelle; aendert Anthropic seine Preise, aendert sich hier nichts von selbst. Deshalb ist diese Route der ehrlichste Ort, um zu pruefen, womit die Kostenzahlen ueberhaupt zustande kommen.\n\nReine Auskunft, keine Datenbank — antwortet immer mit 200.\n\nNur fuer Plattform-Betreiber (`super_admin`) — die `/api/admin`-App ist als Ganzes so verriegelt."}},"/api/admin/ai-audit":{"get":{"responses":{"200":{"description":"Die gefundenen Ereignisse. Steht `note` dabei, ist die Liste leer, WEIL die Tabelle fehlt — nicht, weil es nichts gab.","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","additionalProperties":{}}},"total":{"type":"integer"},"note":{"type":"string"}},"required":["events","total"]},"example":{"events":[{}],"total":0,"note":"string"}}}},"400":{"description":"Abfrageparameter ungueltig (z. B. `limit` groesser als 1000)."},"401":{"description":"Nicht angemeldet."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"503":{"description":"Keine Datenbankverbindung (`{ error: \"DB not available\" }`)."}},"operationId":"getApiAdminAi-audit","tags":["admin","ai"],"parameters":[{"in":"query","name":"userId","schema":{"type":"string"}},{"in":"query","name":"from","schema":{"type":"string"}},{"in":"query","name":"to","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"number","minimum":1,"maximum":1000,"default":100}},{"in":"query","name":"tenantId","schema":{"type":"string"}}],"summary":"KI-Protokoll ueber alle Mandanten lesen","description":"Liest das KI-Aufrufprotokoll aus `public.ai_cost_events` — je Zeile ein Modellaufruf mit Mandant, Benutzer, Modell, Token-Zahlen, Kosten und Zeitpunkt.\n\nOHNE FILTER LIEST DIESE ROUTE UEBER ALLE MANDANTEN HINWEG. `tenantId` grenzt auf einen ein, `userId` auf einen Benutzer, `from`/`to` auf einen Zeitraum (ISO-Zeitstempel; `from` ist einschliesslich, `to` ausschliesslich). `limit` begrenzt auf 1 bis 1000 Zeilen, Voreinstellung 100 — es gibt KEINE Blaetterung und keine Gesamtzahl: `total` zaehlt nur die zurueckgegebenen Zeilen, nicht die vorhandenen.\n\nDIE FELDER KOMMEN ROH AUS DER DATENBANK, also in Unterstrich-Schreibweise (`tenant_id`, `input_tokens`, `cost_usd`, `created_at`). Die Mandanten-Route `/api/v1/tenant/ai/audit` benennt dieselben Werte in camelCase um — wer beide anspricht, bekommt zwei Formen.\n\n`cost_usd` kommt als Zeichenkette (NUMERIC), nicht als Zahl.\n\nACHTUNG, EINE LEERE LISTE HEISST ZWEIERLEI: entweder es gibt keine Ereignisse, oder `public.ai_cost_events` fehlt. Der Handler faengt jeden Abfragefehler ab und antwortet trotzdem mit 200 und leerer Liste — unterscheidbar allein am zusaetzlichen Feld `note`. Nur eine fehlende Datenbankverbindung ergibt 503.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429). Ein Mandanten-Admin kommt hier NICHT durch."}},"/api/admin/ai-audit/right-to-erasure":{"post":{"responses":{"200":{"description":"Geloescht. `tables` nennt die beiden geleerten Tabellen, `note` erinnert an die CloudWatch-/S3-Protokolle.","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"erased":{"type":"boolean"},"tables":{"type":"array","items":{"type":"string"}},"timestamp":{"type":"string"},"note":{"type":"string"}},"required":["userId","erased","tables","timestamp","note"]},"example":{"userId":"string","erased":true,"tables":["string"],"timestamp":"string","note":"string"}}}},"400":{"description":"Validierungsfehler: `userId` fehlt, oder `reason` ist laenger als 500 Zeichen. Der frueher hier beschriebene fehlende Mandantenkontext ist seit dem 30.08.2026 kein Fall mehr."},"401":{"description":"Nicht angemeldet."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"404":{"description":"Der Benutzer gehoert nicht zu diesem Mandanten — es wurde nichts geloescht (`{ \"error\": \"User not found in this tenant\" }`)."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"500":{"description":"Loeschung fehlgeschlagen; die Transaktion ist zurueckgerollt, es ist nichts halb geloescht (`{ \"error\": \"Erasure failed — tables may not exist yet\" }`)."},"503":{"description":"Keine Datenbankverbindung (`{ error: \"DB not available\" }`)."}},"operationId":"postApiAdminAi-auditRight-to-erasure","tags":["admin","ai"],"parameters":[],"summary":"Loeschrecht DSGVO Art. 17 — KI-Protokoll eines Benutzers loeschen","description":"Loescht das KI-Protokoll EINES Benutzers endgueltig.\n\nWAS GELOESCHT WIRD — zwei Tabellen, beide vollstaendig fuer diesen Benutzer, ohne Mengenbegrenzung:\n- `public.ai_cost_events` — jeder Modellaufruf (Token, Kosten, Zeitpunkt)\n- `public.ai_user_feedback` — jede Rueckmeldung des Benutzers zur KI\n\nBeide Loeschungen laufen in EINER Transaktion (`sql.begin`): entweder beide oder keine. Es gibt keine Obergrenze und keinen Stapelbetrieb — sind es hunderttausend Zeilen, gehen hunderttausend Zeilen.\n\nES IST EIN ECHTES `DELETE`, NICHT UMKEHRBAR. Kein Papierkorb, kein `deleted_at`, keine Kopie. Nach der Antwort sind die Daten fort.\n\nES GIBT KEINEN TROCKENLAUF. Das Rumpf-Schema kennt genau zwei Felder — `userId` (Pflicht) und `reason` (frei, hoechstens 500 Zeichen, wandert nur in die Server-Warnung). Ein mitgeschicktes `dryRun`/`preview` wird stillschweigend verworfen und loescht trotzdem. Wer vorher wissen will, wie viel betroffen ist, zaehlt es mit `GET /admin/ai-audit?userId=…` ab — diese Route zaehlt nicht vor.\n\nNICHT GELOESCHT WERDEN die CloudWatch-/S3-Protokolle; die muss jemand von Hand ueber die AWS-CLI entfernen. Das Antwortfeld `note` sagt es nochmals. Die Loeschung selbst wird als `console.warn` festgehalten.\n\nDER MANDANT KOMMT AUS DER SITZUNG, NICHT AUS DEM RUMPF. Beide `DELETE` sind zusaetzlich auf `tenant_id` eingegrenzt, und vorab prueft der Handler, ob der Benutzer ueberhaupt zu diesem Mandanten gehoert (sonst 404). Einen Parameter, um den Mandanten zu waehlen, gibt es bewusst nicht.\n\nBIS ZUM 30.08.2026 ANTWORTETE DIESE ROUTE IMMER MIT 400 und loeschte nie etwas: sie las `c.get('tenant')`, und den Mandantenkontext setzt allein `tenantMiddleware`, die nur an der `/api/v1`-Sub-App haengt — nicht an der admin-Sub-App. Wer sich auf die Loeschung verliess, erfuellte Art. 17 NICHT.\n\nSEITHER WIRD DER MANDANT AUS DEM BENUTZER ABGELEITET (`public.users.tenant_id`). Er kann damit weder fehlen noch vom Aufrufer gewaehlt werden — ein Mandant im Rumpf haette mandantenuebergreifendes Loeschen erlaubt. Ist zusaetzlich ein Sitzungsmandant gesetzt, MUSS er zum Benutzer passen, sonst 404.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429). Ein Mandanten-Admin kommt hier NICHT durch.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","minLength":1},"reason":{"type":"string","maxLength":500}},"required":["userId"]},"example":{"userId":"string","reason":"string"}}}}}},"/api/admin/ai-costs":{"get":{"responses":{"200":{"description":"Kosten je Mandant. `month` fehlt, wenn die Anfrage keinen Monat nannte. Steht `note` dabei, ist die Liste leer, WEIL die Tabelle fehlt.","content":{"application/json":{"schema":{"type":"object","properties":{"month":{"type":"string"},"tenants":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"requestCount":{"type":"integer"},"costUsd":{"type":"number"}},"required":["tenantId","requestCount","costUsd"]}},"note":{"type":"string"}},"required":["tenants"]},"example":{"month":"string","tenants":[{"tenantId":"string","requestCount":0,"costUsd":0}],"note":"string"}}}},"400":{"description":"`month` passt nicht auf `JJJJ-MM`."},"401":{"description":"Nicht angemeldet."},"403":{"description":"Angemeldet, aber kein `super_admin`."},"429":{"description":"Mehr als 120 Anfragen je Minute an die admin-Sub-App."},"503":{"description":"Keine Datenbankverbindung (`{ error: \"DB not available\" }`)."}},"operationId":"getApiAdminAi-costs","tags":["admin","ai","costs"],"parameters":[{"in":"query","name":"month","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$"}},{"in":"query","name":"tenantId","schema":{"type":"string"}}],"summary":"KI-Kosten je Mandant fuer einen Monat","description":"Summiert `public.ai_cost_events` eines Kalendermonats und gibt je Mandant die Zahl der Aufrufe und die Kosten zurueck, absteigend nach Kosten.\n\n`month` waehlt den Monat im Format `JJJJ-MM`; ohne Angabe rechnet die Route den laufenden Monat (UTC). `tenantId` grenzt auf einen Mandanten ein — ohne das Feld sind ALLE Mandanten dabei.\n\nHOECHSTENS 200 MANDANTEN. Die Abfrage endet auf `LIMIT 200`; es gibt keine Blaetterung und keinen Hinweis darauf, dass abgeschnitten wurde. Bei mehr als 200 Mandanten mit Verbrauch fehlen die guenstigsten stillschweigend.\n\nDAS FELD `month` IM RUMPF SPIEGELT NUR DIE ANFRAGE. Es traegt den uebergebenen Wert, nicht den gerechneten Monat — wer `month` weglaesst, bekommt eine Antwort OHNE `month`, obwohl ueber den laufenden Monat gerechnet wurde. Die Mandanten-Route `/api/v1/tenant/ai/costs` macht es anders herum und gibt den gerechneten Monat zurueck.\n\nDIE BETRAEGE SIND US-DOLLAR. Die Spalte heisst `cost_usd`, das Feld `costUsd`; auf vier Nachkommastellen gerundet. Nicht mit den Euro-Betraegen der KI-Ueberwachung (`/admin/ai-monitoring/*`) verwechseln.\n\nEINE LEERE LISTE HEISST ZWEIERLEI: keine Kosten, oder `public.ai_cost_events` fehlt. Der Handler faengt jeden Abfragefehler ab und antwortet trotzdem mit 200 — unterscheidbar allein am zusaetzlichen Feld `note`. Nur eine fehlende Datenbankverbindung ergibt 503.\n\nNur fuer Plattform-Betreiber (`super_admin`). Die admin-Sub-App liegt hinter `authMiddleware` (ohne Anmeldung 401) und `requireSuperAdmin` (angemeldet, aber keine Plattform-Rolle: 403 mit `code: SUPER_ADMIN_REQUIRED`) und ist auf 120 Anfragen je Minute begrenzt (danach 429)."}},"/api/admin/flags":{"get":{"responses":{"200":{"description":"Flags list — the raw rows, in store order.","content":{"application/json":{"schema":{"type":"object","properties":{"flags":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"rolloutPercent":{"type":"number"},"targetingRules":{"type":"array","items":{"type":"object","properties":{"attribute":{"type":"string","enum":["plan","industry","region","tenantId","userId"]},"operator":{"type":"string","enum":["equals","in","notIn","startsWith","contains"]},"values":{"type":"array","items":{"type":"string"}},"result":{"type":"boolean"}},"required":["attribute","operator","values"],"additionalProperties":false}},"updatedAt":{"type":"string"}},"required":["name","enabled","rolloutPercent","targetingRules","updatedAt"],"additionalProperties":false}}},"required":["flags"],"additionalProperties":false},"example":{"flags":[{"name":"string","enabled":true,"rolloutPercent":0,"targetingRules":[{"attribute":"plan","operator":"equals","values":["string"],"result":true}],"updatedAt":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin role required"}},"operationId":"getApiAdminFlags","tags":["flags","admin"],"parameters":[],"description":"List all feature flags (admin only). Returns the stored flag rows — name, master switch, rollout percentage, targeting rules and last change — NOT the decisions for any tenant; for those use `GET /api/v1/flags/eval`. The list is global, not scoped to a tenant, and comes without paging or filters. The role check is a step ladder, so `super_admin` passes it too.","summary":"List all feature flags (admin only)","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"200":{"description":"Flag upserted — the stored row as it now stands, create and update alike.","content":{"application/json":{"schema":{"type":"object","properties":{"flag":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"rolloutPercent":{"type":"number"},"targetingRules":{"type":"array","items":{"type":"object","properties":{"attribute":{"type":"string","enum":["plan","industry","region","tenantId","userId"]},"operator":{"type":"string","enum":["equals","in","notIn","startsWith","contains"]},"values":{"type":"array","items":{"type":"string"}},"result":{"type":"boolean"}},"required":["attribute","operator","values"],"additionalProperties":false}},"updatedAt":{"type":"string"}},"required":["name","enabled","rolloutPercent","targetingRules","updatedAt"],"additionalProperties":false}},"required":["flag"],"additionalProperties":false},"example":{"flag":{"name":"string","enabled":true,"rolloutPercent":0,"targetingRules":[{"attribute":"plan","operator":"equals","values":["string"],"result":true}],"updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin role required"},"422":{"description":"Invalid body"}},"operationId":"postApiAdminFlags","tags":["flags","admin"],"parameters":[],"description":"Upsert a feature flag (admin only). This is a FULL write, not a patch: `name` is the only required field, and every field left out is reset — a missing `enabled` stores `false`, a missing `rolloutPercent` stores 0, and missing or malformed `targetingRules` store an empty list. Rules that lack a string `attribute`/`operator` or an array `values` are dropped silently, so a typo costs you the rule without an error. Answers 200 on both create and update — there is no 201.","summary":"Upsert a feature flag (admin only)","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/flags/{name}":{"delete":{"responses":{"200":{"description":"Deleted — the requested name, echoed back. Nothing else.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"string"}},"required":["deleted"],"additionalProperties":false},"example":{"deleted":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Admin role required"}},"operationId":"deleteApiAdminFlagsByName","tags":["flags","admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"description":"Delete a feature flag by name (admin only). The row is removed from the store; there is no soft delete and no way back. The call answers 200 even when no flag by that name existed — it never returns 404, so the echoed name is not proof that anything was removed. Afterwards `GET /api/v1/flags/eval` reports the name as `value: false` with reason `unknown`, and subscribers of the SSE stream receive a `flag.delete` event.","summary":"Delete a feature flag by name (admin only)","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/migrations":{"get":{"responses":{"200":{"description":"Die Migrationen mit Einstufung — ODER, bei gesetztem `error`, ein FEHLSCHLAG mit leerer Liste.","content":{"application/json":{"schema":{"type":"object","properties":{"migrations":{"type":"array","items":{"type":"object","properties":{"safetyClass":{"type":"string"},"reasons":{"type":"array","items":{"type":"string"}},"affectedTables":{"type":"array","items":{"type":"string"}},"requiresApproval":{"type":"boolean"},"version":{"type":"string"},"description":{"type":"string"},"sqlPreview":{"type":"string"},"status":{"type":"string"},"appliedAt":{"type":["string","null"]}},"required":["safetyClass","reasons","affectedTables","requiresApproval","version","description","sqlPreview","status","appliedAt"]}},"error":{"type":"string"}},"required":["migrations"]},"example":{"migrations":[{"safetyClass":"string","reasons":["string"],"affectedTables":["string"],"requiresApproval":true,"version":"string","description":"string","sqlPreview":"string","status":"string","appliedAt":"string"}],"error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"operationId":"getApiAdminMigrations","tags":["admin"],"parameters":[],"summary":"Alle Migrationen mit Sicherheitseinstufung auflisten","description":"Listet jede bekannte Migration mit ihrer Einstufung: wie riskant sie ist, welche Tabellen sie anfasst, ob sie eine Freigabe braucht, und ob sie schon angewandt wurde.\n\nDIESE LESEROUTE SCHREIBT. Sie traegt die errechnete Einstufung fuer JEDE Migration in `public.schema_migrations_meta` nach — ein Upsert je Zeile, in einer Schleife. Ein GET mit Nebenwirkung; bei vielen Migrationen sind das entsprechend viele Schreibvorgaenge pro Aufruf.\n\nDIE SQL WIRD NICHT AUSGEFUEHRT, um sie einzustufen: die Migration laeuft gegen einen mitschreibenden Stellvertreter, der die Anweisungen nur einsammelt. Keine Datenbank wird dabei angefasst.\n\n`sqlPreview` ist auf 300 Zeichen gekuerzt und endet dann mit `…` — es ist eine Vorschau, keine vollstaendige Anweisung.\n\nEIN FEHLER KOMMT HIER ALS **200** ZURUECK, nicht als 500: die Antwort traegt dann eine leere Liste UND einen `error`-Schluessel. Wer nur auf den Statuscode sieht, haelt einen Fehlschlag fuer „keine Migrationen vorhanden\". Das Feld ist der einzige Unterschied.\n\nNur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"/api/admin/migrations/analyze":{"post":{"responses":{"200":{"description":"Der Befund: Einstufung, Begruendungen, betroffene Tabellen und ob eine Freigabe noetig waere.","content":{"application/json":{"schema":{"type":"object","properties":{"safetyClass":{"type":"string"},"reasons":{"type":"array","items":{"type":"string"}},"affectedTables":{"type":"array","items":{"type":"string"}},"requiresApproval":{"type":"boolean"}},"required":["safetyClass","reasons","affectedTables","requiresApproval"]},"example":{"safetyClass":"string","reasons":["string"],"affectedTables":["string"],"requiresApproval":true}}}},"400":{"description":"`sql` oder `version` fehlt.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"operationId":"postApiAdminMigrationsAnalyze","tags":["admin"],"parameters":[],"summary":"Beliebige SQL auf Risiko pruefen","description":"Stuft eine mitgeschickte SQL-Anweisung ein, ohne sie auszufuehren und ohne sie irgendwo zu hinterlegen. Gedacht, um eine geplante Migration vorab zu beurteilen.\n\nDer Rumpf braucht `sql` und `version`; fehlt eines, gibt es 400. Ein RUMPF, DER KEIN JSON IST, ergibt keinen 400, sondern einen 500 — er wird ungeschuetzt gelesen. Ein Eingabeschema gibt es nicht.\n\n`version` dient nur der Beschriftung des Befunds; es wird nicht gegen die bekannten Migrationen geprueft. Ein erfundener Name ist erlaubt.\n\nDie Antwort ist der Befund SELBST, nicht in einen Umschlag gepackt.\n\nReine Auskunft: nichts wird ausgefuehrt, nichts gespeichert.\n\nNur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"/api/admin/migrations/{version}/approve":{"post":{"responses":{"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Nur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."},"501":{"description":"Immer. `ok: false`, `error: \"not_implemented\"`, dazu die Kennung und eine Begruendung im Klartext.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","const":"not_implemented"},"version":{"type":"string"},"message":{"type":"string"}},"required":["ok","error","version","message"]}}}}},"operationId":"postApiAdminMigrationsByVersionApprove","tags":["admin"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"version","required":true}],"summary":"Freigabe einer Migration — NICHT gebaut, antwortet 501","description":"DIESE ROUTE GIBT NICHTS FREI. Sie antwortet immer **501** mit `error: \"not_implemented\"`, weil es keine Ablage fuer Migrations-Freigaben gibt.\n\nDas ist eine bewusste Entscheidung und die Beschreibung sagt warum: bis 07.08.2026 lieferte sie `{ approved: true, approvedBy, approvedAt }` — und hinterlegte nichts davon. Der naechste Aufruf, der nach Freigaben sah, fand nichts; wer freigegeben hatte, war nicht feststellbar.\n\nBei einer Migrations-Freigabe ist das die teuerste Sorte Attrappe: sie erzeugt genau das Vier-Augen-Gefuehl, das ein Freigabeschritt geben soll, ohne dass zwei Augen nachweisbar hingesehen haetten. Ein 501 ist hier besser als ein `true`, weil es den fehlenden Schritt SICHTBAR macht, statt ihn zu ersetzen.\n\nWer eine Freigabe braucht, kann sie ueber diese API also nicht erteilen — und soll das auch nicht glauben. `version` kommt in der Antwort zurueck, damit ein Aufrufer sieht, worauf sich die Absage bezieht.\n\nNur fuer Plattform-Betreiber (`super_admin`); zusaetzlich `admin` im Modul."}},"/api/admin/rollouts/analyze":{"post":{"responses":{"200":{"description":"Analysis result. Also the answer when the database is unreachable or the analysis throws — the report then carries only the common fields and a recommendation to check by hand. There is no error status on this route.","content":{"application/json":{"schema":{"type":"object","properties":{"compatible":{"type":"boolean","description":"False when at least one tenant needs manual migration"},"breakingChanges":{"type":"array","items":{"type":"string"},"description":"One \"<tenantId>: <description>\" line per breaking change"},"affectedTenants":{"type":"integer"},"recommendation":{"type":"string"},"aiSummary":{"type":"string"},"incomingBaseVersion":{"type":"string"},"currentBaseVersion":{"type":"string"},"totalTenants":{"type":"integer"},"safeTenants":{"type":"integer"},"autoMigratableCount":{"type":"integer"},"requiresManualInterventionCount":{"type":"integer"},"tenantBreakdown":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"kundeLayerId":{"type":"string"},"breakingCount":{"type":"integer"},"deprecatedCount":{"type":"integer"},"autoMigratable":{"type":"boolean"},"breaking":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["field_removed","field_type_changed","required_field_added","api_path_changed","deprecated_field"]},"path":{"type":"string","description":"Dot-path of the affected field, e.g. \"customers.kunde_credit_limit\""},"affectedContributionIds":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"previousType":{"type":"string"},"newType":{"type":"string"}},"required":["kind","path","affectedContributionIds","description"]}}},"required":["tenantId","kundeLayerId","breakingCount","deprecatedCount","autoMigratable","breaking"]},"description":"Only the tenants whose report is not safe"}},"required":["compatible","breakingChanges","affectedTenants","recommendation","aiSummary"]},"example":{"compatible":true,"breakingChanges":["string"],"affectedTenants":0,"recommendation":"string","aiSummary":"string","incomingBaseVersion":"string","currentBaseVersion":"string","totalTenants":0,"safeTenants":0,"autoMigratableCount":0,"requiresManualInterventionCount":0,"tenantBreakdown":[{"tenantId":"string","kundeLayerId":"string","breakingCount":0,"deprecatedCount":0,"autoMigratable":true,"breaking":[{"kind":"field_removed","path":"string","affectedContributionIds":["string"],"description":"string","previousType":"string","newType":"string"}]}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"postApiAdminRolloutsAnalyze","tags":["admin","rollouts"],"parameters":[],"summary":"Analyses breaking changes against every active tenant before a rollout","description":"Pre-rollout compatibility analysis: runs the layer-engine update-checker against every active tenant and aggregates the breaking changes."}},"/api/admin/rollouts":{"get":{"responses":{"200":{"description":"Liste der Rollouts samt Hinweis zur Fluechtigkeit","content":{"application/json":{"schema":{"type":"object","properties":{"rollouts":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Kennung des Rollouts — zugleich der Name des verknuepften Feature-Flags"},"stages":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Name der Stufe, z. B. \"staging\" oder \"50pct\""},"percent":{"type":"number","description":"Anteil in Prozent, 0..100"}},"required":["label","percent"]},"description":"Stufenbahn, aufsteigend; erste 0 %, letzte 100 %"},"currentStage":{"type":"number","description":"0-basierter Index in `stages`"},"automaticPromotion":{"type":"boolean","description":"Darf die Engine selbst weiterschalten?"},"criteria":{"type":"object","additionalProperties":{},"description":"Bedingungen fuer automatisches Weiterschalten"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["name","stages","currentStage","automaticPromotion","criteria"]}},"speicherFluechtig":{"type":"boolean","const":true,"description":"Solange gesetzt: der Zustand liegt nur im Arbeitsspeicher und ist nach einem Neustart weg"},"hinweis":{"type":"string","description":"Klartext-Erklaerung der Fluechtigkeit fuer die Oberflaeche"}},"required":["rollouts","speicherFluechtig","hinweis"]},"example":{"rollouts":[{"name":"string","stages":[{"label":"string","percent":0}],"currentStage":0,"automaticPromotion":true,"criteria":{},"createdAt":"string","updatedAt":"string"}],"speicherFluechtig":true,"hinweis":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Adminrolle noetig"}},"operationId":"getApiAdminRollouts","tags":["admin","rollouts"],"parameters":[],"description":"Listet alle Rollouts. ACHTUNG: der Zustand liegt nur im Arbeitsspeicher — die Antwort sagt das ueber `speicherFluechtig` und `hinweis`.","summary":"Listet alle Rollouts","x-nemix-summary-source":"description:first-sentence"},"post":{"responses":{"201":{"description":"Angelegt — startet in Staging (0 %)","content":{"application/json":{"schema":{"type":"object","properties":{"rollout":{"type":"object","properties":{"name":{"type":"string","description":"Kennung des Rollouts — zugleich der Name des verknuepften Feature-Flags"},"stages":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Name der Stufe, z. B. \"staging\" oder \"50pct\""},"percent":{"type":"number","description":"Anteil in Prozent, 0..100"}},"required":["label","percent"]},"description":"Stufenbahn, aufsteigend; erste 0 %, letzte 100 %"},"currentStage":{"type":"number","description":"0-basierter Index in `stages`"},"automaticPromotion":{"type":"boolean","description":"Darf die Engine selbst weiterschalten?"},"criteria":{"type":"object","additionalProperties":{},"description":"Bedingungen fuer automatisches Weiterschalten"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["name","stages","currentStage","automaticPromotion","criteria"]}},"required":["rollout"]},"example":{"rollout":{"name":"string","stages":[{"label":"string","percent":0}],"currentStage":0,"automaticPromotion":true,"criteria":{},"createdAt":"string","updatedAt":"string"}}}}},"400":{"description":"Ungueltige Eingabe oder nicht unterstuetzter Termin","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Maschinenlesbare Kennung"},"hinweis":{"type":"string","description":"Deutsche Erklaerung fuer die Oberflaeche"},"details":{"type":"array","items":{"type":"string"}}},"required":["error"]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Adminrolle noetig"}},"operationId":"postApiAdminRollouts","tags":["admin","rollouts"],"parameters":[],"description":"Legt einen Rollout an (oder ersetzt ihn). Erwartet PROZENTSTUFEN, z. B. [10,50,100]; die Staging-Stufe (0 %) wird ergaenzt. Ein `scheduledAt` wird ABGELEHNT: zeitgesteuerte Rollouts sind nicht gebaut, es gibt keinen Planer.","summary":"Legt einen Rollout an (oder ersetzt ihn)","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/rollouts/{name}/promote":{"post":{"responses":{"200":{"description":"Neuer Stand des Rollouts","content":{"application/json":{"schema":{"type":"object","properties":{"rollout":{"type":"object","properties":{"name":{"type":"string","description":"Kennung des Rollouts — zugleich der Name des verknuepften Feature-Flags"},"stages":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Name der Stufe, z. B. \"staging\" oder \"50pct\""},"percent":{"type":"number","description":"Anteil in Prozent, 0..100"}},"required":["label","percent"]},"description":"Stufenbahn, aufsteigend; erste 0 %, letzte 100 %"},"currentStage":{"type":"number","description":"0-basierter Index in `stages`"},"automaticPromotion":{"type":"boolean","description":"Darf die Engine selbst weiterschalten?"},"criteria":{"type":"object","additionalProperties":{},"description":"Bedingungen fuer automatisches Weiterschalten"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["name","stages","currentStage","automaticPromotion","criteria"]}},"required":["rollout"]},"example":{"rollout":{"name":"string","stages":[{"label":"string","percent":0}],"currentStage":0,"automaticPromotion":true,"criteria":{},"createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Adminrolle noetig"},"404":{"description":"Rollout unbekannt"},"409":{"description":"Letzte Stufe erreicht"}},"operationId":"postApiAdminRolloutsByNamePromote","tags":["admin","rollouts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"summary":"Schaltet einen Rollout eine Stufe weiter","description":"Schaltet den Rollout eine Stufe weiter und spiegelt den neuen Prozentsatz ins Flag."}},"/api/admin/rollouts/{name}/revert":{"post":{"responses":{"200":{"description":"Neuer Stand des Rollouts","content":{"application/json":{"schema":{"type":"object","properties":{"rollout":{"type":"object","properties":{"name":{"type":"string","description":"Kennung des Rollouts — zugleich der Name des verknuepften Feature-Flags"},"stages":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Name der Stufe, z. B. \"staging\" oder \"50pct\""},"percent":{"type":"number","description":"Anteil in Prozent, 0..100"}},"required":["label","percent"]},"description":"Stufenbahn, aufsteigend; erste 0 %, letzte 100 %"},"currentStage":{"type":"number","description":"0-basierter Index in `stages`"},"automaticPromotion":{"type":"boolean","description":"Darf die Engine selbst weiterschalten?"},"criteria":{"type":"object","additionalProperties":{},"description":"Bedingungen fuer automatisches Weiterschalten"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["name","stages","currentStage","automaticPromotion","criteria"]}},"required":["rollout"]},"example":{"rollout":{"name":"string","stages":[{"label":"string","percent":0}],"currentStage":0,"automaticPromotion":true,"criteria":{},"createdAt":"string","updatedAt":"string"}}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Adminrolle noetig"},"404":{"description":"Rollout unbekannt"},"409":{"description":"Bereits in Staging"}},"operationId":"postApiAdminRolloutsByNameRevert","tags":["admin","rollouts"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"description":"Nimmt den Rollout eine Stufe zurueck. Unterhalb von Staging (0) wird abgelehnt.","summary":"Nimmt den Rollout eine Stufe zurueck","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/_internal/otel-test":{"get":{"responses":{"200":{"description":"Zustand der Ablaufverfolgung; Form je nach Fall.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"enabled":{"type":"boolean","const":false},"traceId":{"type":"null"},"message":{"type":"string","description":"Gesetzt, wenn die Ausfuhr gar nicht konfiguriert ist."},"error":{"type":"string","description":"Gesetzt, wenn das Paket fehlt."}},"required":["enabled","traceId"]},{"type":"object","properties":{"enabled":{"type":"boolean","const":true},"traceId":{"type":"string","description":"Zum Nachschlagen im Trace-Betrachter."},"spanCount":{"type":"integer","description":"Erzeugte Spannen; hier immer 3."},"lookup":{"type":["string","null"],"description":"Fertige Suchzeile fuer Honeycomb; `null`, wenn keine Kennung zustande kam."}},"required":["enabled","traceId","spanCount","lookup"]}]},"example":{"enabled":false,"traceId":null,"message":"string","error":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdmin_internalOtel-test","tags":["Betrieb"],"parameters":[],"summary":"Ausfuhr der Ablaufverfolgung pruefen","description":"Erzeugt drei ineinanderliegende Spannen und gibt die Trace-Kennung\nzurueck, damit man sie im Betrachter (Honeycomb, Tempo, Jaeger)\nwiederfindet. Diagnosewerkzeug, keine Fachfunktion.\n\nIMMER 200, drei Faelle:\n· Ausfuhr aus  → `enabled: false`, `traceId: null`, `message`.\n· Paket fehlt  → `enabled: false`, `traceId: null`, `error`.\n· Ausfuhr an   → `enabled: true`, `traceId`, `spanCount: 3`.\n\nBewusst kein Fehlerstatus: eine Diagnoseroute, die selbst 5xx wirft,\nist im Stoerfall nutzlos. Ob die Ausfuhr laeuft, sagt `enabled`.\n\nLiegt unter `/api/admin` und ist damit als Ganzes `requireSuperAdmin`.\nDer Kopfkommentar dieser Datei nannte bis heute den falschen Pfad\n(`/api/v1/_internal/…`) — der Mount war immer der Admin-Bereich."}},"/api/admin/backendherz/overview":{"get":{"responses":{"200":{"description":"Ops overview snapshot. Auch die Antwort ohne Datenbank — dann stehen alle Zahlen auf 0 und die Ampel auf rot. Ein einzelner fehlgeschlagener Teilwert faellt auf 0 zurueck, ohne die uebrige Uebersicht zu leeren.","content":{"application/json":{"schema":{"type":"object","properties":{"tenants":{"type":"object","properties":{"total":{"type":"number"},"active":{"type":"number"},"new7d":{"type":"number"},"new30d":{"type":"number"}},"required":["total","active","new7d","new30d"]},"users":{"type":"object","properties":{"total":{"type":"number"},"verified":{"type":"number"},"active30d":{"type":"number"},"new7d":{"type":"number"}},"required":["total","verified","active30d","new7d"]},"ai":{"type":"object","properties":{"costTodayEur":{"type":"number","description":"USD-Kosten des laufenden Tages, mit festem Kurs in EUR"},"costMonthEur":{"type":"number","description":"Dasselbe fuer den laufenden Monat"},"tokensToday":{"type":"number"},"callsToday":{"type":"number"}},"required":["costTodayEur","costMonthEur","tokensToday","callsToday"]},"support":{"type":"object","properties":{"unreadChats":{"type":"number"},"tenantsScanned":{"type":"number"}},"required":["unreadChats","tenantsScanned"]},"health":{"type":"object","properties":{"db":{"type":"string","enum":["up","down","unknown"]},"redis":{"type":"string","enum":["up","down","unknown"],"description":"unknown, wenn kein Redis konfiguriert ist"},"aiProvider":{"type":"string","enum":["up","down","unknown"]},"overall":{"type":"string","enum":["green","yellow","red"],"description":"rot nur bei Datenbankausfall"}},"required":["db","redis","aiProvider","overall"]},"server":{"type":"object","properties":{"uptimeSec":{"type":"number"},"rssMb":{"type":"number"},"heapUsedMb":{"type":"number"},"heapTotalMb":{"type":"number"},"heapUsedPct":{"type":"number"},"eventLoopLagMs":{"type":"number"}},"required":["uptimeSec","rssMb","heapUsedMb","heapTotalMb","heapUsedPct","eventLoopLagMs"]},"generatedAt":{"type":"string"}},"required":["tenants","users","ai","support","health","server","generatedAt"]},"example":{"tenants":{"total":0,"active":0,"new7d":0,"new30d":0},"users":{"total":0,"verified":0,"active30d":0,"new7d":0},"ai":{"costTodayEur":0,"costMonthEur":0,"tokensToday":0,"callsToday":0},"support":{"unreadChats":0,"tenantsScanned":0},"health":{"db":"up","redis":"up","aiProvider":"up","overall":"green"},"server":{"uptimeSec":0,"rssMb":0,"heapUsedMb":0,"heapTotalMb":0,"heapUsedPct":0,"eventLoopLagMs":0},"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminBackendherzOverview","tags":["admin"],"parameters":[],"summary":"Sammeluebersicht fuer Super-Admins: Mandanten, Nutzer, KI-Kosten, Betrieb","description":"Unified super-admin ops overview: tenants, users, AI cost, support, health, server — one aggregate."}},"/api/admin/backendherz/tenants-health":{"get":{"responses":{"200":{"description":"Hoechstens 60 aktive Mandanten, neueste zuerst. Ohne Datenbank oder bei einem Fehler kommt dieselbe Form mit leerer Liste, nicht ein Fehlerstatus; ein Mandant, dessen Teilabfragen scheitern, faellt aus der Liste, ohne die uebrigen zu verlieren.","content":{"application/json":{"schema":{"type":"object","properties":{"tenants":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"plan":{"type":["string","null"]},"status":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"users":{"type":"number","description":"Sitzplaetze — Nutzer des Mandanten ohne geloeschte"},"lastActivityMs":{"type":["number","null"],"description":"Letzte Anmeldung als Unix-Zeit in Millisekunden"}},"required":["id","slug","name","plan","status","createdAt","users","lastActivityMs"]}},"totals":{"type":"object","properties":{"users":{"type":"number"}},"required":["users"]},"count":{"type":"number"},"generatedAt":{"type":"string","description":"Fehlt in der leeren Ersatzantwort"}},"required":["tenants","totals","count"]},"example":{"tenants":[{"id":"string","slug":"string","name":"string","plan":"string","status":"string","createdAt":"string","users":0,"lastActivityMs":0}],"totals":{"users":0},"count":0,"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminBackendherzTenants-health","tags":["admin"],"parameters":[],"summary":"Betriebsuebersicht je Mandant: Plan, Status, Sitzplaetze, letzte Anmeldung","description":"Cross-tenant Betriebsuebersicht: je Mandant Plan, Status, Sitzplaetze, letzte Anmeldung. Bewusst OHNE Geschaeftszahlen der Mandanten."}},"/api/admin/support/inbox":{"get":{"responses":{"200":{"description":"Aggregiertes Support-Postfach. Ein Mandant, dessen Tabelle fehlt oder dessen Abfrage scheitert, wird uebersprungen — die Antwort bleibt 200 und die uebrigen Mandanten stehen drin.","content":{"application/json":{"schema":{"type":"object","properties":{"chats":{"type":"array","items":{"type":"object","properties":{"tenantSlug":{"type":"string","description":"Slug des Mandanten, aus dem der Chat stammt"},"tenantName":{"type":"string","description":"Anzeigename des Mandanten aus public.tenants"},"conversationId":{"type":"string","description":"Kennung des Gespraechs innerhalb des Mandanten"},"userId":{"type":"string","description":"Verfasser der letzten Nachricht; leer, wenn die Spalte NULL ist"},"lastMessage":{"type":"string","description":"Die letzte Nachricht, auf 200 Zeichen gekuerzt"},"role":{"type":"string","description":"Rolle des Verfassers der letzten Nachricht, Vorgabe \"user\""},"unread":{"type":"boolean","description":"true, wenn die letzte Nachricht vom Nutzer kam und noch nicht gelesen wurde"},"ageHours":{"type":"number","description":"Alter der letzten Nachricht in Stunden, auf eine Nachkommastelle gerundet; 0 bei unlesbarem Datum"},"createdAt":{"type":"string","description":"Zeitpunkt der letzten Nachricht als ISO-8601"}},"required":["tenantSlug","tenantName","conversationId","userId","lastMessage","role","unread","ageHours","createdAt"]},"description":"Ungelesene zuerst, dann die neuesten; auf `chatLimit` gekuerzt"},"unreadChats":{"type":"integer","description":"Anzahl ungelesener Chats VOR der Kuerzung auf `chatLimit`"},"tenantsScanned":{"type":"integer","description":"Anzahl der durchsuchten aktiven Mandanten"},"generatedAt":{"type":"string","description":"Zeitpunkt dieser Auskunft als ISO-8601"}},"required":["chats","unreadChats","tenantsScanned","generatedAt"]},"example":{"chats":[{"tenantSlug":"string","tenantName":"string","conversationId":"string","userId":"string","lastMessage":"string","role":"string","unread":true,"ageHours":0,"createdAt":"string"}],"unreadChats":0,"tenantsScanned":0,"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"}},"operationId":"getApiAdminSupportInbox","tags":["admin"],"parameters":[],"description":"Cross-tenant Support-Postfach: die Chats, die Mandanten-Nutzer an uns schreiben. Bewusst OHNE die Tickets der Mandanten.","summary":"Cross-tenant Support-Postfach: die Chats, die Mandanten-Nutzer an uns schreiben","x-nemix-summary-source":"description:first-sentence"}},"/api/admin/server/metrics":{"get":{"responses":{"200":{"description":"Server metrics snapshot for ONE container — never fleet-wide, and every number resets when this process restarts. `cloudwatch.enabled` is false in this version; `cloudwatch.reason` names what is missing. The three `http` values are `null` when the prom-client registry holds no matching metric — `null` means „not measured\", a 0 would claim „no traffic\"; `p95LatencyMs` is never filled today. Nothing is cached: `generatedAt` is the moment of the request.","content":{"application/json":{"schema":{"type":"object","properties":{"process":{"type":"object","properties":{"uptimeSec":{"type":"number"},"pid":{"type":"number"},"nodeVersion":{"type":"string"},"rssMb":{"type":"number"},"heapUsedMb":{"type":"number"},"heapTotalMb":{"type":"number"},"externalMb":{"type":"number"},"heapUsedPct":{"type":"number"},"eventLoopLagMeanMs":{"type":"number"},"eventLoopLagP99Ms":{"type":"number"},"eventLoopLagMaxMs":{"type":"number"}},"required":["uptimeSec","pid","nodeVersion","rssMb","heapUsedMb","heapTotalMb","externalMb","heapUsedPct","eventLoopLagMeanMs","eventLoopLagP99Ms","eventLoopLagMaxMs"],"additionalProperties":false},"http":{"type":"object","properties":{"totalRequests":{"type":["number","null"]},"avgLatencyMs":{"type":["number","null"]},"p95LatencyMs":{"type":["number","null"]}},"required":["totalRequests","avgLatencyMs","p95LatencyMs"],"additionalProperties":false},"cloudwatch":{"type":"object","properties":{"enabled":{"type":"boolean"},"reason":{"type":"string"}},"required":["enabled","reason"],"additionalProperties":false},"generatedAt":{"type":"string"}},"required":["process","http","cloudwatch","generatedAt"],"additionalProperties":false},"example":{"process":{"uptimeSec":0,"pid":0,"nodeVersion":"string","rssMb":0,"heapUsedMb":0,"heapTotalMb":0,"externalMb":0,"heapUsedPct":0,"eventLoopLagMeanMs":0,"eventLoopLagP99Ms":0,"eventLoopLagMaxMs":0},"http":{"totalRequests":0,"avgLatencyMs":0,"p95LatencyMs":0},"cloudwatch":{"enabled":true,"reason":"string"},"generatedAt":"string"}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"503":{"description":"server_metrics_unavailable"}},"operationId":"getApiAdminServerMetrics","tags":["admin"],"parameters":[],"summary":"Auslastung dieses API-Containers: Speicher, Laufzeit, HTTP","description":"In-process server load for this API container (uptime, memory, event-loop lag, HTTP summary)."}},"/api/admin/ai/kill":{"get":{"responses":{"200":{"description":"Alle gesetzten Schalter samt Kurzform fuer den globalen Not-Aus","content":{"application/json":{"schema":{"type":"object","properties":{"globalHalted":{"type":"boolean"},"data":{"type":"array","items":{"type":"object","properties":{"scope":{"type":"string"},"tenantId":{"type":"string"},"enabled":{"type":"boolean"},"reason":{"type":["string","null"]},"actor":{"type":["string","null"]},"updatedAt":{}},"required":["scope","tenantId","enabled","reason","actor"],"additionalProperties":false}}},"required":["globalHalted","data"],"additionalProperties":false},"example":{"globalHalted":true,"data":[{"scope":"string","tenantId":"string","enabled":true,"reason":"string","actor":"string"}]}}}},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"},"503":{"description":"Datenbank nicht erreichbar","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"getApiAdminAiKill","tags":["admin","ai","kill-switch"],"parameters":[],"summary":"Liest den aktuellen Zustand des KI-Not-Aus (global + pro Mandant)","description":"Liefert ALLE Zeilen aus `public.ai_governance_switch` — den globalen Schalter und die je Mandant gesetzten, sortiert nach Bereich und Mandant. `globalHalted` ist die Kurzform: true, sobald der globale Schalter auf `enabled: false` steht. Der globale Schalter hat Vorrang vor jedem Mandanten-Schalter. Fehlt eine Zeile ganz, ist der betreffende Bereich NICHT gestoppt. Nur fuer Plattform-Administratoren erreichbar."},"post":{"responses":{"200":{"description":"Der geschriebene Schalter, so wie er jetzt in der Tabelle steht","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"halted":{"type":"boolean"},"switch":{"type":"object","properties":{"scope":{"type":"string"},"tenantId":{"type":"string"},"enabled":{"type":"boolean"},"reason":{"type":["string","null"]},"actor":{"type":["string","null"]},"updatedAt":{}},"required":["scope","tenantId","enabled","reason","actor"],"additionalProperties":false}},"required":["ok","halted","switch"],"additionalProperties":false},"example":{"ok":true,"halted":true,"switch":{"scope":"string","tenantId":"string","enabled":true,"reason":"string","actor":"string"}}}}},"400":{"description":"Validierungsfehler — etwa `scope: \"tenant\"` ohne `tenantId`"},"401":{"description":"No valid session cookie or API key was supplied — or the key is invalid or expired. The `hint` field states how to authenticate.","content":{"application/json":{"schema":{"type":"object","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["AUTH_REQUIRED","INVALID_API_KEY"]},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string","format":"uri"}}},"example":{"error":"unauthorized","code":"AUTH_REQUIRED","message":"Authentication required. Please sign in.","hint":"Provide a valid session cookie (browser login) or an API key via the X-API-Key header: X-API-Key: nemix_live_xxxxx","docs":"https://nemix.ainemix.de/docs/api"}}},"x-nemix-response-source":"auth-middleware"},"403":{"description":"Forbidden"},"503":{"description":"Datenbank nicht erreichbar — nichts geschaltet","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"database_unavailable"},"retryAfter":{"type":"number"}},"required":["error","retryAfter"],"additionalProperties":false}}}}},"operationId":"postApiAdminAiKill","tags":["admin","ai","kill-switch"],"parameters":[],"summary":"Setzt den KI-Not-Aus (enabled=false ⇒ KI-Ausführung gestoppt)","description":"Schreibt einen Schalter fuer `scope: \"global\"` oder fuer einen einzelnen Mandanten (`scope: \"tenant\"` verlangt dann `tenantId`); eine vorhandene Zeile wird ueberschrieben, nicht ergaenzt. `enabled: false` haelt die KI-Ausfuehrung an, `true` gibt sie wieder frei. Der Zwischenspeicher DIESES Prozesses wird sofort verworfen, andere Prozesse ziehen innerhalb ihrer eigenen kurzen Haltezeit nach. Wer geschaltet hat, wird zusaetzlich im KI-Protokoll vermerkt; scheitert dieser Eintrag, aendert das am Schaltvorgang nichts. Nur fuer Plattform-Administratoren erreichbar.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"string","enum":["global","tenant"]},"tenantId":{"type":"string","minLength":1,"maxLength":200},"enabled":{"type":"boolean"},"reason":{"type":"string","maxLength":1000}},"required":["scope","enabled"]},"example":{"scope":"global","tenantId":"string","enabled":true,"reason":"string"}}}}}},"/api/v1/admin/dsgvo/run-cleanup":{"post":{"responses":{"200":{"description":"Lauf beendet. `result` unterscheidet die beiden Faelle: `cleanup completed` traegt `durationMs` und `summariesPreview` (hoechstens fuenf Antraege), `cleanup skipped` stattdessen `reason` — dann fehlte die Datenbank und es wurde NICHTS anonymisiert.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"result":{"type":"string","const":"cleanup skipped"},"reason":{"type":"string"},"processed":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"}},"required":["result","reason","processed","succeeded","failed"],"additionalProperties":false},{"type":"object","properties":{"result":{"type":"string","const":"cleanup completed"},"processed":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"},"durationMs":{"type":"number"},"summariesPreview":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"userId":{"type":"string"},"tablesAffected":{"type":"number"},"recordsAnonymized":{"type":"number"},"errorsCount":{"type":"number"}},"required":["requestId","userId","tablesAffected","recordsAnonymized","errorsCount"],"additionalProperties":false}}},"required":["result","processed","succeeded","failed","durationMs","summariesPreview"],"additionalProperties":false}]},"example":{"result":"cleanup skipped","reason":"string","processed":0,"succeeded":0,"failed":0}}}},"401":{"description":"ACHTUNG, LIVE GEMESSEN AM 30.08.2026: dieser Pfad antwortet ohne gueltige Anmeldung mit 401 aus `authMiddleware` — NICHT mit 403 aus `requireInternalOrAdmin`. Der Router haengt zwar an `app` (index.ts:2819), aber die Auth-Middleware der api-Unter-App wurde neun Zeilen frueher unter `/api/v1` registriert und greift mit."},"403":{"description":"Weder `INTERNAL_CRON_SECRET` noch ein super_admin-Nachweis — erreichbar nur, wenn die Anmeldung davor bestanden wurde."},"500":{"description":"Der Lauf ist mit einer Ausnahme abgebrochen. Bereits anonymisierte Antraege bleiben anonymisiert — die Aktion ist nicht umkehrbar.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","const":"cleanup failed"},"error":{"type":"string"},"durationMs":{"type":"number"}},"required":["result","error","durationMs"],"additionalProperties":false}}}}},"operationId":"postApiV1AdminDsgvoRun-cleanup","tags":["admin"],"parameters":[],"description":"Fuehrt die faelligen Loeschungen nach DSGVO Art. 17 aus — fuer ALLE Mandanten, nicht nur einen. Liest `public.gdpr_deletion_requests` und anonymisiert je Zeile ueber `anonymizeUser()`. Bis zum 24.05.2026 hat dieser Endpunkt nur GEZAEHLT und `processed: 0` gemeldet, egal was anstand; seitdem tut er, was sein Name sagt. Geplant taeglich 02:00 UTC.","summary":"Fuehrt die faelligen Loeschungen nach DSGVO Art. 17 aus","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/admin/invoices/run-dunning":{"post":{"responses":{"200":{"description":"Lauf beendet. Der Rumpf sagt nur, DASS er durchlief (`durationMs`, `asOf`) — wie viele Mahnungen entstanden sind, nennt er NICHT.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","const":"dunning completed"},"durationMs":{"type":"number"},"asOf":{"type":"string"}},"required":["result","durationMs","asOf"],"additionalProperties":false},"example":{"result":"dunning completed","durationMs":0,"asOf":"string"}}}},"401":{"description":"ACHTUNG, LIVE GEMESSEN AM 30.08.2026: dieser Pfad antwortet ohne gueltige Anmeldung mit 401 aus `authMiddleware` — NICHT mit 403 aus `requireInternalOrAdmin`. Der Router haengt zwar an `app` (index.ts:2819), aber die Auth-Middleware der api-Unter-App wurde neun Zeilen frueher unter `/api/v1` registriert und greift mit."},"403":{"description":"Weder `INTERNAL_CRON_SECRET` noch ein super_admin-Nachweis — erreichbar nur, wenn die Anmeldung davor bestanden wurde."},"500":{"description":"Der Lauf ist mit einer Ausnahme abgebrochen. Mandanten, die davor an der Reihe waren, koennen bereits gemahnt sein.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","const":"dunning failed"},"error":{"type":"string"},"durationMs":{"type":"number"}},"required":["result","error","durationMs"],"additionalProperties":false}}}}},"operationId":"postApiV1AdminInvoicesRun-dunning","tags":["admin"],"parameters":[],"summary":"Stoesst das Mahnwesen fuer alle Mandanten an","description":"Stoesst das Mahnwesen fuer alle Mandanten an: ueberfaellige Rechnungen finden, die faellige Mahnstufe bestimmen, Mahnungen erzeugen. Geplant Mo-Fr 07:00 UTC."}},"/api/v1/admin/inventory/check-reorder-levels":{"post":{"responses":{"200":{"description":"Lauf beendet. `inventory check completed` traegt die gefundenen Artikel in `items`; `inventory check skipped` traegt `reason` und eine leere Liste — dann fehlte die Datenbank, und eine 0 heisst nicht „alles auf Bestand\". Nebenwirkung des Erfolgsfalls: je betroffenem Mandanten wird den Rollen admin und manager eine Benachrichtigung `reorder_warning` geschrieben.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"result":{"type":"string","const":"inventory check skipped"},"reason":{"type":"string"},"belowReorderLevel":{"type":"number"},"items":{"type":"array","items":{}}},"required":["result","reason","belowReorderLevel","items"],"additionalProperties":false},{"type":"object","properties":{"result":{"type":"string","const":"inventory check completed"},"belowReorderLevel":{"type":"number"},"items":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string"},"tenantId":{"type":"string"},"currentStock":{"type":"number"},"reorderLevel":{"type":"number"}},"required":["productId","tenantId","currentStock","reorderLevel"],"additionalProperties":false}}},"required":["result","belowReorderLevel","items"],"additionalProperties":false}]},"example":{"result":"inventory check skipped","reason":"string","belowReorderLevel":0,"items":[]}}}},"401":{"description":"ACHTUNG, LIVE GEMESSEN AM 30.08.2026: dieser Pfad antwortet ohne gueltige Anmeldung mit 401 aus `authMiddleware` — NICHT mit 403 aus `requireInternalOrAdmin`. Der Router haengt zwar an `app` (index.ts:2819), aber die Auth-Middleware der api-Unter-App wurde neun Zeilen frueher unter `/api/v1` registriert und greift mit."},"403":{"description":"Weder `INTERNAL_CRON_SECRET` noch ein super_admin-Nachweis — erreichbar nur, wenn die Anmeldung davor bestanden wurde."}},"operationId":"postApiV1AdminInventoryCheck-reorder-levels","tags":["admin"],"parameters":[],"description":"Sucht Artikel unter ihrem Meldebestand und meldet sie zurueck. Schreibt nichts am Bestand — die Antwort ist eine Liste, keine Bestellung. Geplant Mo-Fr 06:00 UTC.","summary":"Sucht Artikel unter ihrem Meldebestand und meldet sie zurueck","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/admin/payroll/send-reminders":{"post":{"responses":{"200":{"description":"Lauf beendet. `payroll reminders queued` nennt in `tenantsNotified` die Zahl der angeschriebenen Mandanten-Verwalter und in `month` den Monat (`JJJJ-MM`); `payroll reminders skipped` traegt stattdessen `reason` — dann fehlte die Datenbank. Nebenwirkung des Erfolgsfalls: je Verwalter eine Benachrichtigung `payroll_reminder`.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"result":{"type":"string","const":"payroll reminders skipped"},"reason":{"type":"string"},"tenantsNotified":{"type":"number"}},"required":["result","reason","tenantsNotified"],"additionalProperties":false},{"type":"object","properties":{"result":{"type":"string","const":"payroll reminders queued"},"tenantsNotified":{"type":"number"},"month":{"type":"string"}},"required":["result","tenantsNotified","month"],"additionalProperties":false}]},"example":{"result":"payroll reminders skipped","reason":"string","tenantsNotified":0}}}},"401":{"description":"ACHTUNG, LIVE GEMESSEN AM 30.08.2026: dieser Pfad antwortet ohne gueltige Anmeldung mit 401 aus `authMiddleware` — NICHT mit 403 aus `requireInternalOrAdmin`. Der Router haengt zwar an `app` (index.ts:2819), aber die Auth-Middleware der api-Unter-App wurde neun Zeilen frueher unter `/api/v1` registriert und greift mit."},"403":{"description":"Weder `INTERNAL_CRON_SECRET` noch ein super_admin-Nachweis — erreichbar nur, wenn die Anmeldung davor bestanden wurde."}},"operationId":"postApiV1AdminPayrollSend-reminders","tags":["admin"],"parameters":[],"description":"Erinnert die Mandanten an den anstehenden Lohnlauf. Geplant am 25. jeden Monats.","summary":"Erinnert die Mandanten an den anstehenden Lohnlauf","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/admin/daily-briefing":{"post":{"responses":{"200":{"description":"Lauf beendet. Im Normalfall kommen nur `processed`, `notified` und `results` — OHNE das Feld `result`; das traegt nur der uebersprungene Lauf (fehlende Datenbank) zusammen mit `reason`. `results` enthaelt ausschliesslich die Mandanten mit einer Meldung, ist also hoechstens so lang wie `notified`. Betrachtet werden hoechstens 100 Mandanten je Lauf; ein Fehler bei einem einzelnen bricht den Lauf nicht ab und ist an der Antwort nicht zu erkennen.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"result":{"type":"string","const":"daily-briefing skipped"},"reason":{"type":"string"},"processed":{"type":"number"},"notified":{"type":"number"},"results":{"type":"array","items":{}}},"required":["result","reason","processed","notified","results"],"additionalProperties":false},{"type":"object","properties":{"processed":{"type":"number"},"notified":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"tenant":{"type":"string"},"notified":{"type":"boolean"},"message":{"type":"string"}},"required":["tenant","notified","message"],"additionalProperties":false}}},"required":["processed","notified","results"],"additionalProperties":false}]},"example":{"result":"daily-briefing skipped","reason":"string","processed":0,"notified":0,"results":[]}}}},"401":{"description":"ACHTUNG, LIVE GEMESSEN AM 30.08.2026: dieser Pfad antwortet ohne gueltige Anmeldung mit 401 aus `authMiddleware` — NICHT mit 403 aus `requireInternalOrAdmin`. Der Router haengt zwar an `app` (index.ts:2819), aber die Auth-Middleware der api-Unter-App wurde neun Zeilen frueher unter `/api/v1` registriert und greift mit."},"403":{"description":"Weder `INTERNAL_CRON_SECRET` noch ein super_admin-Nachweis — erreichbar nur, wenn die Anmeldung davor bestanden wurde."}},"operationId":"postApiV1AdminDaily-briefing","tags":["admin"],"parameters":[],"description":"Erzeugt je Mandant die Tagesuebersicht und benachrichtigt, wo etwas ansteht. Die Antwort nennt beide Zahlen getrennt — `processed` sind die betrachteten Mandanten, `notified` die, bei denen wirklich etwas zu melden war.","summary":"Erzeugt je Mandant die Tagesuebersicht und benachrichtigt, wo etwas ansteht","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/admin/banking/sync-all":{"post":{"responses":{"200":{"description":"Immer dieselbe Antwort: `result` steht fest auf `banking sync skipped` und `synced` fest auf 0. Die Route liest keine Datenbank und ruft keine Bank.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","const":"banking sync skipped"},"reason":{"type":"string"},"synced":{"type":"number"}},"required":["result","reason","synced"],"additionalProperties":false},"example":{"result":"banking sync skipped","reason":"string","synced":0}}}},"401":{"description":"ACHTUNG, LIVE GEMESSEN AM 30.08.2026: dieser Pfad antwortet ohne gueltige Anmeldung mit 401 aus `authMiddleware` — NICHT mit 403 aus `requireInternalOrAdmin`. Der Router haengt zwar an `app` (index.ts:2819), aber die Auth-Middleware der api-Unter-App wurde neun Zeilen frueher unter `/api/v1` registriert und greift mit."},"403":{"description":"Weder `INTERNAL_CRON_SECRET` noch ein super_admin-Nachweis — erreichbar nur, wenn die Anmeldung davor bestanden wurde."}},"operationId":"postApiV1AdminBankingSync-all","tags":["admin"],"parameters":[],"description":"Platzhalter fuer den Banking-Abgleich. Er antwortet 200 und tut NICHTS — der echte Abgleich kam mit W10. Der Endpunkt steht hier, damit der geplante Lauf nicht ins Leere greift; wer ihn aufruft, bekommt keine Umsaetze.","summary":"Platzhalter fuer den Banking-Abgleich","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/admin/email-inbox/email-inbox/poll":{"post":{"responses":{"200":{"description":"Ergebnis des Laufs je Mandant","content":{"application/json":{"schema":{"type":"object","properties":{"tenantsTotal":{"type":"integer","minimum":0},"tenantsProcessed":{"type":"integer","minimum":0},"tenantsSkipped":{"type":"integer","minimum":0},"imported":{"type":"integer","minimum":0},"failed":{"type":"integer","minimum":0},"details":{"type":"array","items":{"type":"object","properties":{"tenantId":{"type":"string"},"reason":{"type":"string"},"imported":{"type":"integer","minimum":0},"failed":{"type":"integer","minimum":0}},"required":["tenantId","imported","failed"]}}},"required":["tenantsTotal","tenantsProcessed","tenantsSkipped","imported","failed","details"]},"example":{"tenantsTotal":0,"tenantsProcessed":0,"tenantsSkipped":0,"imported":0,"failed":0,"details":[{"tenantId":"string","reason":"string","imported":0,"failed":0}]}}}},"401":{"description":"Weder interner Cron-Aufruf noch Admin"}},"operationId":"postApiV1AdminEmail-inboxEmail-inboxPoll","tags":["admin","email-inbox"],"parameters":[],"description":"Manuell IMAP-Polling für alle aktiven Tenant-Inboxen anstoßen. Erreichbar nur mit interner Cron-Berechtigung oder als Admin. Der Lauf geht ueber ALLE Mandanten mit enabled=true, nicht nur den eigenen, und laeuft synchron: die Antwort kommt erst, wenn jedes Postfach abgearbeitet ist. Ist keine Datenbank verbunden, kommt ein Ergebnis mit lauter Nullen statt eines Fehlers — tenantsTotal gehoert deshalb mitgelesen.","summary":"Manuell IMAP-Polling für alle aktiven Tenant-Inboxen anstoßen","x-nemix-summary-source":"description:first-sentence"}},"/api/v1/admin/data-integrity/orphans":{"get":{"responses":{"200":{"description":"Befund. `source` unterscheidet `cached` (Cron-Ergebnis) von `live` (Sofort-Scan); `totalOrphans` ist die Summe ueber `rows`.","content":{"application/json":{"schema":{"type":"object","properties":{"runAt":{"type":"string","description":"Bei `source: \"live\"` der Zeitpunkt DIESES Aufrufs, sonst der des Cron-Laufs"},"totalOrphans":{"type":"integer","description":"Summe ueber `rows`"},"rows":{"type":"array","items":{"type":"object","properties":{"schema":{"type":"string"},"table":{"type":"string"},"fk":{"type":"string","description":"Die Fremdschluessel-Spalte"},"refTable":{"type":"string","description":"Die Tabelle, auf die sie zeigen sollte"},"refColumn":{"type":"string"},"orphanCount":{"type":"integer","description":"Zeilen, deren Ziel es nicht mehr gibt"},"totalCount":{"type":"integer","description":"Zeilen insgesamt — der Bezugswert dazu"},"severity":{"type":"string","description":"high | medium"}},"required":["schema","table","fk","refTable","refColumn","orphanCount","totalCount","severity"]}},"source":{"type":"string","description":"cached | live — siehe Beschreibung des Aufrufs"}},"required":["runAt","totalOrphans","rows","source"]},"example":{"runAt":"string","totalOrphans":0,"rows":[{"schema":"string","table":"string","fk":"string","refTable":"string","refColumn":"string","orphanCount":0,"totalCount":0,"severity":"string"}],"source":"string"}}}},"401":{"description":"Keine aufgeloeste Rolle — `requireMinRole` faellt geschlossen aus."},"403":{"description":"Rolle unterhalb von `admin` — `code: \"INSUFFICIENT_ROLE\"`."},"500":{"description":"SQL fehlgeschlagen. `error` traegt die Postgres-Meldung unveraendert nach aussen."},"503":{"description":"Keine Datenbankverbindung — `error: \"DB unavailable\"`."}},"operationId":"getApiV1AdminData-integrityOrphans","tags":["admin"],"parameters":[],"summary":"Verwaiste Fremdschluessel auflisten","description":"Nennt die Zeilen, deren Fremdschluessel auf einen nicht mehr vorhandenen Datensatz zeigt — je Tabelle und Beziehung mit Anzahl.\n\nDIESE ROUTE AENDERT NICHTS AN DEN DATEN. Sie liest nur den Befund; repariert wird ausschliesslich ueber `POST /orphans/fix`.\n\nES GIBT ZWEI HERKUENFTE, und das Feld `source` sagt welche. `cached` ist das gespeicherte Ergebnis des letzten Cron-Laufs aus `public.data_integrity_audit`. `live` ist ein sofort ausgefuehrter Scan — er greift genau dann, wenn die Audit-Tabelle noch keine einzige Zeile hat (frisch deployter Stand), damit man nicht auf den naechsten Cron warten muss. Im Live-Fall ist `runAt` der Zeitpunkt DIESES Aufrufs, nicht der eines Cron-Laufs.\n\nDie Audit-Tabelle wird bei jedem Aufruf angelegt, falls sie fehlt."}},"/api/v1/admin/data-integrity/orphans/fix":{"post":{"responses":{"200":{"description":"Ausgefuehrt. `affected` ist die Zahl der geaenderten bzw. geloeschten Zeilen — 0 heisst, es gab nichts zu reparieren.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"schema":{"type":"string","description":"Das Schema des ANGEMELDETEN Mandanten, nicht das aus dem Rumpf"},"table":{"type":"string"},"fk":{"type":"string"},"action":{"type":"string","description":"set_null | delete_row"},"affected":{"type":"integer","description":"Geaenderte bzw. geloeschte Zeilen; 0 = nichts zu tun"}},"required":["ok","schema","table","fk","action","affected"]},"example":{"ok":true,"schema":"string","table":"string","fk":"string","action":"string","affected":0}}}},"400":{"description":"Rumpf ist kein JSON, ein Pflichtfeld (`table`, `fk`, `action`) fehlt, die Beziehung steht nicht auf der Freigabeliste, `action` ist weder `set_null` noch `delete_row`, oder `delete_row` wurde fuer eine andere Tabelle als `deliveries` verlangt."},"401":{"description":"Keine aufgeloeste Rolle (`requireMinRole`) oder kein Mandantenkontext — `error: \"tenant context missing\"`."},"403":{"description":"Rolle unterhalb von `admin` (`code: \"INSUFFICIENT_ROLE\"`) ODER ein `schema` im Rumpf, das nicht dem eigenen Mandanten gehoert."},"500":{"description":"SQL fehlgeschlagen. `error` traegt die Postgres-Meldung unveraendert nach aussen."},"503":{"description":"Keine Datenbankverbindung — `error: \"DB unavailable\"`."}},"operationId":"postApiV1AdminData-integrityOrphansFix","tags":["admin"],"parameters":[],"summary":"Verwaiste Fremdschluessel reparieren","description":"Repariert die verwaisten Zeilen GENAU EINER Fremdschluessel-Beziehung. Rumpf: `{ table, fk, action }`.\n\n`action: \"set_null\"` setzt die Fremdschluessel-Spalte der verwaisten Zeilen auf NULL — die Zeile selbst bleibt stehen. `action: \"delete_row\"` LOESCHT die verwaisten Zeilen endgueltig: kein Soft-Delete, kein `deleted_at`, kein Weg zurueck. Deshalb ist `delete_row` auf `deliveries` beschraenkt; fuer jede andere Tabelle antwortet die Route 400, damit kein Beleg verschwindet.\n\nES GIBT KEINEN TROCKENLAUF. Der Aufruf schreibt sofort; `affected` nennt hinterher, wie viele Zeilen geaendert bzw. geloescht wurden. Wer vorher wissen will, was ansteht, fragt `GET /orphans`.\n\nNUR FREIGEGEBENE BEZIEHUNGEN: `table` und `fk` muessen zusammen in der fest verdrahteten Liste stehen — `orders.customer_id`, `orders.quote_id`, `invoices.customer_id`, `invoices.order_id`, `deliveries.order_id`. Alles andere ist 400, auch wenn die Tabelle existiert.\n\nDAS SCHEMA KOMMT AUS DEM ANGEMELDETEN MANDANTEN, nie aus dem Rumpf. Ein mitgeschicktes `schema` wird nur geduldet, wenn es dem eigenen gleicht; jeder andere Wert gilt als mandantenuebergreifender Schreibversuch und endet mit 403.\n\nDer Vorgang wird zusaetzlich in `public.data_integrity_audit` vermerkt (mit Benutzerkennung). Schlaegt dieser Vermerk fehl, aendert das die Antwort NICHT — die Reparatur gilt, der Nachweis fehlt dann nur im Anwendungs-Log."}}},"x-tagGroups":[{"name":"Identity & Access","tags":["2fa","Empfehlungen","api-keys","auth","compliance","gdpr","me","onboarding","permissions","tenant","tenants","users"]},{"name":"CRM & Kontakte","tags":["Activities","Address Types","CRM","CRM · Health","CRM · Statement","Campaigns","Contact Categories","Contact Types","Customer Addresses","Customer Bank Accounts","EmailSync","Leads","Sales · Territories","Scoring","Segments","activity","audit","beta-crm","bulk","contacts","customers","history","impersonate","limits","notes","pipeline","saved-searches","stammdaten"]},{"name":"Vertrieb","tags":["Bau · Nachträge","CreditNotes","Deliveries","DocumentChain","Konditionen","Number Ranges","Provisionen","Rahmen","RahmenAbrufe","Recurring Invoices","Sales · Forecasts","invoices","orders","quotes"]},{"name":"Einkauf","tags":["Lieferanten","MRP","Vendors","budgets","catalog","e-rechnung","einkauf","matching","purchasing","scorecard","spend","supplier"]},{"name":"Lager & Produktion","tags":["Inventory","bom","inventur","lager","lot-tracking","manufacturing","production","warehouse"]},{"name":"Finanzen","tags":["Bau · ZUGFeRD","Bonus","Mahnwesen","VAT Validation","account-schedules","accounting","accounting-periods","ai-token-usage","ai-user-budget","anlagen","banking","billing","billing-overage","datev","dimensions","elster","gobd","journal-entries","kostenrechnung","multi-tenant","payroll","posting-groups"]},{"name":"Projekte, Bau & Service","tags":["Bau · Abrechnung","Bau · Aufmaß","Bau · LV","Budget","Dossier","GAEB-LV","Gantt","Projects","Projects · Capacity","QM","TimeTracking","contracts","gaeb","maengel","rma","service-visits","tickets"]},{"name":"Personal & Zeit","tags":["HR","Zeiterfassung","calendar","employees","timesheets"]},{"name":"Immobilien","tags":["immo"]},{"name":"Dokumente & DMS","tags":["Comments","Documents · Sharing","Tags","Versions","booking","dms","doc-templates","documents","exports","folders","imports","ocr","signatures","uploads","zugferd"]},{"name":"Aufgaben & Kommunikation","tags":["Belege","Calls","EmailTemplates","Tasks","email-inbox","email-tracking","inbox-addresses","notifications","push","telegram","voice","whatsapp"]},{"name":"KI & Automatisierung","tags":["Anpassungen","Eigene Agenten","Finance","KI","KI-Gedaechtnis","Knowledge Base","Operations","Procurement","Quality","RAG","Sales","agent-plans","agent-templates","ai","ai-agent","ai-data-builder","ai-data-ops","ai-rls-builder","build-docs","citations","conversations","custom-agents","customizing-introspect","decision-tables","entity-rules","industry-packs","kill-switch","layers","mcp","mcp-tokens","proactive-insights","prozess-charts","rag-query","rag-wizard","telemetry","templates","undo","vision","workflow-builder","workflows"]},{"name":"Auswertung & Audit","tags":["Datenqualitaet","Historie","analytics","audit-log","dashboard","dashboard-config","metrics","reports","search","sequence-audit","usage"]},{"name":"Einstellungen & Administration","tags":["Betrieb","Eigene Module","Eigene Seiten","Geo","Projekte","_internal","admin","costs","custom-entities","custom-fields","custom-fields-v2","demo","developer","email","flags","instances","list-configs","marketplace","modules","organizations","rollouts","sandbox","saved-views","settings","sidebar-prefs","support","system","task-display-prefs","theme","ui-configs","user-views"]},{"name":"Integrationen & Portale","tags":["Customer-Portal","Konnektoren","embed","integrations","n8n","stb-portal","webhooks"]},{"name":"System & Health","tags":["api-docs","health","platform","status"]}]}