openapi: 3.0.3

# ============================================================================
# ePodatelna24 — Outbox Ingestion API (v1)
#
# Machine-to-machine API for handing UBL/Peppol invoices from an external ERP
# to ePodatelna24 for sending over the Peppol network.
#
# Human-readable prose (summaries, descriptions) is in Slovak — this reference
# is read by Slovak ERP integrators. Machine-facing identifiers (operationId,
# `code` enum values, schema/property names, examples) stay in English so the
# contract is stable for code generators.
#
# MODEL (v1, deliberately minimal — "fire-and-forget custody"):
#   The ERP hands over one invoice. ePodatelna24 validates it, verifies the
#   biller and recipient, reserves the send fee, and takes CUSTODY of the
#   document. The single response tells the ERP whether custody was taken.
#   The ERP does NOT track delivery — delivery is monitored by the human user
#   inside the ePodatelna24 dashboard. There are no callbacks/webhooks in v1.
#
#   Because the custody response is the ERP's only feedback, EVERYTHING that
#   can be checked synchronously is checked BEFORE custody is granted:
#     - XML safety (no XXE/DTD)
#     - Peppol BIS 3.0 / EN 16931 schematron validation (rule-level errors)
#     - Sender (AccountingSupplierParty DIČ) is a registered, Peppol-active
#       company under the authenticated wallet
#     - Receiver is reachable on the Peppol network (SMP lookup)
#     - Wallet balance covers the send fee
#   A 202 therefore means: "validated, biller active, recipient reachable,
#   fee reserved — we expect to deliver."
#
# AUTH: same scheme ePodatelna24 uses toward its Access Point — an opaque
#   token in the Authorization header with the "Token" scheme:
#       Authorization: Token ep24api_prod_XXXXXXXXXXXXXXXXXXXXXXXX
#   Tokens are issued and revoked in the ePodatelna24 dashboard, at the wallet
#   level, and are wallet-wide: one token can send for any company under that
#   wallet. The correct sender is chosen per-invoice from the DIČ in the XML.
# ============================================================================

