openapi: 3.1.0

info:
  title: Signeur REST API
  description: |
    Signeurin julkinen REST API allekirjoituspyyntöjen luontiin, hallintaan
    ja webhook-konfigurointiin. Käytä HTTPS:ää aina; bearer-tokenit ovat
    `sgn_live_…`-muotoa ja luodaan dashboardissa.

    **Versionhallinta**: tämä on API v1. Rikkovia muutoksia tehdään vain
    v2-julkaisun yhteydessä. Lisäykset (uudet kentät, endpointit) tehdään
    v1:n alla taaksepäin yhteensopivasti.

    **Idempotenssi**: POST-endpointit hyväksyvät `Idempotency-Key`-headerin.
    Sama avain + sama body palauttaa identtisen vastauksen 24 h ajan.
  version: "1.0.0"
  contact:
    name: Signeur support
    email: support@signeur.eu
    url: https://signeur.eu
  license:
    name: Proprietary
  termsOfService: https://signeur.eu/privacy

servers:
  - url: https://signeur.eu
    description: Tuotanto

security:
  - bearerAuth: []

tags:
  - name: signatures
    description: Allekirjoituspyyntöjen luonti ja hallinta
  - name: signers
    description: Per-signer-resurssit multi-signer-pyynnöille
  - name: documents
    description: Allekirjoitetun PDF:n ja audit-trail-PDF:n lataus
  - name: webhooks
    description: Webhook-endpointtien hallinta

