openapi: 3.1.0 info: title: efakt portal API version: 1.0.0 summary: REST API portálu pro správu e-faktur (SK eFaktúra / Peppol). description: | Verze `/api/v1`. Zdroj pravdy kontraktu je tento soubor; zod schémata v `src/schemas` jsou s ním udržována v paritě testem (`test/openapi-parity.test.ts`). ## Autentizace * **Uživatelé (web UI)** – cookie session `efakt_session` (HttpOnly, Secure, SameSite=Lax) po `POST /auth/login` (+ `POST /auth/totp`). Změny stavu (POST/PUT/PATCH/DELETE) při cookie autentizaci vyžadují hlavičku `X-CSRF-Token` s hodnotou cookie `efakt_csrf` (double-submit). Viz ADR 001. * **Účetní software** – API klíč tenanta: `Authorization: Bearer efk_<...>` **nebo** `X-API-Key: efk_<...>`. Klíč je vázán na jednoho tenanta a má scopes. CSRF se u API klíčů nevyžaduje. ## Obecné konvence * Chyby: `application/problem+json` (RFC 9457) s vlastním polem `code` (katalog `ErrorCode`). * Čas: ISO 8601 UTC (`2026-10-04T12:00:00Z`). Datum faktury: `YYYY-MM-DD` (den v Europe/Bratislava). * Částky: **desetinný řetězec** (`"1234.50"`), nikdy JSON number. * Stránkování: cursor. Dotaz `?limit=&cursor=`, odpověď `{ "items": [...], "next_cursor": "..." | null }`. Cursor je neprůhledný, platný pro stejné filtry a řazení. * Rate limit: hlavičky `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (sekundy do resetu), při překročení `429` + `Retry-After` (sekundy). * Každá odpověď nese `X-Request-Id` (korelační ID, vrací se i v poli `request_id` chyby). * Data se nikdy nemažou. Endpointy `DELETE` na konfiguraci (webhook, spojení, členství) provádějí **deaktivaci** (soft), záznam zůstává a je auditován. license: name: Proprietary identifier: LicenseRef-Proprietary servers: - url: http://localhost:18080/api/v1 description: Lokální backend (Podman) - url: http://localhost:18082 description: Prism mock server (`npm run mock` v packages/contracts) tags: - name: Auth description: Přihlášení, TOTP, session - name: Tenants description: Tenanti uživatele a aktivní tenant - name: Invoices description: Faktury - seznam, detail, XML, odesílání, import - name: Exports description: Hromadné exporty (ZIP/CSV) - name: Participants description: Dotaz na účastníka Peppolu - name: Connections description: Spojení s poštářem (zákaznický pohled) a příjem webhooků poštáře - name: ApiKeys description: API klíče tenanta - name: Webhooks description: Webhooky portálu směrem k účetnímu SW - name: Admin description: Administrace (role admin) - name: ERP description: ERP API (Caris a kompatibilní účetní programy) a správa ERP integrací - name: Verteco description: Verteco – partnerský (reseller / sprostredkovateľ) režim, FS verifikační a partnerské webhooky - name: Backups description: Zálohy na externí úložiště – proxy k enginu efakt-backup (jen role admin) a notifikační webhook enginu - name: Validation description: Předodesílací validace e-faktury (dry-run, stav vrstev, nastavení, vyloučení příjemci) - name: Forwarding description: Přeposílání faktur e-mailem (pravidla, příjemci s double opt-in, doručení, sdílené odkazy s heslem, oprávnění) a veřejné stránky sdílené kopie - name: Sandbox description: Veřejný vývojářský sandbox - samoobslužná registrace (double opt-in), kvóty, správa - name: System description: Provozní endpointy security: - cookieAuth: [] - bearerApiKey: [] - apiKeyHeader: [] paths: /health: get: tags: [System] operationId: getHealth summary: Liveness API (bez autentizace) security: [] responses: '200': description: OK content: application/json: schema: type: object required: [status, version] properties: status: { type: string, enum: [ok] } version: { type: string } # ------------------------------------------------------------------ AUTH /auth/login: post: tags: [Auth] operationId: login summary: Přihlášení e-mailem a heslem (1. krok) description: | Při úspěchu a vypnutém TOTP vrátí `authenticated` a nastaví cookie. Má-li uživatel TOTP, vrátí `totp_required` + krátkodobý `challenge_token` (5 min) a cookie **nenastaví**. Pokud role vyžaduje TOTP a uživatel ho nemá aktivní, vrátí `totp_enrollment_required` (a omezenou session jen pro `/auth/totp/*`). security: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LoginRequest' } responses: '200': description: Výsledek prvního kroku content: application/json: schema: { $ref: '#/components/schemas/LoginResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/TooManyRequests' } '400': { $ref: '#/components/responses/BadRequest' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /auth/totp: post: tags: [Auth] operationId: loginTotp summary: Přihlášení – 2. krok (kód TOTP nebo záložní kód) security: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/TotpRequest' } responses: '200': description: Přihlášen, nastavena cookie `efakt_session` a `efakt_csrf`. headers: Set-Cookie: { $ref: '#/components/headers/SetCookie' } content: application/json: schema: { $ref: '#/components/schemas/LoginResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/TooManyRequests' } '400': { $ref: '#/components/responses/BadRequest' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /auth/totp/enroll: post: tags: [Auth] operationId: totpEnroll summary: Zahájení zápisu TOTP (vrátí otpauth URI a tajemství) security: [{ cookieAuth: [] }] responses: '200': description: Tajemství se zobrazuje jen jednou. Aktivace až po `/auth/totp/confirm`. content: application/json: schema: { $ref: '#/components/schemas/TotpEnrollment' } '401': { $ref: '#/components/responses/Unauthorized' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /auth/totp/confirm: post: tags: [Auth] operationId: totpConfirm summary: Potvrzení zápisu TOTP prvním kódem; vrátí záložní kódy (jednou) security: [{ cookieAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: { type: string, pattern: '^[0-9]{6}$' } responses: '200': description: TOTP aktivní content: application/json: schema: type: object required: [backup_codes] properties: backup_codes: type: array items: { type: string } '401': { $ref: '#/components/responses/Unauthorized' } '422': { $ref: '#/components/responses/Unprocessable' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /auth/password-reset/request: post: tags: [Auth] operationId: requestPasswordReset summary: Žádost o reset hesla e-mailem (veřejné) description: | Vždy `202` (neprozrazuje, zda účet existuje ani zda je SMTP nastaveno). Pokud účet existuje, není zablokovaný a SMTP je nastaveno, odešle se e-mail s jednorázovým odkazem (platnost 1 hodina). Bez SMTP se nic neodešle (správce může heslo nastavit v administraci). security: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PasswordResetRequest' } responses: '202': { description: 'Přijato (vždy)' } '429': { $ref: '#/components/responses/TooManyRequests' } '400': { $ref: '#/components/responses/BadRequest' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /auth/password-reset/confirm: post: tags: [Auth] operationId: confirmPasswordReset summary: Nastavení hesla jednorázovým odkazem (pozvánka i reset; veřejné) description: | Token je jednorázový a časově omezený (pozvánka 72 h, reset 1 h). Po úspěchu se zruší všechny session uživatele a ostatní odkazy; TOTP zůstává beze změny. Neplatný/prošlý/použitý token: `400 INVALID_TOKEN`; slabé heslo `422 WEAK_PASSWORD`. security: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PasswordResetConfirm' } responses: '204': { description: Heslo nastaveno } '429': { $ref: '#/components/responses/TooManyRequests' } '400': { $ref: '#/components/responses/BadRequest' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /auth/logout: post: tags: [Auth] operationId: logout summary: Odhlášení (zneplatní session) security: [{ cookieAuth: [] }] responses: '204': { description: Odhlášen } '401': { $ref: '#/components/responses/Unauthorized' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /auth/me: get: tags: [Auth] operationId: getMe summary: Přihlášený uživatel, aktivní tenant a členství description: | Funguje i v omezené session po `totp_enrollment_required` (`session_kind: enrollment`): vrací identitu a `csrf_token`, `permissions` i `tenants` jsou pak prázdná pole (aplikační endpointy vrací 403 do dokončení zápisu TOTP). security: [{ cookieAuth: [] }] responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Me' } '401': { $ref: '#/components/responses/Unauthorized' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /me/tenants: get: tags: [Tenants] operationId: listMyTenants summary: Tenanti, které uživatel smí spravovat (admin vidí všechny aktivní) security: [{ cookieAuth: [] }] responses: '200': description: OK content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/TenantSummary' } '401': { $ref: '#/components/responses/Unauthorized' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /me/active-tenant: put: tags: [Tenants] operationId: switchActiveTenant summary: Přepnutí aktivního tenanta v session description: Všechny tenant-scoped endpointy (`/invoices`, `/connections`, `/api-keys`, `/webhooks`, ...) pracují s aktivním tenantem session. Pro API klíč je tenant dán klíčem a tento endpoint není dostupný. security: [{ cookieAuth: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SwitchTenantRequest' } responses: '200': description: Přepnuto content: application/json: schema: { $ref: '#/components/schemas/Me' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '400': { $ref: '#/components/responses/BadRequest' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /tenant: get: tags: [Tenants] operationId: getCurrentTenant summary: Aktivní tenant (včetně nastavení; funguje i s API klíčem) responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Tenant' } '401': { $ref: '#/components/responses/Unauthorized' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } # ------------------------------------------------------------------ INVOICES /invoices: get: tags: [Invoices] operationId: listInvoices summary: Seznam faktur aktivního tenanta description: Výchozí řazení `-issue_date` (sestupně), tie-break `id`. Scope API klíče `invoices:read`. parameters: - { name: direction, in: query, schema: { $ref: '#/components/schemas/Direction' } } - name: status in: query description: Jeden nebo více stavů (čárkou oddělené nebo opakovaný parametr). style: form explode: false schema: type: array items: { $ref: '#/components/schemas/InvoiceStatus' } - { name: validation_status, in: query, schema: { $ref: '#/components/schemas/ValidationStatus' } } - { name: erp_delivered, in: query, description: 'Převzato do ERP (ACK aspoň jednou integrací): `true` = převzato, `false` = nepřevzato (jen příchozí faktury mají smysl).', schema: { type: boolean } } - { name: doc_type, in: query, description: "Kód typu dokladu (380, 381, ...)", schema: { $ref: '#/components/schemas/DocumentTypeCode' } } - { name: source, in: query, schema: { $ref: '#/components/schemas/InvoiceSource' } } - { name: connection_id, in: query, schema: { type: string, format: uuid } } - { name: partner, in: query, description: 'Protistrana: DIČ, IČO, Peppol ID nebo část názvu (podřetězec, case-insensitive). U inbound hledá v dodavateli, u outbound v odběrateli, bez `direction` v obou.', schema: { type: string, maxLength: 200 } } - { name: number, in: query, description: "Číslo faktury (přesná shoda; pro částečnou použij `search`)", schema: { type: string, maxLength: 200 } } - { name: issue_date_from, in: query, schema: { type: string, format: date } } - { name: issue_date_to, in: query, schema: { type: string, format: date } } - { name: currency, in: query, schema: { type: string, pattern: '^[A-Z]{3}$' } } - { name: amount_min, in: query, description: Minimum `total_gross`, schema: { $ref: '#/components/schemas/Money' } } - { name: amount_max, in: query, description: Maximum `total_gross`, schema: { $ref: '#/components/schemas/Money' } } - { name: search, in: query, description: "Fulltext/trigram nad číslem faktury a názvy stran", schema: { type: string, maxLength: 200 } } - name: sort in: query schema: type: string enum: [-issue_date, issue_date, -received_at, received_at, -total_gross, total_gross] default: -issue_date - { $ref: '#/components/parameters/Limit' } - { $ref: '#/components/parameters/Cursor' } responses: '200': description: Stránka faktur headers: RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' } RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' } RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' } content: application/json: schema: { $ref: '#/components/schemas/InvoicePage' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/TooManyRequests' } '409': { $ref: '#/components/responses/Conflict' } '503': { $ref: '#/components/responses/ServiceUnavailable' } post: tags: [Invoices] operationId: submitInvoice summary: Odeslání faktury přes poštáře tenanta (vždy asynchronní) description: | Scope `invoices:send`, role (cookie) `tenant_user`/`admin`. Postup (viz docs/CONTRACTS.md): autentizace -> `Idempotency-Key` -> rychlé kontroly (well-formed XML, Invoice/CreditNote, typ dokladu, **dodavatel = tenant**) -> validace XSD+Schematron -> kontrola `IssueDate` (= dnešní den v Europe/Bratislava; dle `TenantSettings.issue_date_policy` chyba nebo varování) -> uložení se stavem `queued` -> `202 Accepted` + `Location`. Těla: `application/xml` (UBL 2.1, bajty se ukládají beze změny) nebo `application/json` (obálka `InvoiceSubmitEnvelope` s base64 XML). Pro `application/xml` se volby předávají query parametrem `connection_id` a hlavičkou `X-External-Reference`. **Idempotence:** stejný `Idempotency-Key` + stejné tělo (SHA-256 těla) => vrátí se původní odpověď (`202` se stejným `id`, hlavička `Idempotent-Replayed: true`), žádné druhé odeslání. Stejný klíč a jiné tělo => `409 IDEMPOTENCY_CONFLICT`. Souběžné zpracování téhož klíče => `409 IDEMPOTENCY_IN_PROGRESS` (+ `Retry-After`). Klíče se drží min. 7 dní. Nezávisle na klíči brání duplicitě přirozený klíč (tenant, direction, seller, doc_type, number, issue_date) => `409 DUPLICATE_INVOICE` s `existing_invoice_id`. Příjemce neregistrovaný v Peppolu **neblokuje** odeslání; do `warnings` přibude `RECIPIENT_NOT_REGISTERED` (u poštářů s dry-runem `receiverReachable`). Pokud dojde k nedoručení, výsledný stav je `delivery_failed` s důvodem `RECIPIENT_NOT_REGISTERED` (povinnost odeslat je splněna odesláním; fakturu NEvystavujte znovu). **Opětovné odeslání (M-01):** existuje-li faktura se stejným klíčem (číslo, datum, typ, prodavatel) ve stavu `failed_permanent` (kromě důvodu `OUTCOME_UNKNOWN`) nebo `rejected_by_provider`, další `POST` s NOVÝM `Idempotency-Key` ji nahradí novým pokusem (`202`, nová faktura; stará se označí `superseded` a nese `superseded_by_invoice_id`). Jinak `409 DUPLICATE_INVOICE`. Po `failed_permanent/rejected_by_provider` může poštář po pozdním webhooku stav opravit na `delivered`/`delivery_failed`. **Dodavatel = tenant:** shoduje se `EndpointID` (0245:DIČ), DIČ i první `cbc:CompanyID` v `AccountingSupplierParty` (podle něj určuje odesílatele ePodatelna24), jinak `422 SELLER_MISMATCH`. Samofakturace 389/261: odesílatelem je odběratel (`AccountingCustomerParty` = tenant) a tenant musí mít `allow_self_billing` (jinak `422 SELF_BILLING_NOT_ENABLED`). Typ 384 se nepřijímá (`422 UNSUPPORTED_DOCUMENT_TYPE`, použijte 381 + 380). XML > 4 000 000 B => `413 PAYLOAD_TOO_LARGE` u poštáře s tímto limitem (ePodatelna24). parameters: - { $ref: '#/components/parameters/IdempotencyKey' } - name: connection_id in: query description: Odeslat přes konkrétní odchozí spojení (jinak výchozí odchozí poštář tenanta). schema: { type: string, format: uuid } - name: X-External-Reference in: header description: Libovolný odkaz účetního SW (max 200 znaků), vrací se v `external_reference`. schema: { type: string, maxLength: 200 } - { $ref: '#/components/parameters/TestQuery' } - { $ref: '#/components/parameters/TestModeHeader' } - { $ref: '#/components/parameters/SimulateQuery' } - { $ref: '#/components/parameters/SimulateWebhooksQuery' } requestBody: required: true content: application/xml: schema: { type: string, description: "UBL 2.1 Invoice nebo CreditNote (Peppol BIS Billing 3.0)" } application/json: schema: { $ref: '#/components/schemas/InvoiceSubmitEnvelope' } responses: '200': description: | Jen v testovacím režimu (`test=true`): provedlo se vše kromě odeslání, nic se neuložilo do `invoices` ani do fronty. Viz `TestSubmitResponse`. Pověření `test_only` a tenant `sandbox`: viz `GET /erp/me`. `403 TEST_ONLY_CREDENTIAL` při ostrém pokusu s pověřením `test_only`; `400 INVALID_TEST_FLAG` při neplatné hodnotě. headers: X-Test-Mode: { $ref: '#/components/headers/XTestMode' } X-Would-Have-Sent: { $ref: '#/components/headers/XWouldHaveSent' } Idempotent-Replayed: { description: '`true` při přehrání původní testovací odpovědi', schema: { type: string, enum: ['true'] } } content: application/json: schema: { $ref: '#/components/schemas/TestSubmitResponse' } '202': description: Přijato do fronty headers: Location: { description: 'URL stavu `/api/v1/invoices/{id}`', schema: { type: string } } Idempotent-Replayed: { description: '`true` při přehrání původní odpovědi', schema: { type: string, enum: ['true'] } } RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' } RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' } RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' } content: application/json: schema: { $ref: '#/components/schemas/InvoiceAccepted' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /test-runs/{testRunId}: get: tags: [Invoices] operationId: getTestRun summary: Výsledek testovacího odeslání (platí 24 hodin) description: | Vrací výsledek testu (`test_`) vytvořený `POST /invoices` / `POST /erp/invoices` s `test=true`, včetně deterministických `simulated_events[]`. Výsledek se NIKDY nepočítá za doklad ani do statistik. Po 24 hodinách je nedostupný: `410 TEST_RUN_EXPIRED` (řádek se fyzicky nemaže, jen se nezobrazuje). Dostupné cookie session, API klíčem (scope `invoices:read`) i ERP údaji; cizí tenant = `404`. security: [{ cookieAuth: [] }, { bearerApiKey: [] }, { erpBearer: [] }, { erpBasic: [] }] parameters: - { name: testRunId, in: path, required: true, schema: { type: string, pattern: '^test_[0-9a-f-]{36}$' } } responses: '200': description: Testovací běh headers: X-Test-Mode: { $ref: '#/components/headers/XTestMode' } content: application/json: schema: { $ref: '#/components/schemas/TestRun' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '410': description: 'Test je starší než 24 hodin (TEST_RUN_EXPIRED)' content: application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } '429': { $ref: '#/components/responses/TooManyRequests' } '400': { $ref: '#/components/responses/BadRequest' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /erp/test-runs/{testRunId}: get: tags: [ERP] operationId: erpGetTestRun summary: Výsledek testovacího odeslání pro ERP (platí 24 hodin; scope `send` nebo `status`) description: Totéž co `GET /test-runs/{id}`, ale s ERP údaji (ty se přijímají jen na `/erp/*`). security: [{ erpBearer: [] }, { erpBasic: [] }] parameters: - { name: testRunId, in: path, required: true, schema: { type: string, pattern: '^test_[0-9a-f-]{36}$' } } responses: '200': description: Testovací běh headers: X-Test-Mode: { $ref: '#/components/headers/XTestMode' } content: application/json: schema: { $ref: '#/components/schemas/TestRun' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '410': description: 'Test je starší než 24 hodin (TEST_RUN_EXPIRED)' content: application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/summary: get: tags: [Invoices] operationId: getInvoiceSummary summary: Souhrn počtů faktur (pro přehled) description: | Počty faktur aktivního tenanta podle směru a stavu, nad stejnými filtry jako `GET /invoices` (bez `sort`/`limit`/`cursor`). Stavy bez faktur jsou v `by_status` uvedeny s nulou. Scope API klíče `invoices:read`. parameters: - { name: direction, in: query, schema: { $ref: '#/components/schemas/Direction' } } - name: status in: query style: form explode: false schema: type: array items: { $ref: '#/components/schemas/InvoiceStatus' } - { name: validation_status, in: query, schema: { $ref: '#/components/schemas/ValidationStatus' } } - { name: erp_delivered, in: query, description: 'Převzato do ERP (ACK aspoň jednou integrací): `true` = převzato, `false` = nepřevzato (jen příchozí faktury mají smysl).', schema: { type: boolean } } - { name: source, in: query, schema: { $ref: '#/components/schemas/InvoiceSource' } } - { name: connection_id, in: query, schema: { type: string, format: uuid } } - { name: issue_date_from, in: query, schema: { type: string, format: date } } - { name: issue_date_to, in: query, schema: { type: string, format: date } } - { name: currency, in: query, schema: { type: string, pattern: '^[A-Z]{3}$' } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/InvoiceSummary' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/import: post: tags: [Invoices] operationId: importInvoice summary: Ruční import XML (zdroj `manual_import`) description: | Uloží UBL XML bez odeslání. `direction=inbound` (výchozí): buyer musí odpovídat tenantovi, nevalidní doklad se přesto uloží (`received_invalid`). `direction=outbound` (historie): seller musí odpovídat tenantovi, nic se neodesílá. Duplicita (přirozený klíč) => `409 DUPLICATE_INVOICE`. Role `tenant_user`/`admin`. parameters: - { name: direction, in: query, schema: { $ref: '#/components/schemas/Direction' } } requestBody: required: true content: application/xml: schema: { type: string } multipart/form-data: schema: type: object required: [file] properties: file: { type: string, contentEncoding: base64, contentMediaType: application/xml } responses: '201': description: Uloženo content: application/json: schema: { $ref: '#/components/schemas/InvoiceDetail' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '422': { $ref: '#/components/responses/Unprocessable' } '400': { $ref: '#/components/responses/BadRequest' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/validate: post: tags: [Validation] operationId: validateInvoice summary: Dry-run předodesílací validace (NIC se neukládá ani neodesílá) description: | Projde stejný engine jako `POST /invoices` (formát XSD+Schematron, SK národní pravidla, IČO/DIČ/IČ DPH, registry, příjemce v Peppolu, povinnost/zákaz e-faktury, časová pravidla, SK TDD dry-run a dry-run poštáře) a vrátí `ValidationReport`. `Idempotency-Key` se nepoužívá. Zablokovaná faktura (`verdict=blocked`) se vrací jako **200** (dotaz proběhl); `POST /invoices` ve stejné situaci vrací `422 VALIDATION_FAILED` (nebo `OBLIGATION_FORBIDDEN`). Tělo: `application/xml` nebo JSON `{ xml_base64, connection_id?, options? }`. Role `tenant_user`/`admin`, scope `invoices:send`. Výpadek validátoru formátu = `503 VALIDATOR_UNAVAILABLE` (fail-closed); výpadek ostatních vrstev jen nález a stav vrstvy (fail-open). parameters: - name: connection_id in: query description: Odchozí spojení, jehož poštář se použije pro lookup příjemce a dry-run (jinak výchozí odchozí poštář tenanta). schema: { type: string, format: uuid } - name: Accept-Language in: header description: 'Jazyk `message` v nálezech (`sk`, `cs`, `en`); výchozí `en`. Všechny jazyky jsou i v `messages`.' schema: { type: string } - name: X-Check-Routing in: header description: '`1`/`true` = výslovně ověřit příjemce v Peppolu (vrstva `routing`); jinak se spouští jen při `routing_default=always`. Lze i query `check_routing=true` nebo `options.check_routing`.' schema: { type: string, enum: ['1', 'true'] } - name: check_routing in: query description: 'Totéž co hlavička `X-Check-Routing`.' schema: { type: boolean } - name: X-Confidential-Supply in: header description: '`true` = plnění je utajované (e-fakturu nelze vystavit); `false` = uživatel potvrdil, že není. U JSON těla viz `options.confidential_supply`.' schema: { type: string, enum: ['true', 'false'] } requestBody: required: true content: application/xml: schema: { type: string } application/json: schema: { $ref: '#/components/schemas/ValidateInvoiceRequest' } responses: '200': description: 'Report validace. `verdict=blocked` (i doklad, kterému chybí povinná pole, např. `REQUIRED_FIELD_MISSING`) se vrací jako 200 s nálezy; 422 zůstává pro nevalidní XML/kódování, cizího odesílatele aj. (stejné kódy jako odeslání).' content: application/json: schema: { $ref: '#/components/schemas/ValidationReport' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /erp/invoices/validate: post: tags: [ERP, Validation] operationId: erpValidateInvoice summary: Dry-run předodesílací validace pro ERP (scope `send`; NIC se neukládá ani neodesílá) description: | Totéž co `POST /invoices/validate` pro integraci ERP. `Idempotency-Key` se nevyžaduje. Rate limit 20 dry-runů/min na integraci (dry-run poštáře sdílí limit adaptéru). Tělo `application/xml` (spojení z `?connection_id`, příznaky z hlaviček `X-Confidential-Supply`) nebo JSON. security: [{ erpBearer: [] }, { erpBasic: [] }] parameters: - { name: connection_id, in: query, schema: { type: string, format: uuid } } - { name: Accept-Language, in: header, schema: { type: string } } - { name: X-Confidential-Supply, in: header, schema: { type: string, enum: ['true', 'false'] } } - { name: X-Check-Routing, in: header, schema: { type: string, enum: ['1', 'true'] } } - { name: check_routing, in: query, schema: { type: boolean } } requestBody: required: true content: application/xml: schema: { type: string } application/json: schema: { $ref: '#/components/schemas/ValidateInvoiceRequest' } responses: '200': description: 'Report validace. `verdict=blocked` (i doklad, kterému chybí povinná pole, např. `REQUIRED_FIELD_MISSING`) se vrací jako 200 s nálezy; 422 zůstává pro nevalidní XML/kódování, cizího odesílatele aj. (stejné kódy jako odeslání).' content: application/json: schema: { $ref: '#/components/schemas/ValidationReport' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /validation/routing: get: tags: [Validation] operationId: getValidationRouting summary: Ověření příjemce v Peppolu (tlačítko „Ověřit příjemce v Peppol“) description: | Výslovná žádost: zjistí, zda je DIČ příjemce (`0245:`) registrováno v Peppolu – přímo v SMP (je-li v nastavení `registries` zapnuto `routing_smp`), jinak přes `lookupParticipant` výchozího odchozího poštáře tenanta. Výpadek nebo vypnutý zdroj = `registered=null` + `reason` (fail-open, vždy 200). Neexistence v Peppolu je jen informace – odeslání platí za splněnou povinnost. Scope `participants:read`. parameters: - { name: dic, in: query, required: true, schema: { type: string, pattern: '^[0-9]{10}$' } } - { name: connection_id, in: query, schema: { type: string, format: uuid } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ValidationRouting' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /erp/validation/routing: get: tags: [ERP, Validation] operationId: erpValidationRouting summary: Ověření příjemce v Peppolu pro ERP integraci (stejný výsledek jako `GET /validation/routing`) description: | Výslovná žádost (SMP / poštář výchozího odchozího spojení). Výpadek nebo vypnutý zdroj = `registered=null` + `reason` (fail-open, vždy 200). Autentizace ERP (Bearer `erp_…` / Basic), scope `send`. Limit 60 dotazů/min na integraci. security: [{ erpBearer: [] }, { erpBasic: [] }] parameters: - { name: dic, in: query, required: true, schema: { type: string, pattern: '^[0-9]{10}$' } } - { name: connection_id, in: query, schema: { type: string, format: uuid } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ValidationRouting' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/{invoiceId}/validation-runs: get: tags: [Invoices, Validation] operationId: listInvoiceValidationRuns summary: Uložené běhy předodesílací validace faktury (nejnovější první) description: | Záznamy `validation_runs` (append-only) k faktuře: verdikt, vrstvy, verze artefaktů a nálezy každého běhu. Vznikají při přijetí/odeslání faktury; dry-run se neukládá. XML se neukládá, jen jeho `xml_sha256`. Scope `invoices:read`; cizí tenant = `404`. parameters: - { $ref: '#/components/parameters/InvoiceId' } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ValidationRunList' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/{invoiceId}/revalidate: post: tags: [Invoices, Validation] operationId: revalidateInvoice summary: Opakovaná kontrola PŘÍCHOZÍ faktury (nový běh `inbound_recheck`) description: | Nad uloženým XML (beze změny, nikdy se nepřepisuje) znovu spustí vrstvy vhodné pro PŘÍCHOZÍ doklad: formát (XSD + Schematron), slovenská národní pravidla, kontrolu IČO/DIČ/IČ DPH a registry dodavatele, shodu odběratele s tenantem, typ dokladu, časová pravidla a duplicitu/rozpor. Vrstvy jen pro odesílání (příjemce v Peppolu, povinnost/zákaz vystavit, TDD dry-run, dry-run poštáře) se přeskočí. Zapíše nový append-only záznam `validation_runs` (`mode=inbound_recheck`); `validation_status` faktury se změní jen když se změní verdikt (a zapíše se událost). Příchozí doklad se nikdy nezahazuje: jen formát může být chybou, ostatní nálezy jsou nejvýše varování. Výpadek validátoru formátu = `503 VALIDATOR_UNAVAILABLE` (fail-closed), výpadek ostatních vrstev jen nález a stav vrstvy (fail-open). Odchozí faktura = `409 CONFLICT`, faktura bez XML = `409 XML_NOT_AVAILABLE`, cizí tenant = `404`. Rate limit 30/min na tenanta. Scope `invoices:read`, role `tenant_user`/`admin`. Testovací režim (`?test=true` / hlavička `X-Test-Mode`, jinak jako u odeslání): kontrola proběhne a vrátí výsledek, ale NIC se neuloží (žádný běh, změna stavu, událost ani idempotentní záznam; `run.id` je nulové UUID) a odpověď nese `X-Test-Mode: true`. parameters: - { $ref: '#/components/parameters/InvoiceId' } - name: Idempotency-Key in: header required: false description: Volitelný. Stejný klíč + stejná faktura vrátí původní výsledek (`Idempotent-Replayed - true`) bez nového běhu; 1-200 znaků `[A-Za-z0-9._:-]`. schema: { type: string, minLength: 1, maxLength: 200, pattern: '^[A-Za-z0-9._:-]+$' } - name: Accept-Language in: header description: 'Jazyk `message` v nálezech (`sk`, `cs`, `en`); všechny jazyky jsou i v `messages`.' schema: { type: string } responses: '200': description: Nový běh uložen headers: Idempotent-Replayed: { description: 'true = výsledek z dřívějšího volání se stejným Idempotency-Key', schema: { type: string } } content: application/json: schema: { $ref: '#/components/schemas/InvoiceRevalidation' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/Unprocessable' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/revalidate: post: tags: [Invoices, Validation] operationId: revalidateInvoices summary: Hromadná opakovaná kontrola příchozích faktur (asynchronní úloha) description: | Založí úlohu `revalidate_invoices`, která po aktualizaci validátoru nebo pravidel znovu zkontroluje příchozí faktury podle filtru (období vystavení, `validation_status`, `direction` = jen `inbound`, `limit` 1-5000, výchozí 1000; sandbox tenant nejvýše 100). Každá faktura dostane nový běh jako u `POST /invoices/{invoiceId}/revalidate`. Stav úlohy: `GET /invoices/revalidate/jobs/{jobId}`. Na tenanta běží jedna úloha najednou (`409 CONFLICT`). Výpadek validátoru úlohu pozastaví a zopakuje (průběh se zachová). Volitelný `Idempotency-Key`: stejný klíč + stejné tělo = stejná úloha, jiné tělo = `409 IDEMPOTENCY_CONFLICT`. Rate limit 6/min. Scope `invoices:read`, role `tenant_user`/`admin`. parameters: - name: Idempotency-Key in: header required: false description: Volitelný. Stejný klíč + stejná faktura vrátí původní výsledek (`Idempotent-Replayed - true`) bez nového běhu; 1-200 znaků `[A-Za-z0-9._:-]`. schema: { type: string, minLength: 1, maxLength: 200, pattern: '^[A-Za-z0-9._:-]+$' } requestBody: required: false content: application/json: schema: { $ref: '#/components/schemas/RevalidateInvoicesRequest' } responses: '202': description: Úloha založena (nebo existující při opakování se stejným Idempotency-Key) headers: Location: { schema: { type: string } } Idempotent-Replayed: { schema: { type: string } } content: application/json: schema: { $ref: '#/components/schemas/RevalidationJob' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/revalidate/jobs: get: tags: [Invoices, Validation] operationId: listRevalidationJobs summary: Úlohy hromadné opakované kontroly (nejnovější první) description: Scope `invoices:read`; jen úlohy vlastního tenanta. parameters: - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } } - { name: cursor, in: query, schema: { type: string } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/RevalidationJobPage' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/TooManyRequests' } '409': { $ref: '#/components/responses/Conflict' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/revalidate/jobs/{jobId}: get: tags: [Invoices, Validation] operationId: getRevalidationJob summary: Stav úlohy hromadné opakované kontroly description: Počty (`processed`/`changed`/`unchanged`/`failed`), seznam změn stavu a selhání (nejvýše 500). Cizí tenant = `404`. parameters: - { name: jobId, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/RevalidationJob' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/TooManyRequests' } '409': { $ref: '#/components/responses/Conflict' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /validation/layers: get: tags: [Validation] operationId: getValidationLayers summary: Stav vrstev předodesílací validace a jejich verzí description: | Zdraví a verze validátoru (XSD/Schematron/SK/TDD), čerstvost registrů (V-REG), podpora dry-runu u poštářů tenanta (cena, výchozí režim) a účinné nastavení tenanta. Čtení pro všechny role tenanta. responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ValidationLayers' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /validation/excluded-recipients: get: tags: [Validation] operationId: listExcludedRecipients summary: Vlastní seznam příjemců „nikdy e-faktura“ (SIS/VS/utajované plnění) description: Seznam spravuje tenant (žádný oficiální seznam neexistuje). Zásah při odeslání = blokující nález `OBL-EXCLUDED-RECIPIENT` (nelze vypnout). responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ExcludedRecipientList' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } post: tags: [Validation] operationId: createExcludedRecipient summary: Přidání příjemce do seznamu „nikdy e-faktura“ description: Aspoň jedno z `dic` / `ico`. Změna se audituje (`validation.excluded_recipient.added`). Role `tenant_user`/`admin`, cookie session. security: [{ cookieAuth: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ExcludedRecipientCreate' } responses: '201': description: Přidáno content: application/json: schema: { $ref: '#/components/schemas/ExcludedRecipient' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /validation/excluded-recipients/{id}: delete: tags: [Validation] operationId: removeExcludedRecipient summary: Odebrání ze seznamu „nikdy e-faktura“ (soft; záznam zůstává v historii) description: Jen role `admin` (odebrání zmírňuje ochranu). Audituje se (`validation.excluded_recipient.removed`). security: [{ cookieAuth: [] }] parameters: - { name: id, in: path, required: true, schema: { type: string, format: uuid } } responses: '204': { description: Odebráno } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /admin/validation/settings: get: tags: [Admin, Validation] operationId: adminGetValidationSettings summary: Nastavení předodesílací validace (systém, nebo tenant s `tenant_id`) description: | Bez `tenant_id`: systémové výchozí hodnoty (`system_settings` klíč `presend_validation`). S `tenant_id`: řídké přepsání tenanta + účinné hodnoty. Přísnost (`strictness`), vrstvy (`layers`), politika fail-open (`fail_open`), `require_provider_dryrun`, `provider_error_policy`, `severity`, `registry_max_age_days`. Zákonné bloky (`OBL-FORBIDDEN`, `OBL-EXCLUDED-RECIPIENT`) a `PTY-ENDPOINT-SCHEME` nelze vypnout ani přepsat. Jen role admin. security: [{ cookieAuth: [] }] parameters: - { name: tenant_id, in: query, schema: { type: string, format: uuid } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/PresendSettingsDoc' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } put: tags: [Admin, Validation] operationId: adminPutValidationSettings summary: Uložení nastavení předodesílací validace description: | Nahradí uložené hodnoty (systém nebo tenant). Pokus o přepsání závažnosti zákonného bloku / `PTY-ENDPOINT-SCHEME` = `422 INVALID_SETTING`. `expected_version` (optimistický zámek) při neshodě `409 CONFLICT`. Audit `settings.presend.updated`. security: [{ cookieAuth: [] }] parameters: - { name: tenant_id, in: query, schema: { type: string, format: uuid } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PresendSettingsUpdate' } responses: '200': description: Uloženo content: application/json: schema: { $ref: '#/components/schemas/PresendSettingsDoc' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '422': { $ref: '#/components/responses/Unprocessable' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/export: post: tags: [Exports] operationId: createInvoiceExport summary: Hromadný export (ZIP původních XML nebo CSV seznamu) – asynchronní job description: Vyber buď `invoice_ids`, nebo `filter` (stejné parametry jako `GET /invoices`). Výsledek se stahuje přes `GET /exports/{id}/download`, až je `status=ready`. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ExportRequest' } responses: '202': description: Job založen headers: Location: { description: 'URL `/api/v1/exports/{id}`', schema: { type: string } } content: application/json: schema: { $ref: '#/components/schemas/ExportJob' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '422': { $ref: '#/components/responses/Unprocessable' } '409': { $ref: '#/components/responses/Conflict' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '415': { $ref: '#/components/responses/UnsupportedMediaType' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /exports: get: tags: [Exports] operationId: listExports summary: Seznam exportních jobů aktivního tenanta (nejnovější první) description: Po `expires_at` se maže jen odvozený soubor; záznam jobu zůstává (stav `expired`). Scope API klíče `invoices:read`. parameters: - { $ref: '#/components/parameters/Limit' } - { $ref: '#/components/parameters/Cursor' } - { name: status, in: query, schema: { $ref: '#/components/schemas/ExportStatus' } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ExportPage' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /exports/{exportId}: get: tags: [Exports] operationId: getExport summary: Stav exportního jobu parameters: - { name: exportId, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/ExportJob' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /exports/{exportId}/download: get: tags: [Exports] operationId: downloadExport summary: Stažení hotového exportu (ZIP nebo CSV) parameters: - { name: exportId, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': description: Soubor. ZIP obsahuje `__.xml` s přesnými bajty a `manifest.csv` (id, číslo, sha256). headers: Content-Disposition: { schema: { type: string } } content: application/zip: schema: { type: string, format: binary } text/csv: schema: { type: string, format: binary } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/{invoiceId}: get: tags: [Invoices] operationId: getInvoice summary: Detail faktury (stav doručení, validace, události) description: Faktura jiného tenanta => vždy `404` (nikdy 403). parameters: - { $ref: '#/components/parameters/InvoiceId' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/InvoiceDetail' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '400': { $ref: '#/components/responses/BadRequest' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/{invoiceId}/xml: get: tags: [Invoices] operationId: getInvoiceXml summary: Původní XML (bajt po bajtu) description: | Vrací přesně uložené bajty (bez překódování). `ETag: ""` (SHA-256 těla), `Content-Type: application/xml`. Podporuje `If-None-Match` (304). Každý přístup se auditují. parameters: - { $ref: '#/components/parameters/InvoiceId' } - { name: If-None-Match, in: header, schema: { type: string } } responses: '200': description: XML headers: ETag: { description: 'Uvozovkami obalený SHA-256 hex těla', schema: { type: string } } Content-Disposition: { description: 'attachment; filename=".xml"', schema: { type: string } } content: application/xml: schema: { type: string, format: binary } '304': { description: Nezměněno } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/{invoiceId}/source-xml: get: tags: [Invoices] operationId: getInvoiceSourceXml summary: Původní XML přesně tak, jak ho poslalo ERP (před převodem do UTF-8) description: | Vrací **originální bajty** (např. windows-1250) s původním `Content-Type: application/xml; charset=` a `ETag: ""`. Existuje-li převod (kódování ≠ UTF-8 nebo odstraněný BOM), je to zdrojový dokument z `invoice_source_documents` (nemazatelný); jinak je originál shodný s `GET /invoices/{id}/xml` a vrací se uložené XML. Závazná pro archivaci a doručení je UTF-8 verze (`/xml`); originál je důkazní. Stejné role jako `/xml` (včetně `tenant_viewer`), každý přístup se audituje. Podporuje `If-None-Match` (304). Faktura bez XML = `409 XML_NOT_AVAILABLE`. parameters: - { $ref: '#/components/parameters/InvoiceId' } - { name: If-None-Match, in: header, schema: { type: string } } responses: '200': description: Originální bajty headers: ETag: { description: 'Uvozovkami obalený SHA-256 originálu', schema: { type: string } } Content-Disposition: { description: 'attachment; filename=".source.xml"', schema: { type: string } } X-Source-Encoding: { description: Kódování originálu, schema: { type: string } } X-Result-SHA256: { description: 'SHA-256 UTF-8 verze (= ETag `/xml`)', schema: { type: string } } content: application/xml: schema: { type: string, format: binary } '304': { description: Nezměněno } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/TooManyRequests' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /invoices/{invoiceId}/html: get: tags: [Invoices] operationId: getInvoiceHtml summary: Čitelná HTML vizualizace (nezávazná pomůcka, vhodná pro tisk/PDF) description: | Samostatný HTML dokument (`Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:`, bez skriptů). Vkládat do `