info:
  title: ePodatelna24 Outbox Ingestion API
  version: "1.0.0"
  description: |
    Odosielanie faktúr a dobropisov vo formáte UBL 2.1 (Peppol BIS Billing 3.0 /
    EN 16931) z ERP systému do siete Peppol.

    ## Autentifikácia
    API token vytvorený v eP24 (Peňaženka → API prístup) posielajte v hlavičke
    `Authorization` so schémou **`Token`**:

    ```
    Authorization: Token ep24api_prod_XXXXXXXXXXXXXXXXXXXXXXXX
    ```

    Sandbox tokeny majú prefix `ep24api_test_`.

    Token platí **pre celú peňaženku**: jedným tokenom odošlete faktúry za
    ktorúkoľvek spoločnosť pod danou peňaženkou. Konkrétny odosielateľ sa určí
    z DIČ v bloku `AccountingSupplierParty` každého dokumentu — do požiadavky
    netreba dávať žiadny identifikátor spoločnosti.

    ### Ako sa určí DIČ odosielateľa
    Poradie je záväzné a vyhodnocuje sa v bloku odosielateľa
    (`AccountingSupplierParty`; pri `SelfBilledInvoice` naopak
    `AccountingCustomerParty`):

    1. **prvý** `cbc:CompanyID` v poradí dokumentu — v bežnej faktúre je to
       `PartyTaxScheme/CompanyID` (IČ DPH, napr. `SK2020123456`);
    2. ak jeho hodnota nekončí na 10 číslic, použije sa `cbc:EndpointID`.

    Z vybranej hodnoty sa berie **posledných 10 číslic**. Slovenské IČ DPH je
    `SK` + DIČ, takže krok 1 vráti DIČ. `CompanyID` má prednosť pred
    `EndpointID`; ak sa líšia, `EndpointID` sa ignoruje.

    > **Pozor:** `PartyLegalEntity/CompanyID` nesie IČO (8 číslic), nie DIČ.
    > Ak je vo vašom bloku odosielateľa prvým `CompanyID` identifikátor s
    > **viac než 10 číslicami** (napr. 13-miestny GLN), pravidlo „posledných 10
    > číslic“ z neho vyrobí neplatné DIČ a odoslanie skončí na `403
    > sender_not_found` s DIČ, ktoré nepoznáte. Uistite sa, že prvý `CompanyID`
    > v bloku odosielateľa je daňový identifikátor.

    ## Idempotencia
    Každé odoslanie MUSÍ niesť hlavičku `Idempotency-Key` (UUID, ktoré vaše ERP
    vygeneruje a uloží). Opakované odoslanie **rovnakého kľúča s rovnakým
    dokumentom** vráti pôvodné potvrdenie o prevzatí (`200`) namiesto druhého
    odoslania — je preto bezpečné opakovať pri timeoutoch či sieťových chybách.
    Zhoda dokumentu sa určuje z `sha256` tela požiadavky, bajt po bajte.

    Použitie rovnakého kľúča s **iným dokumentom** je konflikt (`409`). Nič sa
    neuloží ani neúčtuje a pôvodný dokument zostáva nedotknutý. Pre každú
    faktúru vygenerujte nový kľúč — kľúče nikdy nerecyklujte medzi dokumentmi.

    ### Čo sa pri opakovaní vyhodnocuje znova
    Opakovaná požiadavka prejde **celou** vstupnou kontrolou (bezpečnosť XML,
    rozpoznanie odosielateľa, jeho Peppol-aktivácia, schematron, dosiahnuteľnosť
    príjemcu). Kontroly sa nepreskakujú a môžu vrátiť `4xx` aj vtedy, keď
    pôvodné odoslanie uspelo — napríklad ak sa príjemca medzitým z Peppolu
    odregistroval. Nevyhodnocuje sa už len **zostatok** peňaženky: replay
    neúčtuje druhý poplatok, takže `402` pri replay nikdy nenastane.

    ## Validácia
    Dokumenty sa validujú voči vendorovanému, bajt-presnému schematronu Peppol
    BIS 3.0 + EN 16931. Neplatné dokumenty sa odmietnu s `422` a strojovo
    čitateľným zoznamom porušených pravidiel (id pravidla, závažnosť, XPath
    umiestnenie, popis). Zodpovednosť sa v takom prípade nepreberá a nič sa
    neúčtuje. Počas integrácie použite `POST /api/v1/outbox/documents/validate`
    na overenie dokumentu bez prevzatia zodpovednosti a bez poplatku.

    ## Limity
    - **Veľkosť dokumentu: 4 MB.** Nad limit vraciame `413` s telom
      `application/problem+json` a `code: too_large`. Bežná UBL faktúra má
      desiatky kB; limit reálne obmedzuje len prílohy kódované v base64
      (4 MB tela ≈ 3 MB PDF).
    - **Rýchlosť: 120 požiadaviek za minútu na token.** Nad limit `429`
      s hlavičkou `Retry-After`. Limit je aktuálne per-inštancia, takže
      skutočná priepustnosť môže byť vyššia — nespoliehajte sa na to.

    > Telo požiadavky nad ~4,5 MB odmietne infraštruktúra **skôr**, než sa
    > požiadavka dostane k API. Taká odpoveď má stavový kód `413`, ale telo je
    > `text/plain`, nie `application/problem+json`. Pri `413` preto vždy
    > kontrolujte `Content-Type` predtým, než telo parsujete ako JSON.

    ## Čo v1 neobsahuje
    - Žiadny endpoint na stav doručenia ani webhooky. Doručenie sleduje
      používateľ v aplikácii eP24. (Môže pribudnúť neskôr bez porušenia tohto
      kontraktu.)
  contact:
    name: ePodatelna24 Integrations
    url: https://www.epodatelna24.sk
  license:
    name: Proprietary
    url: https://www.epodatelna24.sk/legal/vop

servers:
  - url: https://www.epodatelna24.sk
    description: Produkcia
  # Sandbox project (no real sends, no charges) — for ERP integration testing.
  # Tokens on this host carry the ep24api_test_ prefix.
  - url: https://epodatelna24-sandbox.vercel.app
    description: Sandbox (testovanie integrácie)

security:
  - TokenAuth: []

tags:
  - name: Outbox
    description: Odovzdanie faktúr do eP24 na odoslanie cez sieť Peppol.