paths:
  # ===========================================================================
  # /signatures
  # ===========================================================================
  /api/v1/signatures:
    post:
      tags: [signatures]
      summary: Luo uusi allekirjoituspyyntö
      description: |
        Luo signature_request-rivin + 1–20 signer-riviä. Lähettää
        kutsu-emailin jokaiselle signerille per-signer access-koodilla.

        Hyväksyy myös vanhan single-signer-bodyn `{ signer_email, signer_name }`
        — muunnetaan sisäisesti yhden alkion `signers[]`-arrayksi.
      operationId: createSignatureRequest
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSignatureRequestBody"
      responses:
        "200":
          description: Pyyntö luotu, kutsu-emailit yritetty
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateSignatureRequestResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "409":
          description: Idempotency-Key-konflikti tai duplikaatti
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "500":
          $ref: "#/components/responses/InternalError"

    get:
      tags: [signatures]
      summary: Listaa organisaation allekirjoituspyynnöt
      operationId: listSignatureRequests
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum:
              - pending
              - sent
              - opened
              - signed
              - cancelled
              - declined
              - expired
              - awaiting_stamp
              - stamp_failed
        - name: created_from
          in: query
          description: ISO-8601 aikaleima — vain rivit luotu tämän jälkeen
          schema: { type: string, format: date-time }
        - name: created_to
          in: query
          description: ISO-8601 aikaleima — vain rivit luotu ennen tätä
          schema: { type: string, format: date-time }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
        - name: cursor
          in: query
          description: Edellisen vastauksen `next_cursor`-arvo
          schema: { type: string }
      responses:
        "200":
          description: Paginoitu lista
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SignatureRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ===========================================================================
  # /signatures/{id}
  # ===========================================================================
  /api/v1/signatures/{id}:
    get:
      tags: [signatures]
      summary: Hae yksittäisen allekirjoituspyynnön tila
      operationId: getSignatureRequest
      parameters:
        - $ref: "#/components/parameters/SignatureRequestId"
      responses:
        "200":
          description: Pyynnön tilatiedot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SignatureRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/signatures/{id}/cancel:
    post:
      tags: [signatures]
      summary: Peruuta allekirjoituspyyntö
      description: |
        Status muuttuu `cancelled`, kaikki allekirjoittamattomat signer-linkit
        invalidoidaan, ja webhook `signature_request.cancelled` lähetetään.
      operationId: cancelSignatureRequest
      parameters:
        - $ref: "#/components/parameters/SignatureRequestId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 500
                  description: Valinnainen peruste audit-trailia varten
      responses:
        "200":
          description: Pyyntö peruttu
          content:
            application/json:
              schema:
                type: object
                required: [id, status, cancelled_at]
                properties:
                  id: { type: string, format: uuid }
                  status: { type: string, enum: [cancelled] }
                  cancelled_at: { type: string, format: date-time }
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Pyyntö jo allekirjoitettu, peruttu tai vanhentunut
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  # ===========================================================================
  # /signatures/{id}/document
  # ===========================================================================
  /api/v1/signatures/{id}/document:
    get:
      tags: [documents]
      summary: Lataa allekirjoitettu PDF
      description: |
        Palauttaa sinetöidyn PAdES B-T -PDF:n binaryna. 410 Gone jos PDF
        on poistettu retention-säilytysajan päätyttyä (oletuksena 7 päivää —
        pidempi säilytys vaatii Archive Plus -tilauksen).
      operationId: downloadSignedDocument
      parameters:
        - $ref: "#/components/parameters/SignatureRequestId"
      responses:
        "200":
          description: Sinetöity PDF
          content:
            application/pdf:
              schema: { type: string, format: binary }
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Pyyntö ei ole vielä allekirjoitettu
        "410":
          description: PDF on poistettu Storagesta retention-rajan jälkeen
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string, enum: [document_purged] }
                  message: { type: string }
                  purged_at: { type: string, format: date-time }

  /api/v1/signatures/{id}/audit-trail.pdf:
    get:
      tags: [documents]
      summary: Lataa audit-trail-PDF
      description: |
        Erillinen informatiivinen PDF: pyynnön metatiedot, signereiden
        statukset IP-osoitteineen, kronologinen event-aikajana ja PAdES-
        allekirjoituksen tekniset tiedot. PDF EI ole kryptografisesti
        allekirjoitettu — sinetöity itse asiakirja on juridinen todiste.
      operationId: downloadAuditTrailPdf
      parameters:
        - $ref: "#/components/parameters/SignatureRequestId"
      responses:
        "200":
          description: Audit-trail-PDF
          content:
            application/pdf:
              schema: { type: string, format: binary }
        "404":
          $ref: "#/components/responses/NotFound"

  # ===========================================================================
  # /signatures/{id}/signers
  # ===========================================================================
  /api/v1/signatures/{id}/signers:
    get:
      tags: [signers]
      summary: Listaa pyynnön signerit
      operationId: listSigners
      parameters:
        - $ref: "#/components/parameters/SignatureRequestId"
      responses:
        "200":
          description: Signerien lista
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Signer" }
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/signatures/{id}/signers/{signer_id}/resend:
    post:
      tags: [signers]
      summary: Lähetä kutsu uudelleen
      description: |
        Vaihtaa signerin access-koodin uuteen ja lähettää kutsu-emailin
        uudestaan. Vanha koodi vanhoista emaileista lakkaa toimimasta.
      operationId: resendSignerInvitation
      parameters:
        - $ref: "#/components/parameters/SignatureRequestId"
        - name: signer_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                locale:
                  type: string
                  enum: [en, fi, fr, de]
                  description: Vaihda signerin email-locale pysyvästi
      responses:
        "200":
          description: Email lähetetty + access-koodi rotatoitu
          content:
            application/json:
              schema:
                type: object
                required: [ok, signer_id, email_sent, access_code_rotated_at]
                properties:
                  ok: { type: boolean }
                  signer_id: { type: string, format: uuid }
                  email_sent: { type: boolean }
                  email_error: { type: [string, "null"] }
                  access_code_rotated_at: { type: string, format: date-time }
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            Signer on jo allekirjoittanut, kieltäytynyt tai pyyntö ei
            ole enää aktiivinen.

  # ===========================================================================
  # /webhook-endpoints
  # ===========================================================================
  /api/v1/webhook-endpoints:
    get:
      tags: [webhooks]
      summary: Listaa webhook-endpointit
      operationId: listWebhookEndpoints
      responses:
        "200":
          description: Endpointtien lista (ilman secrettejä)
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookEndpoint"

    post:
      tags: [webhooks]
      summary: Luo webhook-endpoint
      description: |
        Palauttaa generoidun `secret`-arvon **yhden kerran**. Tallenna se —
        Signeur ei säilytä plain-text-versiota ja se ei palaudu enää.
      operationId: createWebhookEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  description: https:// vaadittu (paitsi localhost devissä)
                event_types:
                  type: array
                  description: Tyhjä array = vastaanota kaikki eventit
                  items: { $ref: "#/components/schemas/WebhookEventType" }
                description:
                  type: string
                  maxLength: 200
      responses:
        "201":
          description: Endpoint luotu
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/WebhookEndpoint"
                  - type: object
                    required: [secret]
                    properties:
                      secret:
                        type: string
                        description: HMAC-SHA256-avain — näytetään VAIN tässä

  /api/v1/webhook-endpoints/{id}:
    get:
      tags: [webhooks]
      summary: Hae yksittäisen endpointin tiedot
      operationId: getWebhookEndpoint
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      responses:
        "200":
          description: Endpoint
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      tags: [webhooks]
      summary: Päivitä webhook-endpoint
      operationId: updateWebhookEndpoint
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                event_types:
                  type: array
                  items: { $ref: "#/components/schemas/WebhookEventType" }
                description: { type: [string, "null"], maxLength: 200 }
                disabled:
                  type: boolean
                  description: true = pause delivery; false = resume
      responses:
        "200":
          description: Päivitetty
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        "404":
          $ref: "#/components/responses/NotFound"

    delete:
      tags: [webhooks]
      summary: Poista webhook-endpoint
      operationId: deleteWebhookEndpoint
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      responses:
        "200":
          description: Poistettu
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  id: { type: string, format: uuid }
        "404":
          $ref: "#/components/responses/NotFound"

