openapi: 3.1.0
info:
  title: DokuTéka Integrációs API
  version: 0.1.0
  summary: Partnerrendszerek (könyvelőprogramok) által hívott, szűk és stabil felület dokumentum küldésére.
  description: |
    Ez a szerződés azt írja le, amit **egy másik program** hív — nem azt, amit a DokuTéka
    saját felülete. A kettő szándékosan külön fájl: a belső API a felülettel együtt
    mozog és bármikor változhat, ez viszont egy partner kódjában él, amit nem mi
    telepítünk. Ha összeolvasztanánk őket, minden felületi átalakítás egy idegen
    fejlesztőcsapat dolga is lenne.

    ## Mit vált ki

    Az iroda a könyvelőprogramban (a kiváltó eset: **Pénzszám**) állítja elő a
    dokumentumot, például a folyószámla-kivonatot. Ahhoz, hogy ez eljusson az ügyfélhez,
    ma végig kell menni a *mentés → átlépés a böngészőbe → tallózás → feltöltés* köröm.
    Négy kézi lépés olyasmiért, amit a küldő rendszer már tud. Az API ezt a súrlódást
    szünteti meg.

    ## Két üzemmód, mert két különböző kérdésre válaszolnak

    | | `POST /deliveries` — közvetlen kézbesítés | `POST /handoffs` — előkészített átadás |
    |---|---|---|
    | Ki dönt a küldésről | a gép, ember nélkül | a felhasználó, a DokuTéka felületén |
    | Mire jó | sok tétel egyszerre (kétszáz kivonat) | egy tétel, amihez emberi szem kell |
    | Mit hoz létre | kézbesített dokumentum | piszkozat + egy megnyitandó hivatkozás |
    | Kell hozzá bejelentkezés | nem | igen, a felületen |
    | Mit tud a kulcs önmagában | kézbesíteni | **csak előkészíteni** |

    A kettő nem helyettesíti egymást, és nem is zárja ki. Az előkészített átadás
    biztonságilag erősebb — a kulcs birtokosa nem tud iratot kiléptetni az irodából, csak
    a felület elé tenni —, de emberi lépést igényel, tehát nem skálázik. A közvetlen
    kézbesítés skálázik, de a felelősséget a kulcsra teszi. Az iroda a kulcs
    **hatóköreivel** dönti el, melyiket engedi: kiadható olyan kulcs, ami kizárólag
    előkészíteni tud.

    ## A kulcsról

    A kulcs **irodához** tartozik, nem felhasználóhoz — nem egy ember nevében cselekszik,
    hanem a rendszer nevében. Az iroda adminja adja ki, nézi meg és vonja vissza a
    felületen (Iroda → Integrációk). A titkot **egyetlen egyszer**, a létrehozáskor
    adjuk vissza; utána már csak az előtagja látszik (`dtk_live_7f3a…`), mert amit
    visszaolvasni is lehet, azt el is lehet lopni észrevétlenül.

    A hívások nem névtelenek: minden integráción keresztül létrejött dokumentum
    esemény-naplójába bekerül a kulcs neve, tehát a megjegyzés-fonalon látszik, hogy
    "Pénzszám (API)" töltötte fel, nem egy kolléga. Ez nem könyvelési formalitás — enélkül
    egy ügyfél kérdésére ("ezt ki küldte?") nincs válasz.

    ## Címzés adószámmal

    A partnerrendszer nem ismer DokuTéka-azonosítót, viszont ismeri az ügyfél
    **adószámát** — ez a címzés elsődleges kulcsa. A DokuTéka oldalán ez új adat: az
    ügyfél adatlapján kell megadni, és irodán belül egyedinek kell lennie, különben a
    címzés kétértelmű. Ahol nincs adószám (magánszemély ügyfél), ott a `clientId`
    használható; a `GET /clients` végpont pontosan azért van, hogy a párosítás
    ellenőrizhető és felépíthető legyen.

    ## Ami szándékosan nincs benne

    - **Webhook.** Az állapotot kérdezni kell (`GET /documents`), nem küldjük. Egy
      megbízható webhook regisztrációt, aláírást, újrapróbálkozást és sorbanállást
      jelent — önálló alrendszer. Amíg nincs egyetlen partner sem, aki üzemeltetni tudja
      a fogadó végét, a lekérdezés fedi az igényt (megérkezett-e, megnyitották-e).
    - **Fájl nélküli üzenet.** A felületen létező lehetőség, de egy könyvelőprogramnak
      nincs mit üzennie; a felület jobb hely erre.
    - **Törlés, visszavonás a késleltetési ablakon túl, verziófeltöltés.** Az integráció
      küld; ami utána történik a dokumentummal, az emberi döntés a felületen.
    - **Ügyfél létrehozása.** Egy hibás adószám ne hozzon létre új ügyfelet — az ilyen
      hiba csendben duplikálná az ügyféltörzset. Nem talált címzett esetén `404`.

    ## Állapot: tervezet

    **A v0.1.0 review alatt áll, implementáció még nincs.** A DokuTéka szerződés-első
    fejlesztéssel készül: a leíró jóváhagyása után következik a megvalósítás. A jóváhagyás
    része a belső oldalon szükséges két változás is, amit ez a fájl feltételez: az ügyfél
    adószám mezője és a kulcskezelő felület. Ezeket a
    [`docs/integracio-api.md`](https://gitea.com/Financia/DokuTeka/src/branch/main/docs/integracio-api.md)
    írja le.

  contact:
    name: DokuTéka
    url: https://dokuteka.hu
  license:
    name: Zárt, szerződéshez kötött használat
    url: https://dokuteka.hu/impresszum.html

servers:
  - url: https://api.dokuteka.hu/v1
    description: Éles környezet
  - url: http://localhost:8080/integration/v1
    description: Helyi fejlesztői környezet

tags:
  - name: Azonosítás
    description: |
      A kulcs érvényességének és hatóköreinek ellenőrzése. Ez az a hívás, amivel egy
      integráció beüzemelése kezdődik, és amit az iroda adminja a felületen a
      "Kulcs kipróbálása" gombbal indít.
  - name: Törzsadat
    description: |
      Amit a partnerrendszernek egyszer ki kell kérnie, hogy érvényes küldést tudjon
      összeállítani: az ügyfelek adószám-párosítása és az iroda dokumentumtípusai.
  - name: Közvetlen kézbesítés
    description: |
      A gép küld, ember nélkül. Egy hívás egy dokumentumot kézbesít egy ügyfélnek.
  - name: Előkészített átadás
    description: |
      A gép előkészít, az ember küld. A hívás piszkozatot hoz létre és egy hivatkozást
      ad, amit megnyitva a felhasználó a kitöltött küldés-képernyőn találja magát.
  - name: Állapot
    description: |
      Mi történt a kiküldött dokumentummal: megérkezett, megnyitották, letöltötték,
      visszajeleztek rá.

security:
  - apiKey: []

paths:
  # =================================================================== Azonosítás
  /whoami:
    get:
      tags: [Azonosítás]
      operationId: whoami
      summary: A kulcs azonossága, hatókörei és korlátai
      description: |
        Visszaadja, melyik irodához tartozik a kulcs, mit szabad vele, és milyen korlátok
        érvényesek rá. Hatókört nem igényel — bármelyik érvényes kulcs hívhatja.

        Egy integráció beüzemelése ezzel kezdődik: ha ez a hívás nem `200`-at ad, minden
        további hibakeresés az okot itt találja meg, nem a küldésben. Ugyanezt hívja az
        iroda adminja a felületen, amikor kipróbálja a kulcsot.
      responses:
        '200':
          description: A kulcs érvényes.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Identity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ==================================================================== Törzsadat
  /clients:
    get:
      tags: [Törzsadat]
      operationId: listClients
      summary: Az iroda ügyfelei, adószámmal
      description: |
        A címzés-párosítás felépítéséhez és ellenőrzéséhez. A partnerrendszer ezt egyszer,
        beüzemeléskor kéri ki, majd időnként újra — nem küldésenként.

        **Szükséges hatókör:** `clients:read`

        A találatok között ott van a felfüggesztett (`SUSPENDED`) ügyfél is, `acceptsDelivery: false`
        jelöléssel. Aki hiányzik a listából, azt nem lehet kiszűrni a hibából: egy eltűnt
        ügyfél és egy felfüggesztett ügyfél között a partnernek látnia kell a különbséget.
      x-dokuteka-scope: clients:read
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/SizeParam'
        - name: taxNumber
          in: query
          description: |
            Szűrés adószámra. A kötőjelek nem számítanak: a `12345678-1-42` és a
            `12345678142` ugyanazt találja meg.
          schema:
            $ref: '#/components/schemas/TaxNumber'
        - name: query
          in: query
          description: Szabad szöveges keresés az ügyfél nevében.
          schema:
            type: string
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/ClientStatus'
      responses:
        '200':
          description: Ügyféllista.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /document-types:
    get:
      tags: [Törzsadat]
      operationId: listDocumentTypes
      summary: Az iroda dokumentumtípusai
      description: |
        A küldéshez `documentTypeId` kell, mert a típus irodánként karbantartott, zárt
        lista. A partnerrendszer ezt beüzemeléskor kéri ki és eltárolja a párosítást
        (például "folyószámla-kivonat" → azonosító).

        **Szükséges hatókör:** `clients:read`

        Az azonosítót és nem a nevet kéri a küldés, mert a nevet az iroda bármikor
        átírhatja a felületen — egy névre kötött integráció egy átnevezéstől elromlik,
        és a hiba csak a következő küldésnél derül ki.
      x-dokuteka-scope: clients:read
      responses:
        '200':
          description: Dokumentumtípusok.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentTypeListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ======================================================= Közvetlen kézbesítés
  /deliveries:
    post:
      tags: [Közvetlen kézbesítés]
      operationId: createDelivery
      summary: Dokumentum kézbesítése egy ügyfélnek, emberi lépés nélkül
      description: |
        Egy hívás = egy dokumentum egy ügyfélnek. A dokumentum létrejön és a kiküldés
        azonnal elindul; a felületen ugyanaz történik, mint amikor egy kollega feltölt és
        kiküld.

        **Szükséges hatókör:** `documents:send`

        ### Egy hívás egy fájl — és miért nem kötegel

        Kétszáz kivonat kétszáz hívás. Ez szándékos: egy köteg-végpontnál minden hívás
        részlegesen sikerülhet, és a partnernek a válaszból kellene kibányásznia, melyik
        harminc tétel nem ment át. Külön hívásoknál minden tételnek saját HTTP-státusza,
        saját `Idempotency-Key`-e és saját újrapróbálkozása van. A percenkénti korlát
        (`GET /whoami`) ehhez a nagyságrendhez van szabva.

        ### Az `Idempotency-Key` kötelező

        Ez az egyetlen kötelező fejléc az API-ban. Egy időtúllépés után a partner nem
        tudja, megtörtént-e a küldés; ha ilyenkor újrapróbálkozik kulcs nélkül, az ügyfél
        kétszer kapja meg ugyanazt a kivonatot. Ugyanazzal a kulccsal ismételt hívás a
        **korábbi választ** adja vissza újra, nem küld újat. Ha ugyanaz a kulcs más
        tartalommal jön, az `409` — ez programozási hiba, nem újrapróbálkozás.

        A kulcs 24 órán át él. Javasolt formája a partner saját bizonylat-azonosítója,
        mert az természetesen egyedi és a hibakeresésben is beszédes.

        ### Visszavonás és a `dryRun`

        A kézbesítés az iroda beállítása szerinti késleltetéssel (alapból 30 mp) válik
        láthatóvá, addig visszavonható (`POST /deliveries/{deliveryId}/cancellation`).
        **Kötegnél erre ne számítson senki:** kétszáz tételt harminc másodperc alatt
        egyenként visszavonni nem lehet. A köteg védőháza ezért nem a visszavonás, hanem
        a `dryRun` — végigfut az egész ellenőrzésen (címzett, típus, fájltípus, keret,
        előfizetés), és megmondja, mi történne, de nem hoz létre semmit. Egy éles köteg
        előtt ez az a lépés, ami a hibás adószámot még a kiküldés előtt megtalálja.
      x-dokuteka-scope: documents:send
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CreateDeliveryForm'
            encoding:
              file:
                contentType: application/pdf, image/png, image/jpeg, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, text/csv, text/plain
      responses:
        '200':
          description: |
            Csak `dryRun=true` esetén: az ellenőrzés eredménye. Nem jött létre semmi.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeliveryDryRunResult'
        '201':
          description: A dokumentum létrejött, a kiküldés elindult.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Delivery'
        '400':
          description: Hiányzó vagy formátumában érvénytelen `Idempotency-Key` fejléc.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: |
            A címzett vagy a dokumentumtípus nem található. A `type` megmondja, melyik:
            `…/problems/client-not-found`, illetve `…/problems/document-type-not-found`.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: |
            Vagy ugyanaz az `Idempotency-Key` jött más tartalommal
            (`…/problems/idempotency-key-reused`), vagy az adószám az irodán belül
            több ügyfélre illik, tehát a címzés kétértelmű
            (`…/problems/client-ambiguous`). Utóbbi adathiba: a felületen kell rendezni.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /deliveries/{deliveryId}:
    get:
      tags: [Közvetlen kézbesítés]
      operationId: getDelivery
      summary: Egy kézbesítés adatai és állapota
      description: |
        **Szükséges hatókör:** `documents:read`
      x-dokuteka-scope: documents:read
      parameters:
        - $ref: '#/components/parameters/DeliveryId'
      responses:
        '200':
          description: A kézbesítés adatai.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Delivery'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /deliveries/{deliveryId}/cancellation:
    post:
      tags: [Közvetlen kézbesítés]
      operationId: cancelDelivery
      summary: Kézbesítés visszavonása a késleltetési ablakon belül
      description: |
        A dokumentum visszakerül piszkozat állapotba, az ügyfél nem látta és nem is fogja
        látni. Az ablak hosszát a `GET /whoami` `recallWindowSeconds` értéke mondja meg;
        ha az iroda kikapcsolta a visszavonást, ez a hívás `409`.

        **Szükséges hatókör:** `documents:send`
      x-dokuteka-scope: documents:send
      parameters:
        - $ref: '#/components/parameters/DeliveryId'
      responses:
        '200':
          description: Visszavonva.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Delivery'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: |
            Az ablak lejárt, vagy a visszavonás ki van kapcsolva az irodánál. A kézbesítés
            megtörtént — innen csak a felületen, emberi döntéssel lehet tovább menni.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  # ======================================================== Előkészített átadás
  /handoffs:
    post:
      tags: [Előkészített átadás]
      operationId: createHandoff
      summary: Piszkozat előkészítése, amit a felhasználó a felületen küld el
      description: |
        A hívás feltölti a fájlt és összeállítja a küldést, de **nem küld**. A válasz egy
        `openUrl` hivatkozás: a partnerrendszer ezt nyitja meg a böngészőben, a felhasználó
        pedig a kitöltött küldés-képernyőn találja magát — megnézheti, megjegyzést fűzhet
        hozzá, és ő indítja a kiküldést.

        **Szükséges hatókör:** `documents:prepare`

        ### Az `openUrl` nem belépő

        A benne lévő token **egyszer használható, 30 percig érvényes, és nem hitelesít.**
        Csak megnevezi az előkészített piszkozatot. Aki megnyitja és nincs bejelentkezve,
        az a bejelentkezéshez jut, utána vissza ide; aki be van jelentkezve, de nem ennek
        az irodának a tagja, az `403`-at kap a felületen. Ez a lényegi különbség a
        közvetlen kézbesítéshez képest: a kiszivárgott előkészítő kulcs sem tud iratot
        kiléptetni az irodából, mert a küldéshez mindig kell egy belépett ember.

        Ezért nem kellett a vágólapos megoldás sem: ott a fájlnév utaztatása és a rá
        épített automatizmus ugyanezt a határt mosta volna el.

        ### `returnUrl`

        Megadható, hova térjen vissza a felhasználó a sikeres küldés után. **Csak a
        kulcshoz a felületen felvett címekre mutathat**, egyébként `422`. Szabad
        átirányítás egy dokumentumkezelőben nyílt adathalász-felület lenne: a hivatkozás a
        mi tanúsítványunkkal indul, és bárhová vinne.
      x-dokuteka-scope: documents:prepare
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CreateHandoffForm'
      responses:
        '201':
          description: Az előkészítés megtörtént, a hivatkozás megnyitható.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Handoff'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: A címzett vagy a dokumentumtípus nem található.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: Kétértelmű adószám, vagy más tartalommal ismételt `Idempotency-Key`.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /handoffs/{handoffId}:
    get:
      tags: [Előkészített átadás]
      operationId: getHandoff
      summary: Mi lett az előkészített átadással
      description: |
        A partnerrendszer ebből tudja meg, hogy a felhasználó elküldte-e (`SENT`), eldobta-e
        (`DISCARDED`), vagy hozzá sem ért, míg le nem járt (`EXPIRED`). Enélkül a Pénzszám
        oldalán nem lehet megmutatni, hogy a kivonat kiment-e.

        **Szükséges hatókör:** `documents:prepare`
      x-dokuteka-scope: documents:prepare
      parameters:
        - $ref: '#/components/parameters/HandoffId'
      responses:
        '200':
          description: Az átadás állapota.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Handoff'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Előkészített átadás]
      operationId: discardHandoff
      summary: Előkészített átadás visszavonása, amíg nem küldték el
      description: |
        Ha a partnerrendszerben törlik vagy újragenerálják a bizonylatot, az előkészített
        piszkozat feleslegessé válik. Ez a hívás eldobja, és a hivatkozás érvénytelen lesz.
        Már elküldött átadásra `409` — akkor a dokumentum az ügyfélnél van, azt innen
        elérni nem lehet.

        **Szükséges hatókör:** `documents:prepare`
      x-dokuteka-scope: documents:prepare
      parameters:
        - $ref: '#/components/parameters/HandoffId'
      responses:
        '204':
          description: Eldobva.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Az átadást már elküldték; ezen az úton nincs mit visszavonni.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  # ====================================================================== Állapot
  /documents:
    get:
      tags: [Állapot]
      operationId: listIntegrationDocuments
      summary: Az integráción keresztül létrejött dokumentumok
      description: |
        Egyeztetéshez: mit küldtünk ki, megérkezett-e, megnyitották-e. **Csak azok a
        dokumentumok szerepelnek benne, amelyek ennek az irodának az integrációján
        keresztül jöttek létre** — a felületen feltöltött anyagokat ez a felület nem
        mutatja meg.

        **Szükséges hatókör:** `documents:read`

        Ez nem szűkmarkúság, hanem a kulcs hatókörének komolyan vétele: egy
        könyvelőprogramnak nincs dolga az iroda teljes dokumentumforgalmával, és egy
        elveszett kulccsal se legyen.
      x-dokuteka-scope: documents:read
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/SizeParam'
        - name: clientId
          in: query
          schema:
            type: string
            format: uuid
        - name: taxNumber
          in: query
          description: Címzett szűrése adószámmal, a `clientId` alternatívájaként.
          schema:
            $ref: '#/components/schemas/TaxNumber'
        - name: period
          in: query
          description: "Formátum: YYYY-MM"
          schema:
            $ref: '#/components/schemas/Period'
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/DocumentStatus'
        - name: createdFrom
          in: query
          description: Csak az ettől az időponttól létrehozott tételek.
          schema:
            type: string
            format: date-time
        - name: createdUntil
          in: query
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Találati lista.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationDocumentListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /documents/{documentId}:
    get:
      tags: [Állapot]
      operationId: getIntegrationDocument
      summary: Egy dokumentum állapota
      description: |
        **Szükséges hatókör:** `documents:read`

        A tartalom itt nem érhető el, és nem is lesz: a fájlt visszaolvasni csak hitelesített
        felhasználói kérésen lehet. Egy küldő integrációnak nincs szüksége arra, hogy
        letöltsön — a titkosított tárolás értelme veszne el, ha egy irodánál elhelyezett
        kulcs vissza tudná fejteni a teljes archívumot.
      x-dokuteka-scope: documents:read
      parameters:
        - $ref: '#/components/parameters/DocumentId'
      responses:
        '200':
          description: A dokumentum állapota.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationDocument'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer dtk_live_…` — az iroda integrációs kulcsa.

        Nem JWT, nincs lejárata és nem kell megújítani; addig érvényes, amíg az iroda
        vissza nem vonja. A `bearer` sémát azért használjuk egyedi fejléc helyett, mert
        minden HTTP-kliens és minden generált SDK ismeri, tehát nem kell hozzá kézzel írt
        kód a partner oldalán.

        Kizárólag HTTPS-en. A kulcs nem kerülhet lekérdezési paraméterbe: onnan
        webkiszolgáló-naplóba és böngésző-előzménybe kerül, és a naplónkból soha nem lehet
        biztonsággal kitörölni.

  parameters:
    PageParam:
      name: page
      in: query
      schema:
        type: integer
        minimum: 0
        default: 0
    SizeParam:
      name: size
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    IdempotencyKeyRequired:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        A hívás egyedi azonosítója a partner rendszerében, 24 órán át megjegyezve.
        Ugyanezzel a kulccsal ismételt hívás nem küld újat, hanem a korábbi választ adja
        vissza. Javasolt a partner saját bizonylat-azonosítója.
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: '^[A-Za-z0-9._:-]+$'
      example: penzszam-2026-09-KIV-004182
    IdempotencyKeyOptional:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Nem kötelező, mert egy megkettőzött előkészítés nem kézbesít semmit — csak egy
        fölös piszkozatot hagy a felületen. Megadva viszont ugyanúgy véd, mint a
        kézbesítésnél.
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: '^[A-Za-z0-9._:-]+$'
    DeliveryId:
      name: deliveryId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    HandoffId:
      name: handoffId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    DocumentId:
      name: documentId
      in: path
      required: true
      schema:
        type: string
        format: uuid

  headers:
    XRequestId:
      description: |
        A kérés azonosítója. Ugyanez az érték szerepel a szerver naplósoraiban, tehát egy
        hibabejelentésnél ez az egyetlen adat, amivel a konkrét hívás megtalálható.
      schema:
        type: string
    RateLimitLimit:
      description: A percenkénti kérés-keret erre a kulcsra.
      schema:
        type: integer
    RateLimitRemaining:
      description: Hány kérés maradt az aktuális percben.
      schema:
        type: integer
    RetryAfter:
      description: Ennyi másodpercet kell várni az újrapróbálkozás előtt.
      schema:
        type: integer

  responses:
    Unauthorized:
      description: |
        Hiányzó, visszavont vagy érvénytelen kulcs. A válasz nem tesz különbséget a
        "nincs ilyen kulcs" és a "visszavontuk" eset között — a különbség csak annak
        adna információt, aki kulcsokat próbálgat.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Forbidden:
      description: |
        A kulcs érvényes, de a művelethez nincs hatóköre, vagy az iroda kikapcsolta az
        integrációt, vagy az iroda fel van függesztve. A `detail` megmondja, melyik.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: A megadott erőforrás nem létezik, vagy nem ehhez az irodához tartozik.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    PaymentRequired:
      description: |
        Az iroda előfizetése lejárt, a türelmi idő letelt: a rendszer csak olvasható
        módban van. Az olvasó végpontok (`/whoami`, `/clients`, `/documents`) továbbra is
        működnek — dokumentumot elérni fizetési késedelem miatt sem zárunk el —, de küldeni
        és előkészíteni nem lehet.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    UnprocessableEntity:
      description: |
        Tartalmi hiba: nem engedélyezett fájltípus, a kiterjesztésével nem egyező
        tartalom, hiányzó kötelező mező, érvénytelen `period`, vagy nem engedélyezett
        `returnUrl`. Az `errors` mező mezőre bontva mondja meg, mi a baj.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    PayloadTooLarge:
      description: A fájl nagyobb 25 MB-nál.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    TooManyRequests:
      description: |
        Túllépett kérés-keret. A `Retry-After` megmondja, mennyit kell várni; a kötegelt
        küldésnek erre fel kell készülnie, mert kétszáz tételnél ez nem kivétel, hanem
        üzemszerű állapot.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'

  schemas:
    # ------------------------------------------------------------ Alaptípusok
    TaxNumber:
      type: string
      description: |
        Magyar adószám, kötőjelekkel vagy nélkül. A rendszer a csak számjegyekből álló
        alakra normalizálva tárolja és keresi, tehát a két írásmód ugyanaz az ügyfél.
      pattern: '^[0-9]{8}-?[1-9]-?[0-9]{2}$'
      examples:
        - 12345678-1-42
        - 12345678142

    Period:
      type: string
      description: A dokumentum által érintett időszak, év és hónap.
      pattern: '^\d{4}-(0[1-9]|1[0-2])$'
      examples:
        - 2026-08

    Scope:
      type: string
      description: |
        Egy kulcs hatóköre. Az iroda a kulcs kiadásakor választja meg, mit engedélyez.

        - `clients:read` — ügyfelek és dokumentumtípusok olvasása a párosításhoz
        - `documents:prepare` — előkészített átadás; küldeni nem tud
        - `documents:send` — közvetlen kézbesítés, ember nélkül
        - `documents:read` — az integráción keresztül létrejött tételek állapota
      enum:
        - clients:read
        - documents:prepare
        - documents:send
        - documents:read

    ClientStatus:
      type: string
      enum: [ACTIVE, SUSPENDED]

    DocumentStatus:
      type: string
      description: |
        A tétel útja. `DRAFT` az integráción keresztül csak előkészített átadásnál
        fordul elő; `PENDING_RECALL` a késleltetési ablakban lévő kézbesítés.
      enum: [DRAFT, PENDING_RECALL, DELIVERED, VIEWED, DOWNLOADED, DELETED]

    FeedbackResult:
      type: string
      description: Az ügyfél visszajelzése, ha kértünk és adott.
      enum: [OK, PROBLEM]

    WaitingOn:
      type: string
      description: Kinél van a labda — kitől várunk lépést a tételen.
      enum: [OFFICE, CLIENT, NOBODY]

    PageMeta:
      type: object
      properties:
        page:
          type: integer
        size:
          type: integer
        totalElements:
          type: integer
          format: int64
        totalPages:
          type: integer

    Problem:
      type: object
      description: |
        RFC 9457 szerinti hiba-válasz, ugyanabban az alakban, mint a DokuTéka belső API-jában.
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
          description: |
            A hiba állandó azonosítója — erre érdemes programot írni, nem a `title`
            szövegére, mert az fordítható és csiszolható.
          examples:
            - https://api.dokuteka.hu/problems/client-not-found
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        requestId:
          type: string
          description: Ugyanaz, mint az `X-Request-Id` fejléc — hibabejelentéshez ezt küldje.
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ValidationErrorItem'

    ValidationErrorItem:
      type: object
      required: [field, message]
      properties:
        field:
          type: string
        message:
          type: string

    # ------------------------------------------------------------- Azonosítás
    Identity:
      type: object
      description: A kulcs mögötti iroda és a kulcsra érvényes szabályok.
      properties:
        officeId:
          type: string
          format: uuid
        officeName:
          type: string
        keyId:
          type: string
          format: uuid
        keyName:
          type: string
          description: Az iroda által adott név (például "Pénzszám – éles").
        keyPrefix:
          type: string
          description: A kulcs első, azonosításra használható néhány karaktere.
          examples:
            - dtk_live_7f3a
        scopes:
          type: array
          items:
            $ref: '#/components/schemas/Scope'
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
        integrationEnabled:
          type: boolean
          description: |
            Az iroda egyetlen kapcsolója az egész integrációra. Ha `false`, minden író
            hívás `403` — kulcsonkénti visszavonás nélkül is le lehet állítani mindent.
        recallWindowSeconds:
          type: integer
          description: |
            Ennyi ideig visszavonható egy kézbesítés. `0`, ha az iroda kikapcsolta a
            visszavonást.
        readOnly:
          type: boolean
          description: Igaz, ha az előfizetés lejárt és a rendszer csak olvasható módban van.
        rateLimitPerMinute:
          type: integer
        maxFileSizeBytes:
          type: integer
          format: int64
        allowedReturnUrlPrefixes:
          type: array
          description: A kulcshoz engedélyezett `returnUrl` előtagok.
          items:
            type: string

    # -------------------------------------------------------------- Törzsadat
    IntegrationClient:
      type: object
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
        taxNumber:
          $ref: '#/components/schemas/TaxNumber'
        status:
          $ref: '#/components/schemas/ClientStatus'
        acceptsDelivery:
          type: boolean
          description: |
            Fogadhat-e most küldést. Hamis, ha az ügyfél fel van függesztve, vagy még
            egyetlen felhasználója sem fogadta el a meghívót — utóbbi esetben a dokumentum
            létrejönne, de senki nem tudná megnyitni.
        acceptsDeliveryReason:
          type: string
          description: Ha `acceptsDelivery` hamis, ez mondja meg, miért.

    ClientListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/IntegrationClient'
        page:
          $ref: '#/components/schemas/PageMeta'

    DocumentTypeRef:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string

    DocumentTypeListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/DocumentTypeRef'

    # ------------------------------------------------------------- Küldés-űrlap
    DocumentPayloadForm:
      type: object
      description: |
        A két küldő végpont közös törzse. A címzést **pontosan az egyik** módon kell
        megadni: adószámmal vagy azonosítóval.
      required: [file, documentTypeId, period]
      properties:
        file:
          type: string
          format: binary
          description: |
            A dokumentum, legfeljebb 25 MB. A szerver a **tartalomból** ismeri fel a
            típust, nem a küldött `Content-Type` fejlécből — azt a hívó írja, tehát
            hazudhat. Elfogadott: PDF, kép (PNG, JPG, GIF, WEBP), Word, Excel,
            PowerPoint, OpenDocument, CSV, XML, TXT. A ZIP szándékosan nem: a csomagolás
            a fehérlista legkényelmesebb megkerülése.
        taxNumber:
          $ref: '#/components/schemas/TaxNumber'
        clientId:
          type: string
          format: uuid
          description: |
            A címzett azonosítója, a `taxNumber` alternatívájaként. Ott van rá szükség,
            ahol nincs adószám — például magánszemély ügyfélnél.
        documentTypeId:
          type: string
          format: uuid
          description: A `GET /document-types` egyik azonosítója.
        period:
          $ref: '#/components/schemas/Period'
        title:
          type: string
          maxLength: 200
          description: A tétel címe az ügyfél tékájában. Hiánya esetén a fájl neve.
        tags:
          type: string
          description: |
            Vesszővel elválasztott cimkék (például `bérszámfejtés,havi`). Egyetlen mező,
            nem tömb — így minden HTTP-kliens egységesen tudja küldeni.
        feedbackRequested:
          type: boolean
          default: false
          description: Kérünk-e az ügyféltől "rendben van / valami nem stimmel" jelzést.
      oneOf:
        - required: [taxNumber]
        - required: [clientId]

    CreateDeliveryForm:
      allOf:
        - $ref: '#/components/schemas/DocumentPayloadForm'
        - type: object
          properties:
            message:
              type: string
              maxLength: 2000
              description: A kiküldéshez fűzött üzenet, ami a megjegyzés-fonal első bejegyzése lesz.
            dryRun:
              type: boolean
              default: false
              description: |
                Ha igaz, minden ellenőrzés lefut, de semmi nem jön létre és nem megy ki.
                A válasz `200`, a törzse `DeliveryDryRunResult`. Kötegelt küldés előtt ez
                az egyetlen módja megtalálni a hibás adószámot még kézbesítés előtt.

    CreateHandoffForm:
      allOf:
        - $ref: '#/components/schemas/DocumentPayloadForm'
        - type: object
          properties:
            note:
              type: string
              maxLength: 2000
              description: |
                Előre beírt megjegyzés a küldés-képernyőn. A felhasználó átírhatja vagy
                törölheti — ez javaslat, nem kész tény.
            returnUrl:
              type: string
              format: uri
              description: |
                Ide tér vissza a felhasználó a sikeres küldés után. Csak a kulcshoz
                felvett előtagokra mutathat (`Identity.allowedReturnUrlPrefixes`),
                egyébként `422`.

    # ------------------------------------------------------- Kézbesítés / átadás
    DeliveryStatus:
      type: string
      description: |
        `PENDING_RECALL` a késleltetési ablakban, `DELIVERED` ha az ügyfél már látja,
        `CANCELLED` ha visszavonták.
      enum: [PENDING_RECALL, DELIVERED, CANCELLED]

    Delivery:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/DeliveryStatus'
        documentId:
          type: string
          format: uuid
        clientId:
          type: string
          format: uuid
        clientName:
          type: string
        taxNumber:
          $ref: '#/components/schemas/TaxNumber'
        createdAt:
          type: string
          format: date-time
        recallableUntil:
          type: string
          format: date-time
          description: |
            Eddig vonható vissza. Hiányzik, ha az iroda kikapcsolta a visszavonást, vagy
            ha az állapot már nem `PENDING_RECALL`.
        deliveredAt:
          type: string
          format: date-time
        idempotencyKey:
          type: string
        document:
          $ref: '#/components/schemas/IntegrationDocument'

    DeliveryDryRunResult:
      type: object
      description: |
        Mi történne, ha ugyanez a hívás `dryRun` nélkül futna. Az `ok` az egyetlen mező,
        amit érdemes kiértékelni; ha hamis, a `problems` mondja meg, min kell javítani.
      properties:
        ok:
          type: boolean
        clientId:
          type: string
          format: uuid
        clientName:
          type: string
          description: |
            A feloldott címzett neve. Kötegelt küldés előtt ezt érdemes emberrel
            átnézetni: egy elírt adószám is találhat egy létező, de más ügyfelet.
        documentTypeName:
          type: string
        detectedContentType:
          type: string
          description: A fájl tartalmából felismert típus.
        fileSizeBytes:
          type: integer
          format: int64
        problems:
          type: array
          description: Üres, ha `ok` igaz.
          items:
            $ref: '#/components/schemas/ValidationErrorItem'

    HandoffStatus:
      type: string
      description: |
        `PREPARED` amíg senki nem nyitotta meg, `OPENED` ha a felhasználó megnyitotta,
        `SENT` ha elküldte, `DISCARDED` ha eldobta vagy a partner visszavonta,
        `EXPIRED` ha a 30 perc lejárt anélkül, hogy elküldték volna.
      enum: [PREPARED, OPENED, SENT, DISCARDED, EXPIRED]

    Handoff:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/HandoffStatus'
        openUrl:
          type: string
          format: uri
          description: |
            Ezt kell megnyitni a böngészőben. Egyszer használható, és csak amíg
            `expiresAt` el nem múlik. Nem hitelesít: bejelentkezés és iroda-tagság
            nélkül nem visz semmire.
          examples:
            - https://app.dokuteka.hu/app/documents/handoff/9f1c8a72e5b34d0f
        expiresAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        clientId:
          type: string
          format: uuid
        clientName:
          type: string
        documentId:
          type: string
          format: uuid
          description: A létrejött piszkozat azonosítója.
        sentAt:
          type: string
          format: date-time
        sentByName:
          type: string
          description: >-
            Ki küldte el a felületen. Ez a mező a lényeg — ez az emberi lépés nyoma.

    # ----------------------------------------------------------------- Állapot
    IntegrationDocumentSource:
      type: string
      description: Melyik úton jött létre a tétel.
      enum: [DELIVERY, HANDOFF]

    IntegrationDocument:
      type: object
      properties:
        id:
          type: string
          format: uuid
        clientId:
          type: string
          format: uuid
        clientName:
          type: string
        title:
          type: string
        documentTypeId:
          type: string
          format: uuid
        period:
          $ref: '#/components/schemas/Period'
        tags:
          type: array
          items:
            type: string
        status:
          $ref: '#/components/schemas/DocumentStatus'
        feedbackRequested:
          type: boolean
        feedbackState:
          $ref: '#/components/schemas/FeedbackResult'
        waitingOn:
          $ref: '#/components/schemas/WaitingOn'
        createdAt:
          type: string
          format: date-time
        deliveredAt:
          type: string
          format: date-time
        viewedAt:
          type: string
          format: date-time
          description: Mikor nyitotta meg az ügyfél először.
        downloadedAt:
          type: string
          format: date-time
        commentCount:
          type: integer
          description: |
            A megjegyzés-fonal hossza. Ha nő, az ügyfél írt valamit — a szöveget a
            felületen kell elolvasni, ez az API nem adja ki.
        source:
          $ref: '#/components/schemas/IntegrationDocumentSource'
        keyName:
          type: string
          description: Melyik kulcs hozta létre. Több integráció mellett ez választja szét őket.

    IntegrationDocumentListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/IntegrationDocument'
        page:
          $ref: '#/components/schemas/PageMeta'