paths:
  /api/v1/outbox/documents:
    post:
      tags: [Outbox]
      operationId: submitDocument
      summary: Odoslanie faktúry (prevzatie zodpovednosti)
      description: |
        Zvaliduje UBL faktúru alebo dobropis, overí odosielateľa aj príjemcu,
        rezervuje poplatok za odoslanie z peňaženky a prevezme zodpovednosť za
        doručenie cez Peppol. Pri úspechu (`202`) dokument vlastní eP24 a doručí
        ho — ERP nemusí nič dopytovať.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        description: Surové UBL 2.1 XML (Invoice alebo CreditNote), kódovanie UTF-8.
        content:
          application/xml:
            schema:
              type: string
              format: xml
            example: |
              <?xml version="1.0" encoding="UTF-8"?>
              <Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
                <!-- ... Peppol BIS 3.0 faktúra ... -->
              </Invoice>
      responses:
        '202':
          description: Zodpovednosť prevzatá. eP24 dokument doručí.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustodyReceipt'
        '200':
          description: |
            Idempotentné opakovanie — tento `Idempotency-Key` už bol prijatý s
            rovnakým dokumentom. Vráti pôvodné potvrdenie o prevzatí.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustodyReceipt'
        '400':
          description: |
            Poškodené alebo nebezpečné XML (napr. odmietnuté DTD/entity/XXE),
            prázdne telo, alebo chýbajúca či neplatná hlavička `Idempotency-Key`.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
              examples:
                invalid_xml:
                  value:
                    type: https://www.epodatelna24.sk/errors/invalid_xml
                    title: Invalid XML
                    status: 400
                    code: invalid_xml
                    detail: DOCTYPE declarations are not permitted.
                missing_idempotency_key:
                  value:
                    type: https://www.epodatelna24.sk/errors/missing_idempotency_key
                    title: Missing Idempotency-Key
                    status: 400
                    code: missing_idempotency_key
                    detail: A valid UUID Idempotency-Key header is required.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Zostatok peňaženky nepokrýva poplatok za odoslanie.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
              example:
                type: https://www.epodatelna24.sk/errors/insufficient_funds
                title: Insufficient funds
                status: 402
                code: insufficient_funds
                detail: Wallet balance 0.12 EUR is below the send fee 0.22 EUR.
        '403':
          description: |
            DIČ odosielateľa v dokumente nie je Peppol-aktívna spoločnosť pod
            touto peňaženkou. Najprv spoločnosť zaregistrujte a aktivujte v eP24.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
              examples:
                sender_not_found:
                  value:
                    type: https://www.epodatelna24.sk/errors/sender_not_found
                    title: Sender not found
                    status: 403
                    code: sender_not_found
                    detail: No company with DIČ 1234567890 is registered under this wallet.
                sender_not_active:
                  value:
                    type: https://www.epodatelna24.sk/errors/sender_not_active
                    title: Sender not Peppol-active
                    status: 403
                    code: sender_not_active
                    detail: Company DIČ 1234567890 is registered but not yet Peppol-activated.
        '409':
          description: |
            Tento `Idempotency-Key` už bol použitý s **iným** telom dokumentu
            (porovnáva sa `sha256` tela). Nič sa neuložilo ani neúčtovalo a
            pôvodný dokument zostáva nezmenený. Použite nový kľúč.

            Kľúče použité pred 2026-07-09 nemajú uložený odtlačok tela; kolízia
            s takým dokumentom vráti pôvodné potvrdenie (`200`), nie `409`.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
              example:
                type: https://www.epodatelna24.sk/errors/idempotency_conflict
                title: Idempotency key conflict
                status: 409
                code: idempotency_conflict
                detail: This Idempotency-Key was already used with a different document.
        '413':
          description: |
            Dokument prekračuje maximálnu povolenú veľkosť (4 MB).

            **Telo nemusí byť JSON.** Požiadavky nad ~4,5 MB odmieta
            infraštruktúra ešte pred API a vracia `413` s telom `text/plain`
            (`FUNCTION_PAYLOAD_TOO_LARGE`). Tieto odpovede navyše prichádzajú
            pred overením tokenu, takže `413` môže predbehnúť `401`. Pred
            parsovaním tela ako JSON vždy skontrolujte `Content-Type`.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
              example:
                type: https://www.epodatelna24.sk/errors/too_large
                title: Payload too large
                status: 413
                code: too_large
                detail: Document exceeds the 4 MB limit.
        '422':
          description: |
            Dokument neprešiel validáciou Peppol/EN 16931, alebo príjemca nie je
            dosiahnuteľný v sieti Peppol. Zodpovednosť sa nepreberá a nič sa
            neúčtuje.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ValidationProblem' }
              examples:
                validation_failed:
                  value:
                    type: https://www.epodatelna24.sk/errors/validation_failed
                    title: Peppol validation failed
                    status: 422
                    code: validation_failed
                    detail: The document violates 2 Peppol BIS 3.0 rules.
                    valid: false
                    errors:
                      - rule: PEPPOL-EN16931-R010
                        severity: error
                        location: /Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID
                        message: Buyer electronic address MUST be provided.
                      - rule: BR-CO-15
                        severity: error
                        location: /Invoice/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount
                        message: Invoice total amount with VAT must equal net + VAT total.
                    warnings: []
                receiver_unreachable:
                  value:
                    type: https://www.epodatelna24.sk/errors/receiver_unreachable
                    title: Receiver not reachable on Peppol
                    status: 422
                    code: receiver_unreachable
                    detail: No SMP registration found for participant 0208:0987654321.
                    valid: true
                    errors: []
                    warnings: []
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: |
            Prístupový bod je dočasne nedostupný. Opakujte s exponenciálne
            narastajúcim čakaním a **rovnakým** `Idempotency-Key`.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }

  /api/v1/outbox/documents/validate:
    post:
      tags: [Outbox]
      operationId: validateDocument
      summary: Nezáväzná validácia faktúry (bez prevzatia zodpovednosti)
      description: |
        Vykoná úplne rovnakú validáciu, kontrolu odosielateľa aj dosiahnuteľnosti
        príjemcu ako `submitDocument`, ale **nepreberá** zodpovednosť,
        nerezervuje prostriedky a nič neodosiela. Určené na testovanie
        integrácie a kontrolu pred odoslaním. Vracia `200` s validačným
        protokolom pre platné aj neplatné dokumenty.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      requestBody:
        required: true
        description: Surové UBL 2.1 XML (Invoice alebo CreditNote), kódovanie UTF-8.
        content:
          application/xml:
            schema:
              type: string
              format: xml
      responses:
        '200':
          description: Validačný protokol (dokument môže byť platný aj neplatný).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ValidationReport' }
              examples:
                valid:
                  value:
                    valid: true
                    senderDic: "1234567890"
                    senderActive: true
                    receiver: { scheme: "0208", value: "0987654321" }
                    receiverReachable: true
                    documentType: Invoice
                    errors: []
                    warnings:
                      - rule: PEPPOL-EN16931-W-R001
                        severity: warning
                        location: /Invoice/cbc:Note
                        message: Consider avoiding free-text notes on the invoice.
                invalid:
                  value:
                    valid: false
                    senderDic: "1234567890"
                    senderActive: true
                    receiver: { scheme: "0208", value: "0987654321" }
                    receiverReachable: true
                    documentType: Invoice
                    errors:
                      - rule: BR-16
                        severity: error
                        location: /Invoice/cac:InvoiceLine
                        message: An Invoice must have at least one Invoice line.
                    warnings: []
        '400':
          description: Poškodené alebo nebezpečné XML.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