# =============================================================================
# Webhook event-payloadit (Signeur → integraattori)
# =============================================================================
webhooks:
  signatureRequestCompleted:
    post:
      operationId: webhookSignatureRequestCompleted
      summary: signature_request.completed
      description: |
        Fires kerran kun kaikki signereistä ovat allekirjoittaneet ja
        PDF on sinetöity. `signature_request.signed` on deprecated alias
        joka fires samalla hetkellä samalla payloadilla.
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    type:
                      type: string
                      enum: [signature_request.completed]
                    data:
                      type: object
                      required: [signature_request_id, signed_at, document_hash]
                      properties:
                        signature_request_id: { type: string, format: uuid }
                        status: { type: string, enum: [signed] }
                        signed_at: { type: string, format: date-time }
                        document_hash: { type: string }
                        download_url: { type: string, format: uri }
      responses:
        "200":
          description: Integraattorin vahvistus

  signerSigned:
    post:
      operationId: webhookSignerSigned
      summary: signer.signed
      description: |
        Fires N kertaa multi-signer-pyynnössä — kerran jokaisen onnistuneen
        signer-allekirjoituksen jälkeen. `remaining_pending_signers`
        ilmoittaa kuinka monta on vielä jäljellä.
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEnvelope"
                - type: object
                  properties:
                    type:
                      type: string
                      enum: [signer.signed]
                    data:
                      type: object
                      properties:
                        signature_request_id: { type: string, format: uuid }
                        signer_id: { type: string, format: uuid }
                        signer_email: { type: string, format: email }
                        signer_name: { type: string }
                        signed_at: { type: string, format: date-time }
                        remaining_pending_signers: { type: integer }
      responses:
        "200":
          description: Integraattorin vahvistus

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "sgn_live_…"
      description: |
        API-keyt generoidaan dashboardissa
        (https://signeur.eu/dashboard/api-keys). Anna header muodossa
        `Authorization: Bearer sgn_live_…`.

        Avain voi olla joko **personal-tason** (henkilökohtaiselle
        tilille) tai **organisaatio-tason** (jaettu jäsenten kesken).
        Pyyntö kohdistuu automaattisesti avaimen omistajaan: personal-
        avaimen luomat pyynnöt liittyvät `user_id`-kenttään, organisaatio-
        avaimen `organization_id`-kenttään. Käytä `created_via=api`-
        kentää tunnistamaan API:n kautta luodut pyynnöt.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      schema: { type: string, maxLength: 255 }
      description: |
        Uniikki avain (tyypillisesti UUID) idempotenssin takaamiseksi.
        Sama avain + sama body 24 h sisällä → identtinen response.
        Eri body samalla avaimella → 409 idempotency_key_conflict.

    SignatureRequestId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }

    WebhookEndpointId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }

  responses:
    BadRequest:
      description: Pyyntö virheellinen (validointivirhe)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }

    Unauthorized:
      description: |
        Authorization-header puuttuu, on virheellinen tai API-key on peruttu.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }

    PaymentRequired:
      description: Tilaus puuttuu tai on epäaktiivinen
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }

    NotFound:
      description: Resurssia ei löytynyt (tai se ei kuulu organisaatiollesi)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }

    InternalError:
      description: Sisäinen virhe
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }

  schemas:
    ApiError:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          description: Konekäännöskelpoinen virhe-tunniste (esim. "invalid_signer_email")
        message:
          type: string
          description: Ihmisluettava selitys

    Locale:
      type: string
      enum: [en, fi, fr, de]

    SignerInput:
      type: object
      required: [email]
      properties:
        email: { type: string, format: email }
        name: { type: string }
        locale: { $ref: "#/components/schemas/Locale" }

    CreateSignatureRequestBody:
      type: object
      required: [title, document]
      properties:
        title:
          type: string
          description: Pyynnön otsikko (näkyy kutsu-emailissa)
        message:
          type: string
          description: Vapaa viesti signereille
        signers:
          type: array
          minItems: 1
          maxItems: 20
          items: { $ref: "#/components/schemas/SignerInput" }
        signer_email:
          type: string
          format: email
          description: |
            Legacy single-signer-kenttä. Käytä `signers[]` uusissa
            integraatioissa.
          deprecated: true
        signer_name:
          type: string
          description: Legacy single-signer-kenttä.
          deprecated: true
        locale:
          $ref: "#/components/schemas/Locale"
        document:
          type: object
          required: [content_base64]
          properties:
            filename: { type: string }
            content_base64:
              type: string
              description: PDF base64-koodattuna (max 10 MB dekoodattuna)
        metadata:
          type: object
          description: Vapaa avain-arvopari-tietue
          additionalProperties: true

    SignerCreateResult:
      type: object
      required: [id, email, access_token, sign_url, email_sent, locale]
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        name: { type: string }
        access_token: { type: string }
        sign_url: { type: string, format: uri }
        email_sent: { type: boolean }
        email_error: { type: [string, "null"] }
        locale: { $ref: "#/components/schemas/Locale" }

    CreateSignatureRequestResponse:
      type: object
      required: [id, status, expires_at, signers]
      properties:
        id: { type: string, format: uuid }
        status:
          type: string
          enum: [sent, pending]
        expires_at: { type: string, format: date-time }
        signers:
          type: array
          items: { $ref: "#/components/schemas/SignerCreateResult" }
        # Single-signer back-compat (vain kun signers.length === 1)
        token: { type: string, deprecated: true }
        sign_url: { type: string, format: uri, deprecated: true }
        email_sent: { type: boolean, deprecated: true }
        email_error: { type: [string, "null"], deprecated: true }

    Signer:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        name: { type: string }
        name_as_invited: { type: [string, "null"] }
        name_as_signed: { type: [string, "null"] }
        access_token: { type: string }
        sign_url: { type: string, format: uri }
        status:
          type: string
          enum: [pending, sent, opened, signed, declined, invalidated]
        position: { type: integer }
        sent_at: { type: [string, "null"], format: date-time }
        opened_at: { type: [string, "null"], format: date-time }
        signed_at: { type: [string, "null"], format: date-time }
        declined_at: { type: [string, "null"], format: date-time }
        decline_reason: { type: [string, "null"] }
        invalidated_at: { type: [string, "null"], format: date-time }
        ip_address: { type: [string, "null"] }
        user_agent: { type: [string, "null"] }
        locale: { $ref: "#/components/schemas/Locale" }
        representing_as_invited: { type: [string, "null"] }
        representing_as_signed: { type: [string, "null"] }
        representing_role: { type: [string, "null"] }
        representing_entity_id: { type: [string, "null"] }
        audit_trail:
          type: object
          properties:
            events:
              type: array
              items: { type: object, additionalProperties: true }

    Retention:
      type: object
      properties:
        is_archive_protected:
          type: boolean
          description: Onko organisaatiolla aktiivinen Archive Plus -tilaus
        pdf_purged: { type: boolean }
        pdf_purged_at: { type: [string, "null"], format: date-time }
        pdf_available_until:
          type: [string, "null"]
          format: date-time
          description: null jos archive-protected (rajoittamaton retention)
        pdf_days_remaining:
          type: [integer, "null"]

    SignatureRequest:
      type: object
      properties:
        id: { type: string, format: uuid }
        status:
          type: string
          enum:
            - pending
            - sent
            - opened
            - signed
            - cancelled
            - declined
            - expired
            - awaiting_stamp
            - stamp_failed
        title: { type: string }
        message: { type: string }
        created_at: { type: string, format: date-time }
        sent_at: { type: [string, "null"], format: date-time }
        signed_at: { type: [string, "null"], format: date-time }
        cancelled_at: { type: [string, "null"], format: date-time }
        expires_at: { type: string, format: date-time }
        is_expired: { type: boolean }
        retention: { $ref: "#/components/schemas/Retention" }
        signers:
          type: array
          items: { $ref: "#/components/schemas/Signer" }
        document_hash: { type: [string, "null"] }
        audit_trail: { type: object, additionalProperties: true }
        metadata: { type: object, additionalProperties: true }
        sender_email:
          type: [string, "null"]
          description: "Normalised (lower+trim) sender email. NULL for API-created requests."
        created_via:
          type: string
          enum: [web, api]
          description: "How the request was created. 'web' = dashboard or anonymous web form, 'api' = REST API."
        owner_type:
          type: string
          enum: [user, organization]
          description: "Whether the request belongs to a personal user (user_id) or an organization (organization_id)."
        # Single-signer back-compat
        token: { type: string, deprecated: true }
        sign_url: { type: string, format: uri, deprecated: true }
        signer_email: { type: string, deprecated: true }
        signer_name: { type: string, deprecated: true }
        opened_at: { type: [string, "null"], format: date-time, deprecated: true }

    SignatureRequestSummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string }
        title: { type: [string, "null"] }
        signer_count: { type: integer }
        created_at: { type: string, format: date-time }
        sent_at: { type: [string, "null"], format: date-time }
        signed_at: { type: [string, "null"], format: date-time }
        cancelled_at: { type: [string, "null"], format: date-time }
        expires_at: { type: string, format: date-time }
        is_expired: { type: boolean }

    SignatureRequestList:
      type: object
      required: [data, has_more]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/SignatureRequestSummary" }
        has_more: { type: boolean }
        next_cursor: { type: [string, "null"] }

    WebhookEventType:
      type: string
      enum:
        - signature_request.created
        - signature_request.sent
        - signature_request.opened
        - signer.signed
        - signature_request.completed
        - signature_request.signed
        - signature_request.declined
        - signature_request.cancelled
        - signature_request.expired

    WebhookEndpoint:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        event_types:
          type: array
          items: { $ref: "#/components/schemas/WebhookEventType" }
          description: Tyhjä array = vastaanota kaikki eventit
        description: { type: [string, "null"], maxLength: 200 }
        disabled_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    WebhookEnvelope:
      type: object
      required: [id, type, created_at, data]
      properties:
        id:
          type: string
          description: Yksilöllinen event-ID muotoa `evt_<uuid>`
        type: { $ref: "#/components/schemas/WebhookEventType" }
        created_at: { type: string, format: date-time }
        data:
          type: object
          additionalProperties: true