components:

  securitySchemes:
    TokenAuth:
      type: apiKey
      in: header
      name: Authorization
      description: |
        API token so schémou `Token`. Hodnota hlavičky musí byť doslovne slovo
        `Token`, jedna medzera a potom token:
        `Token ep24api_prod_XXXXXXXXXXXXXXXXXXXXXXXX` (produkcia) alebo
        `Token ep24api_test_XXXXXXXXXXXXXXXXXXXXXXXX` (sandbox).
        Tokeny vytvárate a rušíte v aplikácii eP24 pod
        Peňaženka → API prístup. Token platí pre celú peňaženku.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        UUID, ktoré vaše ERP vygeneruje a natrvalo uloží k tomuto dokumentu.
        Opakované odoslanie rovnakého kľúča s rovnakým dokumentom je bezpečné a
        vráti pôvodné potvrdenie. Uložte si ho, aby opakovania použili ten istý.
      schema:
        type: string
        format: uuid
      example: 5980895f-56b6-4a09-a069-e118a146e622
    IdempotencyKeyOptional:
      name: Idempotency-Key
      in: header
      required: false
      description: Pri nezáväznej validácii nepovinné (zodpovednosť sa nepreberá).
      schema:
        type: string
        format: uuid

  responses:
    Unauthorized:
      description: Chýbajúci alebo neplatný API token.
      headers:
        WWW-Authenticate:
          schema: { type: string }
          description: 'Token'
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
          example:
            type: https://www.epodatelna24.sk/errors/unauthorized
            title: Unauthorized
            status: 401
            code: unauthorized
            detail: The API token is missing, malformed, revoked, or expired.
    RateLimited:
      description: Priveľa požiadaviek. Opakujte po uvedenom čase.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Počet sekúnd, ktoré treba počkať pred opakovaním.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
          example:
            type: https://www.epodatelna24.sk/errors/rate_limited
            title: Too Many Requests
            status: 429
            code: rate_limited
            detail: Request rate exceeded. Retry after 30 seconds.

  schemas:

    CustodyReceipt:
      type: object
      description: |
        Potvrdenie, že eP24 prevzala zodpovednosť za dokument a doručí ho cez
        sieť Peppol. Pre ERP je to doklad o odovzdaní.
      required: [documentId, status, senderDic, receiver, acceptedAt]
      properties:
        documentId:
          type: string
          format: uuid
          description: Identifikátor prijatého dokumentu v eP24. Uložte si ho na spárovanie.
          example: 0b8c7d1e-2f34-4a56-8b9c-1d2e3f4a5b6c
        status:
          type: string
          enum: [accepted]
          description: Pri potvrdení o prevzatí vždy `accepted`.
        idempotencyKey:
          type: string
          format: uuid
          description: Odozva hlavičky Idempotency-Key, ktorá toto potvrdenie vytvorila.
        senderDic:
          type: string
          pattern: '^\d{10}$'
          description: DIČ odosielateľa určené z dokumentu.
          example: "1234567890"
        receiver:
          $ref: '#/components/schemas/PeppolParticipant'
        documentType:
          type: string
          enum: [Invoice, CreditNote]
          description: Rozpoznaný typ UBL dokumentu.
        acceptedAt:
          type: string
          format: date-time
          description: Kedy bola prevzatá zodpovednosť (UTC).
          example: 2026-07-08T09:15:06Z

    ValidationReport:
      type: object
      description: Výsledok nezáväznej validácie (zodpovednosť sa nepreberá).
      required: [valid, errors, warnings]
      properties:
        valid:
          type: boolean
          description: True iba vtedy, ak neexistuje žiadny problém so závažnosťou error.
        senderDic:
          type: string
          pattern: '^\d{10}$'
          nullable: true
        senderActive:
          type: boolean
          description: Či je spoločnosť odosielateľa registrovaná a Peppol-aktívna pod touto peňaženkou.
        receiver:
          # `type` is required alongside `nullable` in OAS 3.0 — without it the
          # spec fails validation and example checks error out.
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PeppolParticipant'
        receiverReachable:
          type: boolean
          description: Či bol príjemca nájdený v sieti Peppol (vyhľadanie v SMP).
        documentType:
          type: string
          enum: [Invoice, CreditNote]
          nullable: true
        errors:
          type: array
          items: { $ref: '#/components/schemas/ValidationIssue' }
        warnings:
          type: array
          items: { $ref: '#/components/schemas/ValidationIssue' }

    ValidationIssue:
      type: object
      description: Výsledok jedného pravidla Peppol/EN 16931.
      required: [rule, severity, message]
      properties:
        rule:
          type: string
          description: Identifikátor pravidla (Peppol/EN 16931), napr. PEPPOL-EN16931-R010 alebo BR-CO-15.
          example: PEPPOL-EN16931-R010
        severity:
          type: string
          enum: [fatal, error, warning]
        location:
          type: string
          nullable: true
          description: XPath umiestnenie chybného uzla, ak je dostupné.
          example: /Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID
        message:
          type: string
          description: Zrozumiteľný popis pravidla.

    PeppolParticipant:
      type: object
      description: Identifikátor účastníka siete Peppol (schéma + hodnota).
      required: [scheme, value]
      properties:
        scheme:
          type: string
          description: Schéma Peppol EAS/ICD, napr. 0208 (BE), 0245 (register SK).
          example: "0208"
        value:
          type: string
          description: Hodnota identifikátora v rámci schémy.
          example: "0987654321"

    Problem:
      type: object
      description: Chybový objekt v štýle RFC 7807 (application/problem+json).
      required: [title, status, code]
      properties:
        type:
          type: string
          format: uri
          description: URI identifikujúce druh chyby.
        title:
          type: string
          description: Krátke, zrozumiteľné zhrnutie.
        status:
          type: integer
          description: HTTP stavový kód.
        code:
          type: string
          description: Stabilný, strojovo čitateľný kód chyby — vetvite podľa neho vo svojom ERP.
          enum:
            - invalid_xml
            - unauthorized
            - insufficient_funds
            - sender_not_found
            - sender_not_active
            - idempotency_conflict
            - missing_idempotency_key
            - too_large
            - validation_failed
            - receiver_unreachable
            - rate_limited
            - upstream_unavailable
        detail:
          type: string
          description: Zrozumiteľné vysvetlenie konkrétneho výskytu.

    ValidationProblem:
      description: |
        Problem vrátený pri `422`, ktorý navyše nesie výsledky jednotlivých
        pravidiel, aby ERP mohlo zobraziť presné chyby.
      allOf:
        - $ref: '#/components/schemas/Problem'
        - type: object
          properties:
            valid:
              type: boolean
              example: false
            errors:
              type: array
              items: { $ref: '#/components/schemas/ValidationIssue' }
            warnings:
              type: array
              items: { $ref: '#/components/schemas/ValidationIssue' }
