openapi: 3.1.0
info:
  title: getpeppr API - Network & Validation API
  version: 1.0.0
  description: Resolve Peppol participants, list transports, validate payloads,
    and inspect events.
  contact:
    name: getpeppr Support
    url: https://getpeppr.dev
    email: support@getpeppr.dev
  license:
    name: Proprietary
servers:
  - url: https://api.getpeppr.dev/v1
    description: Production
tags:
  - name: Directory
    description: Look up participants on the Peppol network.
  - name: Transports
    description: View transport documents and available transport types.
  - name: Validation
    description: Validate invoice data before sending.
  - name: Events
    description: View the event log for your invoices.
security:
  - bearerAuth: []
paths:
  /directory/search:
    get:
      tags:
        - Directory
      summary: Search the Peppol Directory
      description: >
        Search for participants in the Peppol Directory by business name or VAT
        number,

        optionally narrowed by country. Country-only searches are accepted but
        usually

        exceed the Directory's accessible window. Name searches require a
        minimum of 3 characters.

        Pagination is exact and counts unique Peppol participants. If a query
        matches more than

        the public Directory's accessible 1,001-business-entity window, the
        request returns 400;

        add or refine the business name or VAT number and retry.

        Results are cached for 1 hour.


        **Rate limit:** 30 requests per minute per API key (shared with the
        participant lookup),

        in addition to your plan's global rate limit.
      operationId: searchDirectory
      parameters:
        - name: name
          in: query
          description: Business name to search (min 3 characters)
          schema:
            type: string
            minLength: 3
          example: Acme
        - name: country
          in: query
          description: Optional narrowing filter (ISO 3166-1 alpha-2, uppercase);
            country-only searches are usually too broad
          schema:
            type: string
            pattern: ^[A-Z]{2}$
          example: BE
        - name: vatNumber
          in: query
          description: >-
            VAT number to search. Include the country prefix (`BE0685660237`,
            `NL861125575B01`, `EL094381904` for Greece) — many participants are
            only findable with it, and passing `country` as well lets us restore
            the prefix when your number has none.


            Coverage is best-effort and depends on how each participant
            registered: the Peppol Directory holds some entries with the country
            prefix and some without, and a participant that published neither an
            additional VAT identifier nor a prefixed identifier may not be found
            by VAT at all. Searching by `name` and `country` remains the
            broadest option.
          schema:
            type: string
          example: BE0685660237
        - name: limit
          in: query
          description: Max unique Peppol participants per page (default 20, max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          description: Zero-based unique-participant offset (default 0)
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Search results
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/DirectoryEntry"
                  meta:
                    type: object
                    required:
                      - total_count
                      - offset
                      - limit
                      - has_more
                    properties:
                      total_count:
                        type: integer
                        description: Total unique participants in the completed result set
                      offset:
                        type: integer
                        description: Zero-based participant offset returned
                      limit:
                        type: integer
                        description: Maximum participants requested for this page
                      has_more:
                        type: boolean
                        description: Whether another participant exists after this page
        "400":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Missing or invalid search criteria, including a query too broad for
            exact pagination
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/DirectoryUpstreamUnavailable"
      security:
        - bearerAuth: []
  /directory/{scheme}/{participantId}:
    get:
      operationId: lookupDirectory
      summary: Look up a Peppol participant
      description: >
        Look up a Peppol participant by their scheme and identifier.

        Returns enriched data from the Peppol Directory including business name,
        country,

        capabilities, registration date, VAT number, and contact info.

        Falls back to basic registration check if the Peppol Directory is
        unavailable.

        Results are cached for 24 hours.


        Two URL formats are supported:

        - `/directory/{scheme}/{id}` (e.g., `/directory/0208/0685660237`)

        - `/directory/{scheme}:{id}` (e.g., `/directory/0208:0685660237`)


        Directory lookups have an additional rate limit of 30 requests per
        minute per API key.
      tags:
        - Directory
      parameters:
        - name: scheme
          in: path
          required: true
          description: Peppol participant scheme (e.g., "0208" for Belgian BCE)
          schema:
            type: string
          example: "0208"
        - name: participantId
          in: path
          required: true
          description: Participant identifier within the scheme
          schema:
            type: string
          example: "0685660237"
      responses:
        "200":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Participant found in the Peppol directory
          content:
            application/json:
              schema:
                type: object
                properties:
                  participant:
                    type: object
                    properties:
                      registered:
                        type: boolean
                        description: Whether the participant is registered on Peppol
                      scheme:
                        type: string
                        description: Peppol scheme code
                        example: "0208"
                      id:
                        type: string
                        description: Participant identifier within the scheme
                        example: "0685660237"
                      name:
                        type: string
                      country:
                        type: string
                      capabilities:
                        type: array
                        items:
                          type: string
                      registrationDate:
                        type: string
                      vatNumber:
                        type: string
                      additionalIds:
                        type: array
                        items:
                          type: object
                          properties:
                            scheme:
                              type: string
                            value:
                              type: string
                      contactInfo:
                        type: object
                        properties:
                          name:
                            type: string
                          email:
                            type: string
                          phone:
                            type: string
                      website:
                        type: string
              example:
                participant:
                  registered: true
                  scheme: "0208"
                  id: "0685660237"
                  name: ACMEDIA
                  country: BE
                  capabilities:
                    - invoice
                    - credit_note
                  registrationDate: 2020-11-20
        "400":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Invalid Peppol ID format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Invalid Peppol ID format. Expected: /directory/{scheme}/{id} or
                  /directory/{scheme}:{id}"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Participant not found in the Peppol network
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Peppol participant 0208:0755752134 not found in the directory
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
        "503":
          $ref: "#/components/responses/ProviderRetryLater"
  /transports:
    get:
      operationId: listTransports
      summary: List transports
      description: >
        Returns the list of active transports. Currently, only the Peppol AS4
        transport

        (via Storecove) is available. Transport routing is managed automatically
        —

        write operations (POST, PUT, DELETE) return `405 Method Not Allowed`.
      tags:
        - Transports
      responses:
        "200":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: List of transports
          content:
            application/json:
              schema:
                type: object
                properties:
                  transports:
                    type: array
                    items:
                      $ref: "#/components/schemas/Transport"
              example:
                transports:
                  - id: peppol
                    transportTypeCode: peppol
                    name: Peppol AS4 (Storecove)
                    status: active
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /transports/{code}:
    get:
      operationId: getTransport
      summary: Get a transport by code
      description: >
        Returns details of a specific transport. Currently only `peppol` is a
        valid code.
      tags:
        - Transports
      parameters:
        - name: code
          in: path
          required: true
          description: Transport code (e.g., "peppol")
          schema:
            type: string
          example: peppol
      responses:
        "200":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Transport details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transport"
              example:
                id: peppol
                transportTypeCode: peppol
                name: Peppol AS4 (Storecove)
                status: active
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Transport not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Transport not found
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /transports/types:
    get:
      operationId: listTransportTypes
      summary: List transport types
      description: |
        Returns the list of available transport types. Currently a single
        static entry: Peppol BIS 3.0 (routing is handled automatically by the
        network provider).
      tags:
        - Transports
      responses:
        "200":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: List of transport types
          content:
            application/json:
              schema:
                type: object
                properties:
                  transportTypes:
                    type: array
                    items:
                      $ref: "#/components/schemas/TransportType"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /validate:
    post:
      operationId: validateInvoice
      summary: Validate invoice data (client-side)
      description: >
        Performs client-side validation of invoice data without sending it.

        Checks that the required fields are present — `number`, `to.name`,

        `to.country`, and at least one line carrying `description`, `quantity`

        and `unitPrice`. It checks nothing else: no formats, no totals, no

        business rules.


        This is a fast, local validation. For gateway-side UBL generation checks

        and registered partial offline pre-flight checks, use `POST
        /validate/server`.
      tags:
        - Validation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceInput"
      responses:
        "200":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Validation result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClientValidationResult"
              example:
                valid: false
                errors:
                  - Customer country (to.country) is required
                  - "Line 1: unitPrice is required"
        "400":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Invalid JSON body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /validate/server:
    post:
      operationId: validateInvoiceServer
      summary: Validate invoice data (server-side)
      description: >
        Performs server-side pre-flight validation in the getpeppr gateway
        without sending

        the invoice to Storecove. The invoice data is checked with the same SDK
        validation

        engine used by the CLI, converted to UBL XML as a format sanity check,
        and evaluated

        against getpeppr's registered partial offline pre-flight checks.


        This is slower than client-side validation but catches additional
        pre-flight issues.

        Storecove Standard JSON validation is not called. `valid` describes only
        the checks

        listed here and is not proof that `POST /v1/invoices` will be accepted
        by the provider;

        read `providerSendability`, which is currently always `not_checked`.

        Validation failures return `200` with `valid: false` so SDK clients can
        inspect all

        errors without exception handling; transport, auth, rate-limit, and
        malformed-request

        failures still use non-2xx HTTP statuses.


        Embedded attachment content is rejected before UBL generation when the
        decoded content

        exceeds 2 MB for a single attachment.
      tags:
        - Validation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ValidateServerInput"
      responses:
        "200":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Server validation result with SDK and registered partial
            offline-check details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServerValidationResult"
              example:
                valid: false
                errors:
                  - field: to.street
                    message: Street address is required for the buyer
                    ruleId: GETPEPPR-BUYER-ADDRESS
                    suggestion: Add to.street, to.city, and to.postalCode for Peppol BG-8
                      compliance.
                warnings: []
                ubl:
                  valid: true
                  errors: []
                xsd:
                  valid: true
                  errors: []
                  note: Deprecated compatibility field. This endpoint verifies UBL XML generation
                    but does not run a standalone XSD validator.
                schematron:
                  valid: false
                  coverage:
                    rulesChecked: 40
                    ofNetworkFatalRules: partial
                  errors:
                    - severity: error
                      message: Endpoint ID must use a valid Peppol participant identifier.
                      location: to.peppolId
                      ruleId: PEPPOL-EN16931-R020
                  warnings: []
                providerSendability: not_checked
        "400":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Missing required fields: number, to, and lines are required"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: |
            Request body exceeds the 4 MB cap this endpoint shares with
            `POST /v1/invoices/import` and `POST /v1/validate/ubl` — four times
            the 1 MB default of the other JSON endpoints, because a full invoice
            with embedded attachments has to fit.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Payload too large. Maximum size: 4096KB"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /validate/ubl:
    post:
      operationId: validateUbl
      summary: Validate a UBL document you produced yourself
      description: >
        Runs the complete official OpenPeppol rulebooks against a UBL document
        you

        produced yourself. Unlike `/validate/server`, which judges getpeppr's
        own JSON

        invoice model, this endpoint judges **the XML itself**.


        The document is never modified, never repaired, and never sent. Every

        verdict — that is, every `200`, conformant or not — names the rulebook
        it

        was judged against, so you can tell when getpeppr has fallen behind the

        network. The refusals do not: a `400`, `413`, `429` or `500` means no

        document was judged, so there is no rulebook to name.


        Both official layers are applied — the OpenPeppol rules and the CEN EN
        16931

        rules — plus whichever national family your document falls into. Only
        rules

        flagged `fatal` set `conformant` to false; `warning` findings are
        reported for

        information and never block a send.
      tags:
        - Validation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: byte
                  description: |
                    The UBL document, base64-encoded. The request body is capped
                    at 4 MB; base64 inflates the document by about a third, so
                    the largest document that fits is roughly 3 MB.
      responses:
        "200":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: >
            Validation verdict. A `200` does NOT mean the document is conformant
            —

            read the `conformant` field.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UblValidationVerdict"
        "400":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: >
            Nothing was judged. Every refusal here describes your request or
            your

            document, never a fault on our side, and each carries a machine code

            in `error`:


            `invalid_json` (the request body is not JSON) · `missing_file` (no

            `file` field, or it is not a string) · `invalid_base64` (the `file`

            field does not decode to exactly the bytes it announces) ·

            `undecodable_document` (the decoded bytes are not valid UTF-8) ·

            `unsupported_encoding` (the XML declaration announces an encoding

            other than UTF-8, contradicting its own bytes) ·

            `doctype_not_allowed` (the document carries a document type

            declaration — the only door to a custom entity, refused before any

            parser sees it) · `malformed_xml` (not well-formed XML) ·

            `not_ubl_document` (well-formed, but not a UBL Invoice or CreditNote

            — a PDF renamed `.xml` lands here) · `document_too_complex` (the

            structure is too large to validate at bounded cost; split it into

            smaller documents).


            The three decoding refusals are **not repairs declined, they are

            repairs never attempted**: a base64 decode silently drops characters

            outside its alphabet and a UTF-8 decode replaces invalid octets with

            U+FFFD, so a lenient reader would judge a document that differs from

            the one you sent and report a verdict about neither. We refuse

            instead. The same discipline, and the same three codes, apply on

            `POST /invoices/import`, where the stakes are higher still: there
            the

            bytes we check are the bytes we transmit.


            The same codes appear on `POST /invoices/import` as `422`, because

            there they refuse a **send**. Here nothing is being sent, so a

            document we cannot judge is an ordinary bad request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: not_ubl_document
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: |
            Request body exceeds the 4 MB cap. The cap is measured on the actual
            request bytes, including chunked bodies and requests with an omitted
            or under-declared `Content-Length`, and returns
            `Payload too large. Maximum size: 4096KB` in `error`. The decoded
            document also keeps a 4 MB defence-in-depth cap; if that separate
            cap is reached, `error` is the machine code `payload_too_large`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Payload too large. Maximum size: 4096KB"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: >
            `validation_unavailable` — validation is temporarily unavailable.
            The

            document was NOT validated. getpeppr fails closed rather than

            reporting a verdict it could not compute, so this is the one answer

            here that is about us and not about your document. It is safe to

            retry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: validation_unavailable
  /events:
    get:
      operationId: listEvents
      summary: List events
      description: >
        Returns a paginated event log for your account. Events include state
        changes,

        delivery confirmations, inbound reception, and other lifecycle events.


        Events are scoped to your account **and to the environment of the API
        key**

        used to make the request — a sandbox key returns only sandbox events, a

        production key only production events.


        They are also scoped to the **type** of the key: with a standard key,
        events

        attached to a sub-tenant's document are omitted, while a master key sees
        the

        activity of every sub-tenant it owns. Events that refer to no document —

        validation calls, directory lookups — are account activity and are
        always

        returned.
      tags:
        - Events
      parameters:
        - name: documentId
          in: query
          description: Canonical document filter. Accepts either the provider document
            GUID or the getpeppr submission ID (the `id` field of GET
            /invoices). An unknown or unowned ID simply yields an empty list (no
            404).
          schema:
            type: string
            minLength: 1
        - name: invoiceId
          in: query
          description: Deprecated alias for `documentId`. When both aliases are provided,
            they must contain exactly the same value.
          deprecated: true
          schema:
            type: string
            minLength: 1
        - name: dateFrom
          in: query
          description: Filter events from this date (ISO 8601)
          schema:
            type: string
            format: date
          example: 2026-02-01
        - name: dateTo
          in: query
          description: Filter events until this date (ISO 8601)
          schema:
            type: string
            format: date
          example: 2026-02-28
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Paginated list of events
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/EventEntry"
                  meta:
                    $ref: "#/components/schemas/PaginationMeta"
        "400":
          headers:
            Getpeppr-Request-Id:
              $ref: "#/components/headers/GetpepprRequestId"
            Getpeppr-Result-Code:
              $ref: "#/components/headers/GetpepprResultCode"
            Getpeppr-Result-Message:
              $ref: "#/components/headers/GetpepprResultMessage"
            Getpeppr-Retryable:
              $ref: "#/components/headers/GetpepprRetryable"
            Getpeppr-Remediation:
              $ref: "#/components/headers/GetpepprRemediation"
            Getpeppr-Result-Docs:
              $ref: "#/components/headers/GetpepprResultDocs"
          description: Malformed date or document filter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                malformedDate:
                  value:
                    error: dateFrom must be in YYYY-MM-DD format
                invalidDocumentFilter:
                  value:
                    error: invalid_query_parameter
                    message: documentId must be provided exactly once with a non-empty value
                conflictingAliases:
                  value:
                    error: conflicting_query_parameters
                    message: documentId and invoiceId must match when both are provided
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
components:
  headers:
    GetpepprRequestId:
      description: The identifier getpeppr assigned to this request. Always generated
        by getpeppr — a value you send is never promoted into it — and unique
        per HTTP request, including an idempotent replay. Quote it to support
        and the exact request can be found.
      schema:
        type: string
        pattern: ^req_[0-9a-f]{32}$
      example: req_4f3c2a1e9d8b7c6a5f4e3d2c1b0a9f8e
    GetpepprResultCode:
      description: The stable machine identifier for this outcome, as
        `domain.outcome`. A code never changes meaning, never moves to a
        different HTTP status and is never reused, so it is safe to branch on.
        See the code reference for the full list.
      schema:
        type: string
        pattern: ^[a-z_]+\.[a-z0-9_]+$
        maxLength: 96
      example: invoices.created
    GetpepprResultMessage:
      description: "A fixed human-readable sentence for this code. It is a constant,
        never a template: no invoice number, Peppol ID or provider text is
        interpolated into it. Variable detail stays in the JSON body."
      schema:
        type: string
        maxLength: 256
      example: The invoice was accepted and submitted to the Peppol network.
    GetpepprRetryable:
      description: Whether repeating the same request can succeed. `false` means the
        request must change first. This is the catalogue's answer for the code,
        and it outranks any rule of thumb based on the status class.
      schema:
        type: string
        enum:
          - "true"
          - "false"
      example: "false"
    GetpepprRemediation:
      description: "What to do next, from a closed set. ⚠️ `retry_after` does NOT
        promise a `Retry-After` header: the limits getpeppr applies send one, a
        limit the Peppol provider applies does not. Treat the header as a
        refinement when it is there, and keep your own backoff when it is not."
      schema:
        type: string
        enum:
          - none
          - fix_request
          - authenticate
          - retry
          - retry_after
          - wait
          - contact_support
      example: fix_request
    GetpepprResultDocs:
      description: A stable link to the page documenting this outcome. ⚠️ It points at
        the guide for the topic, not at a per-code entry, and it is ABSENT for a
        code with no page — treat absence as normal, not as an error.
      schema:
        type: string
        format: uri
        maxLength: 512
      example: https://getpeppr.dev/docs/send-invoice/
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        API key authentication. Pass your API key as a Bearer token.

        Keys follow the format:
        - `sk_sandbox_*` for sandbox/staging environment
        - `sk_live_*` for production environment
  parameters:
    InvoiceId:
      name: id
      in: path
      required: true
      description: Invoice ID
      schema:
        type: string
      example: "12345"
    ContactId:
      name: id
      in: path
      required: true
      description: Contact ID (UUID — any non-UUID value yields a 404)
      schema:
        type: string
        format: uuid
      example: 6f1c2b3a-9d4e-4c5f-8a7b-1e2d3c4b5a69
    BankAccountId:
      name: id
      in: path
      required: true
      description: Bank account ID (UUID — any non-UUID value yields a 404)
      schema:
        type: string
        format: uuid
      example: 2a7b4c1d-5e6f-4a8b-9c0d-3f2e1d4c5b6a
    LegalEntityId:
      name: id
      in: path
      required: true
      description: getpeppr legal entity id
      schema:
        type: string
        format: uuid
      example: 7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Client-generated unique key to ensure idempotent operation. Keys are
        scoped to your API key, remember the endpoint and a SHA-256 fingerprint
        of the raw request body, and expire after 24 hours. Retrying an
        identical request replays the cached response; reusing the key for a
        different endpoint or body returns `422 idempotency_key_reuse`.

        Omitting the header is fine — it simply means you are not asking for
        idempotency. Sending it **blank** is not: a value that is empty, or
        that a transport reduces to empty by stripping the whitespace at its
        edges, returns `400 idempotency_key_blank` rather than being ignored. A
        key that protects nothing is worse than no key at all, because you
        would believe you were covered. Sending the header **twice** is a third
        case: HTTP joins the values with a comma, so the result is not blank
        and is accepted as a key.
      schema:
        type: string
        pattern: "[^\\t\\n\\r ]"
        maxLength: 256
      example: inv-2026-001-create
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum number of items to return (1-100, default 20)
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    Offset:
      name: offset
      in: query
      required: false
      description: Number of items to skip for pagination (default 0)
      schema:
        type: integer
        minimum: 0
        default: 0
  schemas:
    UblValidationVerdict:
      type: object
      required:
        - conformant
        - rulebook
        - violations
        - rulesFired
      properties:
        conformant:
          type: boolean
          description: >
            False as soon as one FATAL rule is violated. Warnings never affect
            this

            field and never block a send.
        rulebook:
          type: object
          required:
            - peppol
            - verifiedAt
          properties:
            peppol:
              type: string
              description: |
                The OpenPeppol release these rules come from. Named `peppol`
                rather than `release` because the rulebook is not a single
                thing: the CEN EN 16931 layer and any national family ride
                alongside it, and a bare `release` would leave a second version
                nowhere to go.
              example: v3.0.21
            verifiedAt:
              type: string
              format: date
              description: >
                The date getpeppr last verified this release byte-identical
                against the

                upstream OpenPeppol repository.
              example: 2026-08-28
        violations:
          type: array
          description: >
            Every rule that fired against your document, fatal and warning
            alike. A

            fatal violation on `/v1/invoices/import` produces a terminal 422 and
            the

            document is not sent.
          items:
            type: object
            required:
              - ruleId
              - flag
              - text
              - location
            properties:
              ruleId:
                type: string
                description: The official rule identifier.
                example: PEPPOL-EN16931-R003
              flag:
                type: string
                enum:
                  - fatal
                  - warning
                description: >
                  Only `fatal` prevents a send. `warning` is reported for
                  information.

                  Two German IBAN checksum rules (DE-R-019, DE-R-020) are known
                  false

                  positives of the XPath engine and are always reported as
                  warnings.
              text:
                type: string
                description: The rule text, verbatim from the official rulebook.
              location:
                type: string
                description: XPath of the offending node in your document.
        rulesFired:
          type: integer
          description: |
            How many rule **activations** occurred — one per rule per matching
            node, not one per rule. A rule whose context matches twenty invoice
            lines counts twenty times, so this number grows with the size of
            your document and is normally far larger than the number of distinct
            rules involved. Duplicating a single `InvoiceLine` in one corpus
            document moves it from 165 to 174 while the count of distinct rules
            stays at 19.

            Read it as a sign of life from the validator, never as a measure of
            how much of the rulebook was covered. Zero would mean the validator
            itself failed; getpeppr reports that as an error rather than as a
            clean result.
          example: 165
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Machine-readable error code on newer structured errors, or a
            human-readable legacy message
        code:
          type: string
          description: Machine-readable error code when available
        message:
          type: string
          description: Additional developer-facing detail when available
        field:
          type: string
          description: JSON field path responsible for the error when available
        rule:
          type: string
          description: Peppol or EN 16931 rule identifier when available
        reason:
          type: string
          description: Lifecycle or conflict reason when available
        duplicateOf:
          type: string
          description: "On `409 duplicate_document` only: the provider document GUID of
            the earlier send this request collides with. Present so you can look
            the document up rather than take our word for it."
        supportedLanguages:
          type: array
          items:
            type: string
          description: "On `400 attestation.language_unsupported` only: the languages this
            deployment can send an attestation request in, as canonical BCP 47
            tags. Send one of them as `language`, or omit it for English."
        docs:
          type: string
          format: uri
          description: Documentation link when available
        details:
          type: array
          items:
            type: string
          description: Field-level validation details when available
      example:
        error: Internal server error
    PaginationMeta:
      type: object
      description: Pagination envelope. Field names are snake_case on the wire; the
        official SDK re-maps them to camelCase client-side.
      required:
        - total_count
        - offset
        - limit
        - has_more
      properties:
        total_count:
          type: integer
          description: Total number of matching items
          example: 42
        offset:
          type: integer
          description: Current offset
          example: 0
        limit:
          type: integer
          description: Items per page
          example: 20
        has_more:
          type: boolean
          description: Whether more items exist beyond this page
          example: true
    Party:
      type: object
      required:
        - name
        - peppolId
        - country
      properties:
        name:
          type: string
          description: Business name
          example: ACMEDIA
        peppolId:
          type: string
          description: Peppol participant ID in scheme:id format
          example: 0208:0685660237
        vatNumber:
          type: string
          description: VAT number
          example: BE0685660237
        street:
          type: string
          description: Street address
        city:
          type: string
          description: City
        postalCode:
          type: string
          description: Postal/zip code
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
          example: BE
        companyId:
          type: string
          description: Company registration number (BT-30/BT-47)
        companyIdScheme:
          type: string
          description: Scheme ID for company registration (e.g., "0208" for Belgian BCE)
        contactName:
          type: string
          description: Contact person name
        phone:
          type: string
          description: Contact telephone
        email:
          type: string
          format: email
          description: Contact email
    InvoiceSender:
      type: object
      description: |
        Platform-only sender selector. Provide exactly one field — a master key
        sending both, or an empty object, is refused with `400`. A standard key
        never gets that far: see the key-type note below.

        ⚠️ On a master key, `sender: null` is NOT an error: it is treated as
        "no sender", and the document goes out under your own account identity.
        Send exactly one selector, or omit the field entirely.

        Master keys use it to send as a sub-tenant. Standard keys may not, and
        both send endpoints answer that the same way: a `sender` field that is
        PRESENT on a standard key — including `null` or an empty object — is
        refused with `403`, on `POST /invoices` and on `POST /invoices/import`
        alike. The field is never dropped in silence. Omit it entirely and the
        document is sent under your own account identity, as before.
      oneOf:
        - required:
            - legalEntityId
        - required:
            - externalSubTenantId
      properties:
        legalEntityId:
          type: string
          format: uuid
          description: getpeppr legal entity id for the sub-tenant sender
          example: 7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b
        externalSubTenantId:
          type: string
          description: Your stable external id for the sub-tenant sender
          example: customer_8412
    SellerContact:
      type: object
      description: |
        The seller CONTACT (BG-6) for this document. The legal entity that
        sends has no contact of its own, so this is where it comes from.

        The seller IDENTITY (name, postal address, VAT and identifiers) comes
        from the registered legal entity that sends — your account's, or the
        sub-tenant named in `sender`. On a sub-tenant send, every other `Party`
        field you put here (the SDK's `from` is a full `Party`) is stripped
        before the document leaves, so it can never re-state who the seller is.
        On your own account's sends, do not use `from` to change the seller
        identity: the provider builds it from your legal entity.

        Domestic German invoices (German seller, German buyer) require all
        three fields. Without any of them the request is refused with `422`
        `country_rule_violation` and code `DE-R-002`; with some of them, with
        `DE-R-005` (contact name), `DE-R-006` (telephone) or `DE-R-007` (email).
      properties:
        contactName:
          type: string
          description: Seller contact point (BT-41)
          example: Tim Tester
        phone:
          type: string
          description: Seller contact telephone number (BT-42)
          example: 012 3456789
        email:
          type: string
          format: email
          description: Seller contact email address (BT-43)
          example: billing@example.com
    BuyerParty:
      description: "The invoice recipient on a SEND. Same fields as `Party`, with the
        postal address mandatory: `POST /v1/invoices` refuses a `to` without
        `street`, `city` and `postalCode` with 422
        `invoices.buyer_address_required` — Peppol BIS 3.0 requires a buyer
        postal address (BG-8). Matches the SDK's `BuyerParty` type."
      allOf:
        - $ref: "#/components/schemas/Party"
        - type: object
          required:
            - street
            - city
            - postalCode
    SendInvoiceInput:
      description: "`InvoiceInput` as the SEND endpoints require it. The only
        difference from the shared schema is `to`: sending needs the buyer's
        postal address, and the two validation endpoints deliberately do not —
        their job is to accept an incomplete document and tell you what is
        missing."
      allOf:
        - $ref: "#/components/schemas/InvoiceInput"
        - type: object
          properties:
            to:
              $ref: "#/components/schemas/BuyerParty"
    ValidateServerInput:
      description: "`InvoiceInput` as `POST /v1/validate/server` bounds it. This
        endpoint builds the UBL, so it caps what it will build: more than 500
        lines or 10 attachments is refused. `POST /v1/invoices` applies neither
        cap itself."
      allOf:
        - $ref: "#/components/schemas/InvoiceInput"
        - type: object
          properties:
            lines:
              type: array
              maxItems: 500
            attachments:
              type: array
              maxItems: 10
    InvoiceLine:
      type: object
      required:
        - description
        - quantity
        - unitPrice
        - vatRate
      properties:
        description:
          type: string
          description: Line item description
          example: API Integration Setup
        quantity:
          type: number
          exclusiveMinimum: 0
          description: >
            Quantity, a finite number greater than zero on invoices AND credit

            notes. The document kind carries the sign: with `isCreditNote:
            true`,

            getpeppr credits the line, so send the quantity as a positive
            number.

            To reduce the amount credited, add an allowance. A zero or negative

            number, or a string, boolean, object or array, is refused with

            `422 invoices.invalid_line_quantity` before anything is sent.
          example: 1
        unit:
          type: string
          description: Unit of measure (UN/ECE code or human-readable name like "hour",
            "day", "kg")
          default: EA
          example: hours
        unitPrice:
          type: number
          description: Unit price (exclusive of tax)
          example: 500
        vatRate:
          type: number
          description: VAT rate in percent
          example: 21
        vatCategory:
          type: string
          enum:
            - S
            - Z
            - E
            - AE
            - K
            - G
            - O
          default: S
          description: >
            VAT category code. These seven are the whole set this API can route.
            `L` (IGIC, Canary Islands), `M` (IPSI, Ceuta & Melilla) and `B`
            (Italian split payment) are valid EN 16931 codes that our provider
            has no vocabulary for, so this endpoint refuses them with 422
            `unsupported_vat_category` — distinct from `invalid_vat_category`,
            which is a code EN 16931 does not define at all. The limit is ours,
            not the network's: measured against the official rulebook on
            2026-08-28, an IGIC document validates exactly like a standard-rate
            one. `L` and `M` were listed in this enum until that date while this
            endpoint already refused them (GPR-1212).
        taxExemptReason:
          type: string
          description: "Free-text reason for a zero-rated, exempt or reverse-charge line
            (UBL `TaxExemptionReason`). `POST /v1/validate/server` reads it —
            the SDK's UBL builder emits it and refuses conflicting reasons
            inside one VAT group. `POST /v1/invoices` does not forward it: the
            network document is built by the provider from `vatCategory`."
          example: VAT reverse charge
        itemId:
          type: string
          description: Seller's item number
        allowances:
          type: array
          items:
            $ref: "#/components/schemas/LineAllowanceCharge"
          description: Line-level allowances/discounts
        charges:
          type: array
          items:
            $ref: "#/components/schemas/LineAllowanceCharge"
          description: Line-level charges/surcharges
        standardItemId:
          type: string
          description: Standard item identifier (e.g., GTIN/EAN)
        standardItemScheme:
          type: string
          description: 'Scheme for standard item ID (default: "0160" for GTIN)'
        commodityCode:
          type: string
          description: Commodity classification code (e.g., UNSPSC)
        commodityScheme:
          type: string
          description: List ID for commodity classification (e.g., "STI", "CPV")
        properties:
          type: array
          items:
            $ref: "#/components/schemas/ItemProperty"
          description: Additional key-value item properties
        baseQuantity:
          type: number
          exclusiveMinimum: 0
          description: Finite base quantity greater than zero for price calculation (e.g.,
            100 for "price per 100 units"). The invoice line net amount is
            computed as `quantity × unitPrice / baseQuantity`
            (PEPPOL-EN16931-R120); the declared unit price and base quantity are
            transmitted to the network as-is (BT-146/BT-149).
          default: 1
        baseQuantityUnit:
          type: string
          description: Unit code for base quantity
        accountingCost:
          type: string
          description: Buyer accounting reference for this line
    ItemProperty:
      type: object
      required:
        - name
        - value
      properties:
        name:
          type: string
          description: Property name
          example: Color
        value:
          type: string
          description: Property value
          example: Blue
    LineAllowanceCharge:
      type: object
      required:
        - reason
        - amount
      properties:
        reason:
          type: string
          description: Reason for the allowance/charge
        amount:
          type: number
          minimum: 0
          description: Amount, zero or positive. An allowance reduces the line amount and
            a charge increases it — the direction is carried by this field,
            never by the sign. Negative amounts and malformed shapes (arrays
            sent as null) are refused with 422
            `invalid_allowance_charge_amount`; payloads whose derived totals
            overflow are refused with 422 `non_finite_derived_amount`.
    AllowanceCharge:
      type: object
      required:
        - reason
        - amount
        - vatRate
      properties:
        reason:
          type: string
          description: Reason for the allowance/charge
          example: Early payment discount
        amount:
          type: number
          minimum: 0
          description: Amount, zero or positive, exclusive of tax. An allowance reduces
            the invoice total and a charge increases it — the direction is
            carried by this field, never by the sign. Negative amounts and
            malformed shapes (arrays sent as null) are refused with 422
            `invalid_allowance_charge_amount`; payloads whose derived totals
            overflow are refused with 422 `non_finite_derived_amount`.
          example: 50
        vatRate:
          type: number
          description: VAT rate in percent
          example: 21
        vatCategory:
          type: string
          enum:
            - S
            - Z
            - E
            - AE
            - K
            - G
            - O
          default: S
          description: >
            VAT category code — same seven routable codes as on an invoice line.
            See `InvoiceLine.vatCategory`.
        taxExemptReason:
          type: string
          description: Free-text exemption reason, same semantics and same reach as
            `InvoiceLine.taxExemptReason`.
    Attachment:
      type: object
      required:
        - id
      properties:
        id:
          type: string
          description: Document reference identifier
        description:
          type: string
          description: Human-readable description
        filename:
          type: string
          description: Filename (required when content is provided)
        mimeType:
          type: string
          description: MIME type (e.g., "application/pdf")
        content:
          type: string
          description: Raw base64-encoded file content (for embedded attachments), without
            a `data:*;base64,` URI prefix. The `POST /validate/server` endpoint
            rejects content whose decoded size exceeds 2 MB per attachment;
            other endpoints apply their own payload limits.
        url:
          type: string
          format: uri
          description: "External URL reference. ⚠️ Accepted by the local UBL builder only.
            `POST /v1/invoices` refuses any non-empty `url` with
            `invoices.attachment_url_not_supported` (422), including when you
            also send `content`: a Peppol document carries the file itself, our
            provider has no field for a link, and sending both would silently
            discard the URL. Use `content`, `mimeType` and `filename` to send."
    InvoicePeriod:
      type: object
      properties:
        startDate:
          type: string
          format: date
          description: Period start date (ISO 8601)
        endDate:
          type: string
          format: date
          description: Period end date (ISO 8601)
    DeliveryAddress:
      type: object
      required:
        - country
      properties:
        street:
          type: string
        city:
          type: string
        postalCode:
          type: string
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
    Delivery:
      type: object
      properties:
        date:
          type: string
          format: date
          description: Actual delivery date (ISO 8601)
        locationId:
          type: string
          description: Delivery location identifier
        address:
          $ref: "#/components/schemas/DeliveryAddress"
    InvoiceInput:
      type: object
      required:
        - number
        - to
        - lines
      properties:
        sender:
          $ref: "#/components/schemas/InvoiceSender"
        from:
          $ref: "#/components/schemas/SellerContact"
        number:
          type: string
          description: Invoice number (must be unique per supplier)
          example: INV-2026-001
        date:
          type: string
          format: date
          description: Invoice date (ISO 8601, defaults to today)
          example: 2026-02-26
        dueDate:
          type: string
          format: date
          description: Due date (ISO 8601)
          example: 2026-03-28
        currency:
          type: string
          description: ISO 4217 currency code
          default: EUR
          example: EUR
        taxCurrency:
          type: string
          description: Tax reporting currency if different from document currency
        taxCurrencyRate:
          type: number
          description: Exchange rate from document currency to tax currency
        to:
          $ref: "#/components/schemas/Party"
        payeeParty:
          $ref: "#/components/schemas/Party"
        taxRepresentative:
          $ref: "#/components/schemas/Party"
          description: Tax representative party (BG-11). POST /v1/invoices accepts this
            field but currently ignores it because the provider mapping has no
            equivalent. The local UBL builder and POST /v1/validate/server
            include it as cac:TaxRepresentativeParty.
        lines:
          type: array
          items:
            $ref: "#/components/schemas/InvoiceLine"
          minItems: 1
          description: Line items (at least one required). `POST /v1/validate/server`
            refuses more than 500 lines; `POST /v1/invoices` applies no cap of
            its own.
        attachments:
          type: array
          items:
            $ref: "#/components/schemas/Attachment"
          description: Supporting documents/attachments. `POST /v1/validate/server`
            refuses more than 10 attachments and caps embedded content at 2 MB
            decoded per attachment; `POST /v1/invoices` applies neither cap
            itself.
        allowances:
          type: array
          items:
            $ref: "#/components/schemas/AllowanceCharge"
          description: Document-level allowances/discounts
        charges:
          type: array
          items:
            $ref: "#/components/schemas/AllowanceCharge"
          description: Document-level charges/surcharges
        invoicePeriod:
          $ref: "#/components/schemas/InvoicePeriod"
        delivery:
          $ref: "#/components/schemas/Delivery"
        note:
          type: string
          description: Optional note/memo
        paymentReference:
          type: string
          description: Payment reference (e.g., structured communication)
        buyerReference:
          type: string
          description: Buyer reference (required by Peppol BIS 3.0 if no orderReference)
        orderReference:
          type: string
          description: Buyer's purchase order number
        salesOrderReference:
          type: string
          description: Seller's sales order reference
        contractReference:
          type: string
          description: Contract reference number
        projectReference:
          type: string
          description: Project reference identifier
        despatchReference:
          type: string
          description: Despatch advice / delivery note reference
        receiptReference:
          type: string
          description: Receiving advice reference
        paymentTerms:
          type: string
          description: Free-text payment terms (e.g., "Net 30 days")
        paymentMeans:
          type: integer
          description: "UNCL 4461 payment means code. Sendable values: 10 (cash), 30
            (credit transfer), 31 (debit transfer; domestic Danish invoices also
            require paymentIban and paymentBic), 48 (bank card), 57 (standing
            agreement), 58 (SEPA credit transfer). 30 and 58 require a non-empty
            paymentIban, otherwise 422 payment_account_required: the Peppol
            network fatally requires the payee account (rule BR-61). Any other
            value returns 422 unsupported_payment_means. Direct debit (49 and
            59) returns 422 payment_mandate_required: the Peppol network fatally
            requires a mandate reference this API cannot yet carry. Omit the
            field and provide paymentIban to default to a credit transfer — for
            domestic Danish invoices the gateway upgrades that default to 58,
            which is what Danish national rules require."
        paymentIban:
          type: string
          description: IBAN for payment. Required, and must not be empty or
            whitespace-only, when paymentMeans is 30 or 58 (rule BR-61). With no
            paymentMeans, a whitespace-only value also returns 422
            payment_account_required, since it would send the default credit
            transfer with no account; an empty string sends no payment means at
            all.
        paymentBic:
          type: string
          description: BIC/SWIFT code
        taxPointDate:
          type: string
          format: date
          description: Tax point date (when VAT becomes accountable)
        prepaidAmount:
          type: number
          description: Amount already paid before this invoice
        roundingAmount:
          type: number
          description: Rounding applied to the payable amount (-0.99 to 0.99)
        accountingCost:
          type: string
          description: Buyer accounting reference at document level
        invoiceTypeCode:
          type: integer
          enum:
            - 71
            - 80
            - 81
            - 82
            - 83
            - 84
            - 102
            - 130
            - 202
            - 203
            - 204
            - 211
            - 218
            - 219
            - 261
            - 262
            - 295
            - 296
            - 308
            - 325
            - 326
            - 331
            - 380
            - 381
            - 382
            - 383
            - 384
            - 385
            - 386
            - 387
            - 388
            - 389
            - 390
            - 393
            - 394
            - 395
            - 396
            - 420
            - 456
            - 457
            - 458
            - 471
            - 472
            - 473
            - 500
            - 501
            - 502
            - 503
            - 527
            - 532
            - 553
            - 575
            - 623
            - 633
            - 751
            - 780
            - 817
            - 870
            - 875
            - 876
            - 877
            - 935
          description: >
            Invoice type code (UNTDID 1001). The enum is the UNION of the two

            vocabularies that the fatal network rule BR-CL-01 keeps disjoint —

            a value is legal only in its own context:

            - Invoice (isCreditNote absent or false): 71, 80, 81, 82, 84, 102,
              130, 202, 203, 204, 211, 218, 219, 295, 325, 326 (partial), 331,
              380 (commercial, default), 382 (correction), 383 (debit note),
              384 (corrective), 385, 386 (prepayment), 387, 388, 389 (self-billed),
              390, 393, 394, 395, 456, 457, 471, 472, 473, 500, 501, 527, 553,
              575, 623, 633, 751 (accounting purposes), 780, 817, 870, 875, 876,
              877, 935
            - Credit note (isCreditNote: true): 81, 83, 261, 262, 296, 308,
              381 (default), 396, 420, 458, 502, 503, 532
            Omit the field and the document kind decides: 380 for an invoice,

            381 for a credit note. On POST /v1/invoices this field is currently

            inert — the document kind is derived from isCreditNote and the

            provider generates the UBL itself; it shapes the UBL built for

            validation by POST /v1/validate/server.

            That local UBL uses Peppol billing profile 01: P0100/P0101 narrow

            the invoice/credit-note vocabularies for that profile without

            removing globally legal values from this enum.

            P0112 additionally reserves codes 326 and 384 for documents whose

            buyer and seller are both German organizations.
        france:
          type: object
          description: >-
            French e-invoicing reform (Facturation Électronique) declaration.

            Sending this block IS the declaration that the document belongs to
            the French regulated flow. The Peppol France Solution Architecture
            1.3.2 (§4.1) puts that declaration on the sender: "C2 is then aware
            if the invoice is regulated or not (because C1 have declared it, or
            because of a business rule)". getpeppr never infers it — a French
            buyer on an ordinary Peppol send stays an ordinary Peppol send, and
            the invoice keeps the standard Peppol profile.

            Two refusals are answered before anything is submitted, so no
            document reaches the regulated flow under a declaration it cannot
            carry: invoices.france_cadre_invalid (400) when the value is outside
            the published list, and invoices.france_regime_not_eligible (422)
            when the identity issuing the document is not ready for the regime:
            either it is not registered in the French directory (scheme 0225,
            settled registry check, published to the network), or it carries no
            SIREN under scheme 0002, which French invoicing rule BR-FR-10 puts
            on the document itself, or that SIREN is not the first non-tax
            identifier registered on the legal entity — the one the network
            operator writes as the seller's legal registration number. The
            message names which one. A first send, made before your legal entity
            exists, is refused the same way — register the identifiers first,
            then declare.

            The buyer's SIREN travels to the French tax platform as the buyer's
            legal registration number (scheme 0002, nine digits). getpeppr takes
            it from `to.companyId` when you send a SIREN under
            `to.companyIdScheme: "0002"` or a SIRET under `"0009"`, and
            otherwise from a French address in `to.peppolId`:
            `0225:<SIREN>[_suffix]`, `0002:<SIREN>` or `0009:<SIRET>`. A SIRET
            gives its first nine digits, which are the SIREN. When none of them
            gives a valid SIREN, the invoice is refused with 400
            invoices.france_buyer_siren_missing before anything is submitted.

            Ignored on POST /v1/invoices/import: the Enveloped import carries no
            French declaration, so the key has no effect there and no régime is
            recorded. It is not refused — that path accepts unknown keys.
          required:
            - cadreDeFacturation
          properties:
            cadreDeFacturation:
              type: string
              enum:
                - B1
                - S1
                - M1
                - B2
                - S2
                - M2
                - S3
                - B4
                - S4
                - M4
                - S5
                - S6
                - B7
                - S7
                - B8
                - S8
                - M8
              description: "Cadre de Facturation (AFNOR XP Z12-012) — the use case this
                document falls under. Declaring it also constrains the invoice
                `number`: French rule BR-FR-01 allows at most 35 characters,
                only letters, digits and `+ - _ /`, no spaces, and the French
                tax platform currently accepts at most 20 characters until
                2026-12-01. A number outside either limit is refused with 400
                invoices.france_invoice_number_invalid."
            legalMentions:
              type: object
              description: >-
                Replace any of the three legal mentions a French invoice must
                carry (rule BR-FR-05 of the FNFE-MPE socle). A first French
                invoice sends without supplying anything here.

                Precedence is resolved one mention at a time, never as a block:
                this invoice field wins, then the mention saved on the issuing
                legal entity, then the statutory default. An entry that is
                omitted, empty or whitespace-only counts as ABSENT rather than
                as a blank mention, so it falls through to the next level —
                which is the legal entity's saved mention when one exists, and
                only then the statutory default. Overriding one mention leaves
                the other two on whatever they resolved to.

                Each entry must be a string. A null, a number or any other type
                is refused with 400 invoices.france_legal_mentions_invalid —
                omit the field instead.

                Supply the sentence only. getpeppr writes the normative marker
                in front of it, and rule BR-FR-06 refuses a marker that appears
                twice, so do not repeat it inside your own text.

                Two ways to set these. A company sending its own invoices can
                save them once, on its legal entity, from the getpeppr console —
                then omit this block entirely. A platform sending on behalf of
                its customers keeps their terms of sale in its own system and
                sends them here, per invoice: we do not store a platform's
                customers' terms, and there is no screen where they could be
                entered. What you send here wins over anything saved, mention by
                mention, so an invoice can override one sentence without
                disturbing the other two.

                Override what your terms of sale actually say. The forty-euro
                recovery indemnity is fixed by article D441-5 and is the same
                for every French issuer, but the penalty rate and the discount
                are not: three times the legal interest rate with no discount is
                only what the law applies when a contract is silent.
              properties:
                recoveryCosts:
                  type: string
                  description: Flat recovery indemnity. Default states forty euros under article
                    D441-5 of the code de commerce.
                latePaymentPenalties:
                  type: string
                  description: Late-payment penalties. Default states three times the legal
                    interest rate, due the day after the due date.
                earlyPaymentDiscount:
                  type: string
                  description: Early-payment discount, or the statement that there is none.
                    Default states that no discount applies.
        isCreditNote:
          type: boolean
          description: Set to true for credit notes (sets type code to 381)
        invoiceReference:
          type: string
          description: Reference to original invoice (required when isCreditNote is true)
        invoiceReferenceDate:
          type: string
          format: date
          description: "Issue date of the invoice being credited, YYYY-MM-DD (BT-26). Sent
            with invoiceReference. Optional in general, required on a credit
            note that declares the French regime: French rule BR-FR-CO-05 counts
            the credited invoice only with its date. Missing, it is refused with
            400 invoices.france_credit_note_reference_invalid before anything is
            submitted."
        _draft:
          type: boolean
          description: Unsupported by the current Storecove provider. `true` returns 422
            with `drafts_not_supported`; omit this field to submit immediately.
          default: false
    ImportInvoiceRequest:
      type: object
      required:
        - file
        - filename
        - to
      properties:
        file:
          type: string
          description: >-
            Base64-encoded UBL Invoice or CreditNote. It must decode to exactly
            the bytes you intend to send — getpeppr never repairs your document,
            so a lossy decode is refused (`invalid_base64`) rather than patched.

            Its content reaches your recipient as you wrote it, but byte
            equality is not guaranteed: the network re-serialises the document
            in transit. Seal a canonical form (C14N) rather than the raw bytes.
        filename:
          type: string
          description: Original filename (e.g., "invoice.xml")
          example: invoice-2026-001.xml
        mimeType:
          type: string
          description: MIME type of the document. Optional, and currently ignored by the
            gateway — the file is always read as UBL XML. `@getpeppr/sdk` fills
            it from the filename extension; sending it by hand changes nothing.
          example: application/xml
        to:
          type: object
          required:
            - peppolId
          description: "The recipient, declared explicitly. **Required** and the sole
            routing authority: getpeppr reads the document's customer EndpointID
            only to warn about disagreement, never to derive or rewrite the
            destination. A request without this value is refused with
            `missing_recipient`."
          properties:
            peppolId:
              type: string
              description: Recipient Peppol participant identifier, `scheme:identifier`.
              example: 0208:0685660237
        sender:
          $ref: "#/components/schemas/InvoiceSender"
    ImportInvoiceResult:
      description: The `201` body of `POST /invoices/import` — a `SendResult`
        carrying, in addition, the provenance of the rulebook the document was
        judged against and what getpeppr does not guarantee about your bytes.
      allOf:
        - $ref: "#/components/schemas/SendResult"
        - type: object
          properties:
            rulebook:
              allOf:
                - $ref: "#/components/schemas/UblValidationVerdict/properties/rulebook"
              description: "The rulebook this document was validated against before it was
                sent. **Absent when validation did not run** — that is, when the
                call carried `x-skip-validation: true`. It is never invented: a
                document nobody judged names no rulebook."
            transmission:
              type: object
              required:
                - mode
                - bytePreservation
              description: >-
                How the bytes that left getpeppr relate to the bytes you
                supplied. Present on every `201` this endpoint produces,
                including when `x-skip-validation: true` removed `rulebook` —
                validation and byte handling are independent.


                ⚠️ Deliberately NOT `required`, and the reason is worth knowing
                before you rely on it. An identical replay of an
                `Idempotency-Key` returns the body cached for it, verbatim and
                untouched; an entry cached before this field existed therefore
                has none, and the cache holds entries for 24 hours. Declaring it
                `required` would have been a promise a real replay can break —
                the same reason `rulebook` never claimed unconditional presence.


                **Absent is not `false`.** It means this response did not say,
                never that your bytes are safe.


                This is a **guarantee, not a measurement of your document**.
                `not_guaranteed` does not mean "we changed it" — getpeppr
                forwards your bytes verbatim and takes no decision from their
                content. It means the Peppol network re-serialises in transit,
                so byte equality is never something to rely on. Measured
                2026-08-18: namespace declarations reordered, numeric character
                references resolved, whitespace inside tags dropped, no element
                or value altered.


                ⛔ Do not read this as "canonicalise first and your bytes will
                survive". A document already in canonical form has been observed
                to come back unchanged **once**, on a document the provider had
                itself generated — which is exactly the measurement that misled
                us before. Nothing guarantees it, and a normaliser that happens
                to be idempotent today is not a contract. Seal a canonical form
                because the canonical form is what survives, never because the
                bytes might.


                In one line: byte-for-byte equality is not guaranteed, and the
                remedy is to seal a canonical form (C14N) rather than raw bytes.


                **Seal a canonical form (C14N), not raw bytes.** A digest over
                the bytes you sent attests to your record, not to what was
                transmitted.
              properties:
                mode:
                  type: string
                  example: enveloped
                  description: >-
                    Whose rendering went out. `enveloped` means getpeppr
                    forwarded the document you supplied, wrapped only in the
                    Peppol SBDH, rather than generating UBL from JSON.


                    Deliberately NOT an `enum`. An enum is a closed set by
                    specification — code generated from one cannot represent a
                    value added later, and a strict validator rejects it. Since
                    a second import mode may be named in future, an enum here
                    would promise exhaustiveness we do not intend. `enveloped`
                    is the only value this version sends; compare against the
                    values you know and treat an unfamiliar one as "not a JSON
                    send". `@getpeppr/sdk` types it `string` for the same
                    reason.
                bytePreservation:
                  type: string
                  example: not_guaranteed
                  description: Whether byte equality between what you supplied and what reaches
                    the recipient is guaranteed. It is not, and the reason is
                    downstream of getpeppr. `not_guaranteed` is the only value
                    this version sends; same open-set reasoning as `mode`.
    SendResult:
      type: object
      required:
        - id
        - status
      properties:
        id:
          type: string
          deprecated: true
          description: The provider (Storecove) document GUID. **Deprecated:** `id` means
            the submission ID on `GET /invoices` and the provider GUID here, so
            code that carries it between surfaces has to know which one it
            holds. Read `providerDocumentId` or `submissionId` instead. Both are
            accepted as input on every `/v1` endpoint that takes an invoice id —
            the `/v1/invoices/{id}` routes and the `invoiceId`/`documentId`
            filter on `/v1/events`. This field is unchanged and stays.
          example: "12345"
        providerDocumentId:
          type: string
          description: Provider (Storecove) document GUID — the same value as `id`.
        submissionId:
          type: string
          format: uuid
          description: "getpeppr submission ID — the local record of this document, and
            the `submissionId` your webhooks carry. **Absent** when the local
            record could not be written: the document is on the Peppol network
            either way, and an invented id would resolve to nothing."
        status:
          $ref: "#/components/schemas/DocumentStatus"
          description: Raw provider status at submission time — may occasionally carry a
            provider value outside the DocumentStatus vocabulary.
        peppolMessageId:
          type: string
          description: "Peppol AS4 message ID. **Never present on this response**: it only
            exists once the transmission has actually gone out, seconds after
            this call returns. Read it from `GET
            /invoices/{id}?include=evidence`. The field is declared here because
            the SDK shares one result type across both surfaces."
        clearing:
          type: object
          description: "The DGFiP leg of a French regulated send. **Never present on this
            response**: the provider reports it 8 to 15 seconds after this call
            returns. Read it from `GET /invoices/{id}?include=evidence`. The
            field is declared here because the SDK shares one result type across
            both surfaces."
          required:
            - authority
            - network
          properties:
            authority:
              type: string
            network:
              type: string
            reference:
              type: string
        paymentReport:
          allOf:
            - $ref: "#/components/schemas/PaymentReport"
          description: "**Never present on this response**: a collection report is filed
            on an invoice already sent. Read it from `GET /invoices/{id}`.
            Declared here because the SDK shares one result type across both
            surfaces."
        ublXml:
          type: string
          description: Generated UBL XML (for debugging)
        warnings:
          type: array
          items:
            $ref: "#/components/schemas/ValidationWarning"
          description: Non-blocking validation warnings
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp, when the provider supplied one. **Not
            guaranteed** on this response — read it defensively rather than
            substituting the moment of your call, which is a different
            measurement wearing the same name.
          example: 2026-02-26T10:30:00Z
        duplicateOf:
          type: string
          description: "Provider document GUID of an earlier send of **this same
            document** (same sender, same recipient, same invoice number).
            **Absent** on an ordinary first send — its presence means a second
            copy has been accepted for transmission, because Peppol does not
            de-duplicate and we did not block a resend this far apart. It rides
            on the `201`, so it says nothing about delivery: read that from the
            document's status, as for any send. Within a short window the same
            document is refused with `409 duplicate_document` instead."
          example: e8ec77a9-0000-4000-8000-000000000000
    DocumentStatus:
      type: string
      enum:
        - submitted
        - delivered
        - accepted
        - rejected
        - paid
        - failed
        - cleared
        - acknowledged
        - in_process
        - under_query
        - conditionally_accepted
        - partially_paid
        - no_action
        - unknown
      description: >
        Document lifecycle status (mapped from Storecove webhook events):

        - `submitted`: Document submitted for Peppol delivery

        - `delivered`: Received by the recipient's access point (corner 3)

        - `accepted`: Accepted by the recipient (corner 4)

        - `rejected`: Rejected by the recipient

        - `paid`: Payment confirmed by the recipient

        - `failed`: The invoice failed (final state): delivery failed, or the
        recipient's accredited platform rejected it after receiving it (France:
        Rejetée, 213). Correct and resend; `detail.platformFiscal` tells which

        - `cleared`: Cleared by the sender's tax authority (e.g. KSA, PT; in
        France, the invoice data reported to the DGFiP). It does not say the
        recipient received the document.

        - `acknowledged`: Receipt acknowledged by corner 4

        - `in_process`: Processing started by corner 4

        - `under_query`: Under query by corner 4

        - `conditionally_accepted`: Conditionally accepted

        - `partially_paid`: Partially paid

        - `no_action`: No recipients found for delivery

        - `unknown`: Unrecognized status from gateway
    InvoiceSummary:
      type: object
      description: Row from the invoice-submissions ledger, as returned by GET
        /invoices (camelCase keys, returned as stored).
      properties:
        id:
          type: string
          format: uuid
          deprecated: true
          description: "getpeppr submission ID. **Deprecated:** `id` names a different
            thing depending on the surface — the submission ID here, the
            provider GUID on `POST /invoices` and `GET /invoices/{id}`. Read
            `submissionId` or `providerDocumentId` instead; both are accepted as
            input on every `/v1` endpoint that takes an invoice id — the
            `/v1/invoices/{id}` routes and the `invoiceId`/`documentId` filter
            on `/v1/events`. Two narrow exceptions, both answered explicitly
            rather than guessed: an id naming two of your documents answers
            `404`, and replaying one `Idempotency-Key` across the two names of
            one invoice answers `422 idempotency_key_reuse`. The field itself is
            not going away without a decided removal date."
        submissionId:
          type: string
          format: uuid
          description: getpeppr submission ID — the same value as `id` on this surface,
            under a name that cannot be confused with the provider GUID.
        accountId:
          type: string
          format: uuid
          description: Internal account identifier (surrogate key, not the organization ID)
        providerDocumentId:
          type: string
          description: Provider (Storecove) document GUID. Pass this to `GET
            /invoices/{id}` or `GET /invoices/{id}/as/{format}` to move from a
            list row to the document itself.
        environment:
          type: string
          enum:
            - sandbox
            - production
        invoiceNumber:
          type: string
          description: Invoice number as submitted
        issueDate:
          type: string
          format: date
        currency:
          type: string
          description: ISO 4217 currency code
          example: EUR
        recipientName:
          type:
            - string
            - "null"
        recipientPeppolId:
          type:
            - string
            - "null"
        recipientCountry:
          type:
            - string
            - "null"
        isCreditNote:
          type: boolean
        totalAmount:
          type:
            - integer
            - "null"
          description: "Grand total including tax, in the minor units of the document's
            own currency (2 for EUR, 0 for JPY, 3 for BHD) — display only, never
            the legal payable amount. It is `cbc:TaxInclusiveAmount` (BT-112) on
            both send paths: derived from line-level allowances and charges,
            document-level allowances and charges, and VAT for JSON sent through
            `POST /v1/invoices`; read from your own UBL for a document imported
            through `POST /v1/invoices/import`. Prepaid amount and payable
            rounding affect the separate BT-115 settlement amount, never this
            display total. Treat null as \"not available\" rather than as a
            specific condition — among other reasons an imported document may
            not state the amount, may state one outside the range this API
            records, or may state one we decline to record (a negative figure,
            or a currency contradicting the document's own). A null in this
            field is never itself a reason to retry: the import never
            substitutes or borrows a neighbouring amount, and it succeeds
            regardless. Note that a document whose amounts contradict its own
            currency is normally rejected earlier, by validation
            (`PEPPOL-EN16931-R051`), and only reaches this field at all when
            validation is explicitly skipped."
        hasAttachment:
          type: boolean
        status:
          $ref: "#/components/schemas/DocumentStatus"
        legalEntityId:
          type:
            - string
            - "null"
        legalEntityName:
          type:
            - string
            - "null"
        detail:
          $ref: "#/components/schemas/StatusDetail"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    StatusDetail:
      type: object
      description: >
        Structured national status detail, one entry per axis. Present on list
        items when the jurisdiction provides structured lifecycle codes (e.g.
        the French DGFiP CTC lifecycle); omitted otherwise. The native code is
        preserved, never over-translated.
      properties:
        platformFiscal:
          $ref: "#/components/schemas/StatusDetailEntry"
        delivery:
          $ref: "#/components/schemas/StatusDetailEntry"
        businessDisposition:
          $ref: "#/components/schemas/StatusDetailEntry"
        settlement:
          $ref: "#/components/schemas/StatusDetailEntry"
    StatusDetailEntry:
      type: object
      required:
        - axis
        - jurisdiction
        - code
        - label
        - codeSystem
        - codeVersion
      properties:
        axis:
          type: string
          enum:
            - platformFiscal
            - delivery
            - businessDisposition
            - settlement
            - unmapped
        jurisdiction:
          type: string
          description: Jurisdiction the code belongs to (e.g. "FR", or "*" for network-wide)
        code:
          type: string
          description: Native status code (e.g. "213") — not the label
        label:
          type: string
          description: Human label for the native code (e.g. "Rejetée")
        codeSystem:
          type: string
          description: Code system identifier (e.g. "fr-ctc-lifecycle")
        codeVersion:
          type: string
        standardCode:
          type: object
          description: The international code behind this entry, when there is one. For a
            status your recipient sent back as a Peppol Invoice Response,
            `system` is `UNCL4343-T111` and `code` is its response code (`AB`,
            `IP`, `UQ`, `CA`, `RE`, `AP`, `PD`).
          properties:
            system:
              type: string
            code:
              type: string
        reason:
          type: string
          description: Machine-readable reason. When `standardCode.system` is
            `UNCL4343-T111`, it is the Peppol OPStatusReason code your recipient
            gave (for example `REF` for references incorrect, `PRI` for prices
            incorrect). When `codeSystem` is `fr-ctc-lifecycle` and `code` is a
            French lifecycle status that carries a motif (for example `210`
            Refusée or `213` Rejetée), it is the French status reason code
            (MDT-113) the recipient's platform sent, for example `CALCUL_ERR` or
            `REJ_SEMAN`. Only codes the French standard allows for that status
            are returned. Free text written by the recipient is never returned.
        warnings:
          type: array
          items:
            type: string
        failureCategory:
          type: string
          enum:
            - transport
            - routing
            - syntax
            - semantic
            - authority
        payment:
          type: object
          properties:
            amount:
              type: number
            currency:
              type: string
            date:
              type: string
        paymentSemantics:
          type: string
          enum:
            - received
            - initiated
        actor:
          type: string
          enum:
            - seller
            - buyer
    InvoiceStatus:
      type: object
      required:
        - id
        - submissionId
        - providerDocumentId
        - number
        - status
        - createdAt
      description: >-
        The invoice as getpeppr recorded it. Until 2026-08 this endpoint read
        Storecove's sending-evidence envelope and reshaped it as if it were a
        document — the envelope carries no status at all, so a delivered invoice
        answered with almost nothing. It now projects the submission ledger, the
        same source `GET /invoices` reads.

        ⚠️ `number` here is the field `GET /invoices` calls `invoiceNumber`, and
        `id` is the provider GUID where the list's `id` is the submission ID.
        Both asymmetries are historic and frozen rather than broken; use
        `submissionId` / `providerDocumentId`, which mean the same thing
        everywhere.
      properties:
        id:
          type: string
          deprecated: true
          description: The provider (Storecove) document GUID. **Deprecated** because `id`
            names a different value on `GET /invoices`; read
            `providerDocumentId` or `submissionId`. Unchanged, and staying until
            a removal date is decided.
        submissionId:
          type: string
          format: uuid
          description: getpeppr submission ID — the local record of this document.
        providerDocumentId:
          type: string
          description: Provider (Storecove) document GUID — the same value as `id`.
        accountId:
          type: string
          format: uuid
        environment:
          type: string
          enum:
            - sandbox
            - production
        number:
          type: string
          description: Invoice number as submitted (`invoiceNumber` on `GET /invoices`).
        issueDate:
          type: string
          format: date
        currency:
          type: string
          example: EUR
        recipientName:
          type:
            - string
            - "null"
        recipientPeppolId:
          type:
            - string
            - "null"
        recipientCountry:
          type:
            - string
            - "null"
        isCreditNote:
          type: boolean
        totalAmount:
          type:
            - integer
            - "null"
          description: "Grand total including tax, in the minor units of the document's
            own currency: `cbc:TaxInclusiveAmount` (BT-112), including
            line-level allowances and charges, document-level allowances and
            charges, and VAT. Prepaid amount and payable rounding affect the
            separate BT-115 settlement amount, never this display total. The
            value is derived from JSON sent through `POST /v1/invoices` or read
            from UBL supplied to `POST /v1/invoices/import`. Treat null as \"not
            available\" rather than as a specific condition."
        hasAttachment:
          type: boolean
        status:
          $ref: "#/components/schemas/DocumentStatus"
          description: The document's current status, as recorded by getpeppr from the
            provider's webhooks.
        legalEntityId:
          type:
            - string
            - "null"
          format: uuid
        legalEntityName:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        detail:
          type: object
          description: Layer-2 national status detail, when a jurisdiction reports one.
        peppolMessageId:
          type: string
          description: The Peppol AS4 message id. Returned **only** with
            `?include=evidence`, and only once the document has actually gone
            out — it is read from the network on demand rather than stored.
        clearing:
          type: object
          description: "The DGFiP leg of a French regulated send: the invoice's data (flow
            1) was reported to the administration. Returned **only** with
            `?include=evidence`, only for an invoice sent under the French
            regime, and only once the provider reports that leg — read on
            demand, never stored. It says the administration was informed, never
            that the buyer received the invoice. The flow itself is served by
            `GET /invoices/{id}/as/clearing`."
          required:
            - authority
            - network
          properties:
            authority:
              type: string
              example: FR-DGFiP
            network:
              type: string
              example: fr-dgfip
            reference:
              type: string
              description: The flow's reference at the administration. Absent when unknown.
              example: FFE0111A_PPF250_PPF2502026092714013472838
        paymentReport:
          $ref: "#/components/schemas/PaymentReport"
    PaymentReport:
      type: object
      description: "The collection report filed by `POST /invoices/{id}/mark-as` with
        `state: paid`, on an invoice sent under the French regime. Present once
        a report exists, read from our own record — no `include` needed. It is
        not a status of the invoice: the invoice's `status` does not move when a
        payment is reported. `processed` says the provider processed the report,
        never that the tax administration received it."
      required:
        - status
        - updatedAt
      properties:
        status:
          type: string
          description: "`in_progress` — being filed. `submitted` — the provider accepted
            it. `processed` — the provider processed it. `failed` — refused;
            calling `mark-as` again retries. `interrupted` — the last attempt
            got no answer, so it may already be filed; calling again within 30
            minutes of the first attempt resolves it without filing twice.
            `provider_failed` — the provider reported it as failed; whether the
            tax authority received it is not established, contact support.
            `unconfirmed` — whether it was filed could not be established;
            contact support. New values may be added: treat an unknown one as
            not settled."
          enum:
            - in_progress
            - submitted
            - processed
            - failed
            - interrupted
            - provider_failed
            - unconfirmed
          example: processed
        id:
          type: string
          description: The provider's id of the report itself, once it answered.
          example: 6e675e95-8237-408f-aee3-4aacdf962e32
        updatedAt:
          type: string
          format: date-time
    ValidationError:
      type: object
      required:
        - field
        - message
      properties:
        field:
          type: string
          description: Field path (e.g., "to.street")
        message:
          type: string
          description: Error message
        ruleId:
          type: string
          description: Stable Peppol or getpeppr rule ID (e.g., "BR-07")
        suggestion:
          type: string
          description: Suggested fix
    ValidationWarning:
      type: object
      required:
        - field
        - message
      properties:
        field:
          type: string
          description: Field path (e.g., "lines[0].vatRate")
        message:
          type: string
          description: Warning message
        ruleId:
          type: string
          description: Stable Peppol or getpeppr rule ID (e.g., "BR-CO-26")
    ClientValidationResult:
      type: object
      required:
        - valid
        - errors
      properties:
        valid:
          type: boolean
          description: Whether the invoice passed all validation checks
        errors:
          type: array
          items:
            type: string
          description: List of validation error messages
      example:
        valid: true
        errors: []
    ServerValidationMessage:
      type: object
      required:
        - severity
        - message
      properties:
        severity:
          type: string
          enum:
            - error
            - warning
        message:
          type: string
          description: Validation message
        location:
          type: string
          description: Location in the XML document (XPath)
        ruleId:
          type: string
          description: Rule ID (e.g., Schematron rule)
    ServerValidationResult:
      type: object
      required:
        - valid
        - errors
        - warnings
        - ubl
        - xsd
        - schematron
        - providerSendability
      properties:
        valid:
          type: boolean
          description: Overall result of getpeppr's offline SDK, UBL-build, partial
            Schematron, and gateway-owned checks. Not a provider sendability
            verdict; Storecove is not called by this endpoint.
        errors:
          type: array
          items:
            $ref: "#/components/schemas/ValidationError"
          description: SDK-level validation errors from validateInvoice
        warnings:
          type: array
          items:
            $ref: "#/components/schemas/ValidationWarning"
          description: SDK-level validation warnings from validateInvoice
        ubl:
          type: object
          required:
            - valid
            - errors
          properties:
            valid:
              type: boolean
              description: UBL XML generation completed without gateway build errors
            errors:
              type: array
              items:
                $ref: "#/components/schemas/ServerValidationMessage"
        xsd:
          type: object
          required:
            - valid
            - errors
          deprecated: true
          description: Deprecated compatibility field. The gateway verifies UBL XML
            generation but does not run a standalone XSD validator.
          properties:
            valid:
              type: boolean
              description: Mirrors `ubl.valid` for backward compatibility; not a standalone
                XSD validation result.
            errors:
              type: array
              items:
                $ref: "#/components/schemas/ServerValidationMessage"
            note:
              type: string
              description: Compatibility note explaining that standalone XSD validation is not
                performed
        schematron:
          type: object
          required:
            - valid
            - coverage
            - errors
            - warnings
          properties:
            valid:
              type: boolean
              description: |
                No problem found among the checks this endpoint ran — **not** a
                statement of Peppol conformance, and not a prediction that the
                document will be accepted.

                This offline pass normally runs 40 registered offline checks:
                exact network-rule equivalents plus ten explicit `GETPEPPR-*`
                diagnostics. The legacy provider-capability diagnostic
                `unsupported_vat_category` can also appear, but is outside that
                count. The network applies 335 fatal rules to a document with no
                national family, and more when there is one. Read `coverage` for
                what was actually checked.

                To run the complete official rulebooks instead, build your UBL
                and send it to `POST /validate/ubl`.
            coverage:
              type: object
              required:
                - rulesChecked
                - ofNetworkFatalRules
              description: |
                What `valid` is worth. Present so that a caller never has to
                infer the scope of the check from the verdict alone.
              properties:
                rulesChecked:
                  type: integer
                  description: |
                    How many registered offline checks ran. The additional
                    `unsupported_vat_category` capability diagnostic is not
                    counted. `0` means no registered check ran — the document
                    failed to build as UBL, so `valid` carries no information.
                  example: 40
                ofNetworkFatalRules:
                  type: string
                  enum:
                    - partial
                  description: >
                    Always `partial`. A single-valued enum on purpose: this pass

                    is never complete, so there is no value a caller could
                    branch

                    on to conclude otherwise.
            errors:
              type: array
              items:
                $ref: "#/components/schemas/ServerValidationMessage"
            warnings:
              type: array
              items:
                $ref: "#/components/schemas/ServerValidationMessage"
        countryRules:
          type: array
          description: Gateway-level country-rule findings the SDK validators cannot see
            (some need the account's registered Peppol identity). A non-empty
            array means POST /v1/invoices would reject the same payload with a
            gateway-owned 422 carrying the same code. Does not affect `valid`
            and does not include provider validation.
          items:
            type: object
            required:
              - code
              - message
              - docs
            properties:
              code:
                type: string
                description: Official Peppol rule id (e.g. "NL-R-003")
                example: NL-R-003
              message:
                type: string
                description: Actionable explanation of the violated rule
              docs:
                type: string
                format: uri
                description: Official Peppol documentation page for the rule
        providerSendability:
          type: string
          enum:
            - not_checked
          description: Storecove Standard JSON validation was not run. A later POST
            /v1/invoices can still return 422 even when valid is true.
    Contact:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
          format: uuid
          description: Unique contact ID
          example: 6f1c2b3a-9d4e-4c5f-8a7b-1e2d3c4b5a69
        accountId:
          type: string
          format: uuid
          description: Internal account identifier (surrogate key, not the organization ID)
        name:
          type: string
          description: Business name
          example: ACMEDIA
        peppolId:
          type: string
          description: Peppol participant ID
          example: 0208:0685660237
        vatNumber:
          type: string
          description: VAT number
          example: BE0685660237
        companyId:
          type: string
          description: Company registration number
        street:
          type: string
          description: Street address
        city:
          type: string
          description: City
        postalCode:
          type: string
          description: Postal/zip code
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
          example: BE
        email:
          type: string
          format: email
          description: Email address
        phone:
          type: string
          description: Phone number
        isClient:
          type: boolean
          description: Whether this contact is a client
        isProvider:
          type: boolean
          description: Whether this contact is a provider/supplier
        directoryVerified:
          type: boolean
          description: Whether the Peppol ID was found in the Peppol Directory at last check
        directoryLastChecked:
          type:
            - string
            - "null"
          format: date-time
          description: Timestamp of the last Peppol Directory check (null if never checked)
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp
        updatedAt:
          type: string
          format: date-time
          description: Last update timestamp
    ContactUpdate:
      type: object
      description: Partial update — only the fields present in the body are
        overwritten; omitted fields keep their current value. No field is
        required.
      properties:
        name:
          type: string
        peppolId:
          type: string
        vatNumber:
          type: string
        companyId:
          type: string
        street:
          type: string
        city:
          type: string
        postalCode:
          type: string
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
        email:
          type: string
          format: email
        phone:
          type: string
        isClient:
          type: boolean
        isProvider:
          type: boolean
    ContactInput:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Business name (required)
          example: ACMEDIA
        peppolId:
          type: string
          description: Peppol participant ID
        vatNumber:
          type: string
          description: VAT number
        companyId:
          type: string
          description: Company registration number
        street:
          type: string
        city:
          type: string
        postalCode:
          type: string
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
        email:
          type: string
          format: email
        phone:
          type: string
          description: Phone number
        isClient:
          type: boolean
          description: Whether this contact is a client (default true)
          default: true
        isProvider:
          type: boolean
          description: Whether this contact is a provider/supplier (default false)
          default: false
    BankAccount:
      type: object
      required:
        - id
        - name
        - type
      properties:
        id:
          type: string
          format: uuid
          description: Unique bank account ID
          example: 2a7b4c1d-5e6f-4a8b-9c0d-3f2e1d4c5b6a
        accountId:
          type: string
          format: uuid
          description: Internal account identifier (surrogate key, not the organization ID)
        name:
          type: string
          description: Display name
          example: Main EUR Account
        type:
          type: string
          enum:
            - iban
            - number
          description: Account type
          example: iban
        iban:
          type: string
          description: IBAN (when type is "iban")
          example: BE68539007547034
        number:
          type: string
          description: Account number (when type is "number")
        bic:
          type: string
          description: BIC/SWIFT code
          example: BBRUBEBB
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
          example: BE
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    BankAccountInput:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Display name (required)
          example: Main EUR Account
        type:
          type: string
          enum:
            - iban
            - number
          default: iban
          description: Account type
        iban:
          type: string
          description: IBAN (for type "iban" accounts; not enforced server-side)
        number:
          type: string
          description: Account number (for type "number" accounts; not enforced server-side)
        bic:
          type: string
          description: BIC/SWIFT code
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
    BankAccountUpdate:
      type: object
      description: Partial update — only the fields present in the body are
        overwritten; omitted fields keep their current value. No field is
        required.
      properties:
        name:
          type: string
        type:
          type: string
          enum:
            - iban
            - number
        iban:
          type: string
        number:
          type: string
        bic:
          type: string
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
    DirectoryEntry:
      type: object
      required:
        - name
        - peppolId
        - country
        - capabilities
      properties:
        name:
          type: string
          description: Registered business name
          example: ACMEDIA
        peppolId:
          type: string
          description: Peppol participant ID
          example: 0208:0685660237
        country:
          type: string
          description: Country of registration
          example: BE
        capabilities:
          type: array
          items:
            type: string
          description: Supported document types (e.g., "invoice", "credit_note")
          example:
            - invoice
            - credit_note
        registrationDate:
          type: string
          description: Date of Peppol registration (ISO 8601 date)
          example: 2020-11-20
        vatNumber:
          type: string
          description: VAT number if available from directory
          example: "0685660237"
        additionalIds:
          type: array
          description: Additional identifiers (e.g., GLN, DUNS)
          items:
            type: object
            properties:
              scheme:
                type: string
                example: be:cbe
              value:
                type: string
                example: "0685660237"
        contactInfo:
          type: object
          description: Contact information from directory
          properties:
            name:
              type: string
            email:
              type: string
            phone:
              type: string
        website:
          type: string
          description: Website URL
          example: https://acmedia.be
    OnboardingResult:
      type: object
      required:
        - legalEntityId
      properties:
        legalEntityId:
          type: string
          description: Legal entity identifier for your account
          example: "12345"
        peppolIdentifier:
          type:
            - object
            - "null"
          description: Peppol ID registration result (null if not requested)
          properties:
            registered:
              type: boolean
              description: Whether the Peppol ID was successfully registered
            id:
              type: object
              description: Registered Peppol identifier (present when registered=true)
              properties:
                scheme:
                  type: string
                  example: "0208"
                identifier:
                  type: string
                  example: "0685660237"
            error:
              type: string
              description: Error message (present when registered=false)
            schemeWarning:
              type: string
              description: Warning if an uncommon Peppol scheme was used
        directoryWarning:
          type: string
          description: Warning if the Peppol ID was not found in the Peppol Directory
    IdentityResponse:
      type: object
      required:
        - environment
        - legalEntity
        - identifiers
        - sandboxFirstSend
      properties:
        environment:
          type: string
          enum:
            - sandbox
            - production
          description: The environment your API key targets
          example: sandbox
        legalEntity:
          type:
            - object
            - "null"
          description: Your account's own legal entity in this environment. `null` when no
            identity has been registered here yet — never a `404`.
          properties:
            companyName:
              type:
                - string
                - "null"
              example: Bright Health Ltd
            country:
              type:
                - string
                - "null"
              minLength: 2
              maxLength: 2
              description: ISO 3166-1 alpha-2 country code
              example: GB
            address:
              type: object
              description: Fields not captured at registration are `null`
              properties:
                line1:
                  type:
                    - string
                    - "null"
                  example: 10 King Street
                city:
                  type:
                    - string
                    - "null"
                  example: London
                zip:
                  type:
                    - string
                    - "null"
                  example: EC2V 8EA
            createdAt:
              type: string
              format: date-time
              example: 2026-06-01T10:00:00.000Z
        identifiers:
          type: array
          description: The Peppol identifiers registered for your own account in this
            environment. May be empty.
          items:
            type: object
            required:
              - scheme
              - value
              - status
              - createdAt
            properties:
              scheme:
                type: string
                description: Peppol identifier scheme
                example: GB:VAT
              value:
                type: string
                example: gb123456789
              status:
                $ref: "#/components/schemas/PeppolLifecycleStatus"
              createdAt:
                type: string
                format: date-time
                example: 2026-06-01T10:05:00.000Z
        sandboxFirstSend:
          description: Provider-compatible tax fields for a sandbox integration fixture,
            or a fail-closed reason. Null for production keys. Read this before
            copying tax fields into a first-send example; never guess from the
            sender scheme.
          oneOf:
            - type: object
              required:
                - status
                - taxMode
                - line
              properties:
                status:
                  type: string
                  const: ready
                taxMode:
                  type: string
                  const: outside_scope
                line:
                  type: object
                  required:
                    - vatRate
                    - vatCategory
                    - taxExemptReason
                  properties:
                    vatRate:
                      type: number
                      const: 0
                    vatCategory:
                      type: string
                      const: O
                    taxExemptReason:
                      type: string
                      minLength: 1
            - type: object
              required:
                - status
                - taxMode
                - line
              properties:
                status:
                  type: string
                  const: ready
                taxMode:
                  type: string
                  const: reverse_charge
                line:
                  type: object
                  required:
                    - vatRate
                    - vatCategory
                    - taxExemptReason
                  properties:
                    vatRate:
                      type: number
                      const: 0
                    vatCategory:
                      type: string
                      const: AE
                    taxExemptReason:
                      type: string
                      minLength: 1
            - type: object
              required:
                - status
                - code
                - message
              properties:
                status:
                  type: string
                  const: blocked
                code:
                  type: string
                  minLength: 1
                message:
                  type: string
                  minLength: 1
            - type: "null"
    PeppolLifecycleStatus:
      type: string
      enum:
        - pending
        - verifying
        - verified
        - no_registry
        - unsupported_scheme
        - verification_failed
        - awaiting_authz
        - expired
        - attested
        - provisioning
        - awaiting_release
        - active
        - provisioning_failed
        - registration_failed
        - archived
      description: >
        Public lifecycle status — one vocabulary shared by platform sub-tenants

        (`GET /legal-entities/{id}`) and your own account's identifiers

        (`GET /identity`). Production `GET /legal-entities/{id}` layers

        attestation status on top of registry verification.


        `no_registry` means the published Peppol code list states that no entity

        stands behind this identifier's scheme ("No entity behind id"), so there
        is

        no registry to query and none was consulted. It is settled and sendable
        in

        **sandbox**, which is what lets you exercise the platform flow without

        registering an identifier that belongs to someone else. It is
        deliberately

        not reported as `verified`: it proves the identifier is registered and

        routable on the test network, and nothing about a company existing or
        about

        your right to act for it. Registering such a scheme with a production
        key is

        refused.


        `unsupported_scheme` means no automatic validator is currently active
        for

        this identifier scheme. It is a settled public answer, not a registry

        rejection and not work in progress. Contact support before sending. The

        gateway may retry automatically if support for the scheme is added
        later.


        `awaiting_release` means the identifier is verified but its address is

        still registered with another Peppol provider. getpeppr registers it

        automatically once that provider removes it. On `GET /identity` it

        applies to your own account's production identifiers; a production send

        is meanwhile refused with `422 peppol_identity_not_verified`, code

        `address_held_elsewhere`.
      example: active
    LegalEntityCreateRequest:
      type: object
      required:
        - externalId
        - companyName
        - country
        - address
      description: Send exactly one of `identifier` or `identifiers`. Sending both, an
        empty list, more than three entries or the same participant twice is
        refused with `400 legal_entities.identifier_set_invalid`.
      oneOf:
        - required:
            - identifier
        - required:
            - identifiers
      properties:
        externalId:
          type: string
          minLength: 1
          maxLength: 64
          description: Your stable customer reference, echoed on responses and webhooks
          example: customer_8412
        companyName:
          type: string
          minLength: 2
          maxLength: 64
          description: Legal company name matched against the business registry
          example: Bright Health Ltd
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2 country code
          example: GB
        address:
          type: object
          required:
            - line1
            - city
            - zip
          properties:
            line1:
              type: string
              minLength: 2
              maxLength: 192
              example: 10 King Street
            city:
              type: string
              minLength: 2
              maxLength: 64
              example: London
            zip:
              type: string
              minLength: 2
              maxLength: 32
              example: EC2V 8EA
        identifier:
          type: object
          required:
            - scheme
            - value
          properties:
            scheme:
              type: string
              minLength: 1
              maxLength: 16
              description: Peppol identifier scheme
              example: GB:VAT
            value:
              type: string
              minLength: 1
              maxLength: 64
              description: Identifier value
              example: gb123456789
        identifiers:
          type: array
          minItems: 1
          maxItems: 3
          description: "An ORDERED list of the entity's Peppol identifiers, as an
            alternative to `identifier`. The order is part of the identity: the
            first entry is the primary identifier, and the list is registered on
            the network in that order. The seller's address and legal
            registration number on the documents the entity sends are the first
            NON-TAX identifier registered; a VAT number placed first is skipped.
            It cannot be changed once created — archive the entity and create it
            again. A French entity lists its SIREN (`0002`, nine digits) first,
            then its annuaire address (`0225`); the reverse order is refused
            with `400 legal_entities.france_identifier_order`, because French
            rule BR-FR-10 could never hold on such an entity. In the sandbox the
            annuaire address is `SIREN_XXX` (a label of up to 50 letters and
            digits after the SIREN): the French annuaire is set up with every
            company's bare `0225:SIREN` address, which an access point takes
            over rather than creates, and on the Peppol test network each of
            those is already held. Available in sandbox only for now: a
            production key sending more than one identifier receives `422
            legal_entities.multiple_identifiers_production_unavailable`, as the
            authorisation your customer confirms in production covers a single
            identifier."
          items:
            type: object
            required:
              - scheme
              - value
            properties:
              scheme:
                type: string
                minLength: 1
                maxLength: 16
                example: "0002"
              value:
                type: string
                minLength: 1
                maxLength: 64
                example: "000136747"
          example:
            - scheme: "0002"
              value: "000136747"
            - scheme: "0225"
              value: 000136747_SHOP
    LegalEntity:
      type: object
      required:
        - id
        - externalId
        - companyName
        - country
        - identifier
        - status
        - networkDiscovery
        - environment
        - createdAt
        - identifiers
      properties:
        id:
          type: string
          format: uuid
          example: 7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b
        externalId:
          type:
            - string
            - "null"
          description: Your stable customer reference
          example: customer_8412
        companyName:
          type:
            - string
            - "null"
          example: Bright Health Ltd
        country:
          type:
            - string
            - "null"
          example: GB
        identifier:
          type:
            - object
            - "null"
          properties:
            scheme:
              type: string
              example: GB:VAT
            value:
              type: string
              example: gb123456789
        status:
          $ref: "#/components/schemas/PeppolLifecycleStatus"
        verificationDetail:
          type: object
          properties:
            reason:
              type: string
              enum:
                - name_mismatch
                - not_found
            checkedAt:
              type: string
              format: date-time
            registryStatus:
              type: string
              enum:
                - inactive
              description: "Present only when the registry knows the company but does not
                consider it active — struck off, in liquidation, or not yet
                active. Re-sending the same details will not change the answer
                until the registry changes it. When this field is ABSENT, we
                simply have no such finding to show for the current state —
                which is not the same as no finding existing. Any of these
                produce it: no registry entry for the identifier, a registry
                unreachable at check time, a finding our team has since reviewed
                and superseded, evidence aged out of retention, or simply no
                inactive finding on record — verification can succeed on the VAT
                registration alone, so the national registry is not always
                consulted, and where it is, it is not always the source that
                decides. Absence is therefore not evidence about the company —
                treat this field as a POSITIVE signal only."
              example: inactive
            hint:
              type: string
              description: "What to do next, in the SANDBOX only, when an identity could not
                be verified. Inventing a company to test with is the natural
                first move and it does not work: registries are queried in
                sandbox exactly as in production, so a plausible-looking number
                is refused. This names the way through — scheme 9915, which the
                Peppol code list publishes as having no entity behind it, so no
                registry is queried — and links to the walkthrough. ABSENT in
                production, deliberately: 9915 stands for no one, so proposing
                it for a real invoice would mean invoicing a real customer under
                a fictitious identity. Absent too on any status other than
                verification_failed. The wording is not a contract — read it,
                show it, never parse it."
              example: To test without a real company, create the sub-tenant under scheme
                9915, which the Peppol code list publishes as having no entity
                behind it — no registry is queried. The identifier cannot be
                changed once set, so archive this sub-tenant and create a new
                one. See
                https://getpeppr.dev/docs/platform/sandbox-pilot/#test-identity
        registrationDetail:
          type: object
          required:
            - reason
          description: Present when status is registration_failed, provisioning_failed
            (production, when a cause was recorded) or awaiting_release. The
            reason is a stable, privacy-safe code; raw provider messages are
            never exposed.
          properties:
            heldBy:
              type: string
              description: "awaiting_release only, when known: the SMP host of the Peppol
                access point that still holds the address. Published only in DNS
                host-name form."
              example: peppol-smp.prod.maventa.com
            waitingUntil:
              type: string
              format: date-time
              description: awaiting_release only — when getpeppr stops waiting (30 days after
                the wait began).
            reason:
              type: string
              enum:
                - already_registered
                - invalid_format
                - provider_error
              description: already_registered means the participant is already attached at an
                Access Point; invalid_format means the provider rejected the
                participant format; provider_error is the safe fallback for any
                other or unreadable provider outcome.
        networkDiscovery:
          type: object
          required:
            - state
            - attempts
          description: Cross-Access-Point receive readiness discovered through the public
            Peppol SML and SMP path. This is independent from outbound sending.
            A Legal Entity is reported as active only after state is verified.
            URLs, DNS answers and SMP XML are never exposed.
          properties:
            state:
              type: string
              enum:
                - pending
                - verified
                - failed
            attempts:
              type: integer
              minimum: 0
              description: Consecutive public discovery attempts started since the latest
                success. Resets to 0 on success.
            checkedAt:
              type: string
              format: date-time
            nextAttemptAt:
              type: string
              format: date-time
            error:
              type: string
              enum:
                - invalid_participant
                - sml_record_not_found
                - dns_timeout
                - dns_error
                - naptr_not_found
                - naptr_invalid
                - unsafe_smp_url
                - network_timeout
                - network_error
                - http_redirect_rejected
                - service_group_not_found
                - service_metadata_not_found
                - smp_response_too_large
                - smp_xml_invalid
                - participant_mismatch
                - invoice_service_missing
                - service_metadata_mismatch
                - as4_endpoint_missing
                - as4_endpoint_inactive
                - as4_certificate_untrusted
                - as4_certificate_inactive
                - as4_certificate_revoked
                - as4_certificate_status_unavailable
        environment:
          type: string
          enum:
            - sandbox
            - production
          example: production
        createdAt:
          type: string
          format: date-time
          example: 2026-06-01T10:00:00.000Z
        identifiers:
          type: array
          minItems: 0
          maxItems: 3
          description: "Every Peppol identifier of the legal entity, in the order they are
            registered on the network — the primary first. `identifier`,
            `status`, `networkDiscovery`, `verificationDetail` and
            `registrationDetail` above describe the primary, `identifiers[0]`.
            Each entry carries the status of that identifier alone: on a French
            entity the annuaire address (`0225`) reads `provisioning` until the
            SIREN (`0002`) is registered. Each entry produces its own
            `legal_entity.registered` webhook event."
          items:
            type: object
            required:
              - scheme
              - value
              - status
              - networkDiscovery
            properties:
              scheme:
                type: string
                example: "0002"
              value:
                type: string
                example: "000136747"
              status:
                $ref: "#/components/schemas/PeppolLifecycleStatus"
              networkDiscovery:
                type: object
                description: Same shape and meaning as the top-level `networkDiscovery`, for
                  this identifier.
              verificationDetail:
                type: object
                description: Same shape and meaning as the top-level `verificationDetail`, for
                  this identifier.
              registrationDetail:
                type: object
                description: Same shape and meaning as the top-level `registrationDetail`, for
                  this identifier.
        vatRegistration:
          type: object
          required:
            - status
            - checkedAt
            - source
            - vatNumberAdded
          description: "Norwegian customers only (primary identifier `0192`); ABSENT for
            every other country. `status: active` means receive-ready. Norwegian
            invoices need the seller's MVA number, and our Peppol provider
            refuses every Norwegian invoice without it — so for a Norwegian
            customer, `vatNumberAdded` is what says whether invoices can also be
            sent in its name. getpeppr adds the MVA number itself, and only
            after the Brønnøysund register confirms the company is in the VAT
            Register (Merverdiavgiftsregisteret). It never appears in
            `identifiers`."
          properties:
            status:
              type: string
              enum:
                - registered
                - not_registered
                - unknown
              description: "What the Brønnøysund VAT Register said, at `checkedAt`.
                `not_registered`: the company is not in the VAT Register — it
                can receive, but invoices cannot be sent in its name. `unknown`:
                no usable answer yet — never read it as not registered.
                `registered` with `vatNumberAdded: false` (for example, the
                company joined the register after it was onboarded): contact
                support."
            checkedAt:
              type:
                - string
                - "null"
              format: date-time
            source:
              type: string
              description: Where `status` comes from — `brreg`, the Brønnøysund Register
                Centre.
              example: brreg
            vatNumberAdded:
              type: boolean
              description: "True when getpeppr holds the customer's MVA number and invoices
                can be sent in its name. False: receiving only."
    LegalEntityPagination:
      type: object
      required:
        - total_count
        - offset
        - limit
        - has_more
      properties:
        total_count:
          type: integer
          example: 42
        offset:
          type: integer
          example: 0
        limit:
          type: integer
          example: 50
        has_more:
          type: boolean
          example: true
    LegalEntityArchiveResult:
      type: object
      required:
        - id
        - externalId
        - status
      properties:
        id:
          type: string
          format: uuid
          example: 7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b
        externalId:
          type:
            - string
            - "null"
          example: customer_8412
        status:
          type: string
          enum:
            - archived
          example: archived
    LegalEntityAttestationRequest:
      type: object
      required:
        - contactEmail
      properties:
        contactEmail:
          type: string
          format: email
          maxLength: 254
          description: Customer contact who can authorise production sending
          example: owner@brighthealth.co.uk
        contactName:
          type: string
          maxLength: 128
          description: Optional display name for the authorisation email
          example: Dr Jane Okafor
        language:
          type: string
          description: |
            Language of the authorisation email, the confirmation page and the
            wording the contact confirms, as a BCP 47 tag. Matched without regard
            to case (`nl-be` is `nl-BE`) and never approximated: `nl` or `en-GB`
            is refused, not mapped to a neighbour. Omit it to send the request in
            English.

            A translation is served on production only once a native reader has
            approved its wording, and until then a request for it is refused with
            `400` (`attestation.language_unsupported`), whose `supportedLanguages`
            lists what you can send instead. The languages and whether production
            uses each one today are listed in the Supported languages table:
            https://getpeppr.dev/docs/platform/legal-entities/#attestation-languages.
            Other languages are assessed and added on request
            (hello@getpeppr.dev), with the same native review before production
            uses them.

            A resend may use a different language; the new link then shows the
            wording in that language.
          example: en
    LegalEntityAttestationResult:
      type: object
      required:
        - id
        - externalId
        - status
        - expiresAt
        - language
        - languageSource
      properties:
        id:
          type: string
          format: uuid
          example: 7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b
        externalId:
          type:
            - string
            - "null"
          example: customer_8412
        status:
          type: string
          enum:
            - awaiting_authz
          description: The link was emailed and awaits the customer's confirmation.
          example: awaiting_authz
        expiresAt:
          type: string
          format: date-time
          example: 2026-06-08T10:00:00.000Z
        language:
          type: string
          description: The language the request was sent in, as a canonical BCP 47 tag.
          example: en
        languageSource:
          type: string
          enum:
            - explicit
            - default
          description: |
            `explicit` when your request named the language, `default` when it
            named none and the request went out in English.
          example: default
    LegalEntityAttestationRearmResult:
      type: object
      description: "Result code attestation.registration_rearmed: the entity was
        already signed and its production registration had failed. getpeppr
        tries again for up to 30 days (a definitive provider refusal ends it
        earlier); no email is sent, so there is no expiresAt, language or
        languageSource."
      required:
        - id
        - externalId
        - status
      properties:
        id:
          type: string
          format: uuid
          example: 7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b
        externalId:
          type:
            - string
            - "null"
          example: customer_8412
        status:
          type: string
          enum:
            - provisioning
          example: provisioning
    HealthStatus:
      type: object
      required:
        - status
        - checks
      properties:
        status:
          type: string
          enum:
            - healthy
            - unhealthy
          description: Overall health status (readiness — unhealthy as soon as any check
            fails)
        checks:
          type: object
          required:
            - db
            - redis
          properties:
            db:
              type: boolean
              description: Database connectivity
            redis:
              type: boolean
              description: Redis connectivity
    WebhookEventPayload:
      type: object
      required:
        - id
        - type
        - data
        - createdAt
      properties:
        id:
          type: string
          description: "Unique event ID (format: evt_{random16})"
          example: evt_abc123def456
        type:
          type: string
          enum:
            - invoice.sent
            - invoice.accepted
            - invoice.refused
            - invoice.error
            - invoice.registered
            - invoice.received
            - invoice.paid
            - invoice.undeliverable
            - invoice.delivery_unconfirmed
            - invoice.partially_paid
            - invoice.under_query
            - invoice.conditionally_accepted
            - invoice.status_changed
            - legal_entity.registered
            - legal_entity.unsupported_scheme
            - legal_entity.verification_failed
            - legal_entity.awaiting_authz
            - legal_entity.awaiting_release
            - legal_entity.registration_failed
            - peppol_identifier.verified
            - peppol_identifier.verification_failed
            - test.ping
            - inbound.invoice.received
            - inbound.creditnote.received
            - inbound.document.undeliverable
          description: Event type. The inbound.* values represent documents received from
            the Peppol network addressed to one of your Legal Entities, distinct
            from the outbound invoice.received status (acknowledgement of a
            document you sent).
          example: invoice.sent
        environment:
          type: string
          enum:
            - sandbox
            - production
          description: "Business environment of the resource this event is about
            (GPR-1329). Read it after verifying the signature, then call the API
            with the matching key (sk_sandbox_* / sk_live_*); a key from the
            other environment gets a 404 on the event's resources. OPTIONAL by
            contract, for three reasons: events emitted before this field
            existed are replayed verbatim from the delivery outbox; test.ping (a
            synthetic dashboard event tied to no resource) deliberately carries
            none; and a status event whose submission environment is not yet
            resolved transiently carries none either — it appears on the next
            event once resolution lands. Absence never means \"production by
            default\". When present the value is exact — never guessed or
            defaulted. Some event families also carry the same value as
            data.environment, kept for compatibility; this root field is the
            documented one."
          example: sandbox
        data:
          description: "Event-specific data. The shape depends on the event family:
            invoice.* status events carry WebhookStatusEventData
            (invoice.status_changed adds the per-axis state), inbound.*.received
            carry WebhookInboundDocumentData, inbound.document.undeliverable
            carries WebhookInboundUndeliverableData, peppol_identifier.* carry
            WebhookPeppolIdentifierData, legal_entity.* carry
            WebhookLegalEntityData and test.ping carries WebhookTestPingData."
          anyOf:
            - $ref: "#/components/schemas/WebhookStatusEventData"
            - $ref: "#/components/schemas/WebhookStatusChangedData"
            - $ref: "#/components/schemas/WebhookInboundDocumentData"
            - $ref: "#/components/schemas/WebhookInboundUndeliverableData"
            - $ref: "#/components/schemas/WebhookPeppolIdentifierData"
            - $ref: "#/components/schemas/WebhookLegalEntityData"
            - $ref: "#/components/schemas/WebhookTestPingData"
        createdAt:
          type: string
          format: date-time
          description: Event timestamp (ISO 8601)
          example: 2026-02-26T10:30:00.000Z
    WebhookStatusEventData:
      title: Status event data (invoice.*)
      type: object
      description: Payload of the outbound document status events (invoice.sent,
        invoice.accepted, invoice.paid, invoice.undeliverable,
        invoice.delivery_unconfirmed, …).
      required:
        - invoiceId
        - invoiceNumber
        - providerDocumentId
        - status
        - environment
      properties:
        invoiceId:
          type:
            - string
            - "null"
          format: uuid
          deprecated: true
          description: getpeppr submission ID (null if the submission row is not yet
            linked). **Deprecated:** the name says "invoice id" while `POST
            /invoices` returns the provider GUID under `id`, so a consumer that
            fed one to the other's endpoint used to get a 404. Read
            `submissionId` — same value, unambiguous. This field is unchanged
            and is not going away without a decided removal date.
        submissionId:
          type:
            - string
            - "null"
          format: uuid
          description: >-
            getpeppr submission ID — the same value as `invoiceId`. Usable
            directly against GET /invoices/{id} and the ?invoiceId= filter on
            /events, as is `providerDocumentId` below.

            ⚠️ **Not `required`, deliberately.** Every live emitter sends it,
            but a delivery that failed before it existed is retried from a
            payload stored at the time it was queued — so our own outbox can
            still replay events without the field. Marking it required would
            make a subscriber that validates this schema reject exactly those
            retries. Read `invoiceId` when it is absent; the two are the same
            value.
        invoiceNumber:
          type:
            - string
            - "null"
        providerDocumentId:
          type: string
          description: Provider (Storecove) document GUID
        status:
          $ref: "#/components/schemas/DocumentStatus"
        environment:
          type:
            - string
            - "null"
          description: sandbox or production (null if unresolved). Same value as the root
            environment when both are present; the root field (GPR-1329) is the
            documented one and is simply absent when unresolved.
        submittedAt:
          type: string
          format: date-time
          description: "When the document was submitted. Sent only on
            `invoice.delivery_unconfirmed`, where the age of the send is the
            point of the notification. Optional: no other event carries it."
        observedAt:
          type: string
          format: date-time
          description: When we established that no delivery evidence had arrived yet. Sent
            only on `invoice.delivery_unconfirmed`. Optional.
    WebhookStatusChangedData:
      title: Generic status notification data (invoice.status_changed)
      allOf:
        - $ref: "#/components/schemas/WebhookStatusEventData"
        - type: object
          required:
            - axes
          properties:
            axes:
              type: object
              description: Per-axis state of the document (multi-jurisdiction status model).
                Each axis is null until a signal has been received for it.
              required:
                - platformFiscal
                - delivery
                - businessDisposition
                - settlement
              properties:
                platformFiscal:
                  type:
                    - string
                    - "null"
                delivery:
                  type:
                    - string
                    - "null"
                businessDisposition:
                  type:
                    - string
                    - "null"
                settlement:
                  type:
                    - string
                    - "null"
            detail:
              $ref: "#/components/schemas/StatusDetail"
    WebhookInboundDocumentData:
      title: Inbound document data (inbound.*)
      type: object
      description: Payload of inbound.invoice.received / inbound.creditnote.received.
      required:
        - receivedDocumentId
        - legalEntityId
        - documentType
        - sender
        - receivedAt
        - providerDocumentId
        - document
      properties:
        receivedDocumentId:
          type: string
          format: uuid
        legalEntityId:
          type: string
        externalSubTenantId:
          type:
            - string
            - "null"
          description: Your external sub-tenant reference (platform accounts only)
        documentType:
          type: string
          enum:
            - invoice
            - creditnote
        sender:
          type: object
          description: "The supplier party the document names for itself:
            `cac:AccountingSupplierParty/cac:Party` — its `cbc:EndpointID` and
            that element's `schemeID`. Read from the document and nowhere else,
            so both fields name the same company and neither is ever a guess.
            Both are null together when the document does not state a supplier
            endpoint unambiguously — a conformant Peppol document always does,
            so in practice this means a document that did not come through the
            network, or one we could not read (see
            `inbound.document.undeliverable`)."
          properties:
            peppolId:
              type:
                - string
                - "null"
              description: The supplier participant identifier, `scheme:value`, as the
                document states it. The scheme is its numeric EAS code when it
                is in the CEF EAS code list, and the scheme as declared
                otherwise. `to.peppolId` accepts both that code and the scheme's
                symbolic alias, so compare the two canonically rather than as
                strings.
              example: 0208:0999999999
            name:
              type:
                - string
                - "null"
              description: The supplier's registered name (BT-27), or its trade name (BT-28)
                when the document states no registered one. Null when the
                document names neither, even if `peppolId` is present.
        invoiceNumber:
          type:
            - string
            - "null"
        receivedAt:
          type: string
          format: date-time
        providerDocumentId:
          type: string
        document:
          type: object
          description: The received UBL document, base64-embedded when within the size cap.
          properties:
            format:
              type: string
              enum:
                - ubl
            encoding:
              type: string
              enum:
                - base64
            content:
              type:
                - string
                - "null"
              description: Base64 UBL content (null when omitted for size)
            contentOmittedReason:
              type:
                - string
                - "null"
              enum:
                - size
                - null
            sizeBytes:
              type: integer
    WebhookInboundUndeliverableData:
      title: Undeliverable inbound document data (inbound.document.undeliverable)
      type: object
      description: "A document addressed to your account was received from the Peppol
        network but could not be delivered to you. There is no received
        document: nothing to fetch from /received-documents. Ask the sender to
        send the document again; contact support with providerDocumentId for
        help."
      required:
        - undeliverableDocumentId
        - providerDocumentId
        - reason
        - environment
        - legalEntityId
        - externalSubTenantId
        - documentType
        - sender
        - invoiceNumber
        - receivedAt
        - sizeBytes
        - storageLimitBytes
      properties:
        undeliverableDocumentId:
          type: string
          format: uuid
          description: Stable idempotency key for this notice — deduplicate on it
        providerDocumentId:
          type: string
          description: Reference of the document at the access point — quote it to support
        reason:
          type: string
          enum:
            - too_large
            - legal_entity_unresolved
          description: "too_large — larger than we can receive: over storageLimitBytes, or
            carried by an access-point response too large to read before its
            size was measured. legal_entity_unresolved — could not be matched to
            an active Legal Entity of your account: the access point named none,
            or named one that is not active."
        environment:
          type: string
          enum:
            - sandbox
            - production
          description: Same value as the root environment; the root field (GPR-1329) is
            the documented one.
        legalEntityId:
          type:
            - string
            - "null"
          description: Set only when the document provably belongs to one of your active
            Legal Entities
        externalSubTenantId:
          type:
            - string
            - "null"
          description: Your external sub-tenant reference (platform accounts only)
        documentType:
          type:
            - string
            - "null"
          enum:
            - invoice
            - creditnote
            - unknown
            - null
          description: Null when the document could not be read
        sender:
          type: object
          required:
            - peppolId
            - name
          description: "Whom the document names as its supplier, read from the document
            exactly as on `inbound.invoice.received`. Both fields are ALWAYS
            null when `reason` is `too_large`: that document was never parsed —
            refusing to store it and reading it anyway would spend the very
            resource the size cap exists to bound — and we do not name a sender
            we did not read. Use `providerDocumentId` to trace such a document."
          properties:
            peppolId:
              type:
                - string
                - "null"
            name:
              type:
                - string
                - "null"
        invoiceNumber:
          type:
            - string
            - "null"
        receivedAt:
          type: string
          format: date-time
        sizeBytes:
          type:
            - integer
            - "null"
          description: Exact decoded size when it was measured, otherwise null
        storageLimitBytes:
          type: integer
          description: Largest received document getpeppr stores (10485760)
    WebhookPeppolIdentifierData:
      title: Peppol identifier verification data (peppol_identifier.*)
      type: object
      required:
        - identifierId
        - scheme
        - identifier
        - verifiedAt
      properties:
        identifierId:
          type: string
          format: uuid
        scheme:
          type: string
          example: "0208"
        identifier:
          type: string
          description: The identifier value (may be partially masked on collision events)
        companyName:
          type:
            - string
            - "null"
        verifiedAt:
          type: string
          format: date-time
        similarity:
          type:
            - number
            - "null"
          description: Registry name similarity score when applicable
        reason:
          type:
            - string
            - "null"
          description: Failure or collision reason when applicable
    WebhookLegalEntityData:
      title: Platform sub-tenant lifecycle data (legal_entity.*)
      type: object
      required:
        - subTenantId
        - legalEntityId
        - peppolId
        - status
        - environment
        - occurredAt
      properties:
        subTenantId:
          type:
            - string
            - "null"
          description: Your external sub-tenant reference
        legalEntityId:
          type: string
        peppolId:
          type:
            - string
            - "null"
          description: scheme:value form, when registered
        status:
          type: string
          description: Sub-tenant lifecycle status (same vocabulary as LegalEntity.status)
        environment:
          type: string
          description: Same value as the root environment; the root field (GPR-1329) is
            the documented one.
        reason:
          type: string
          description: Present on failure events, and on legal_entity.awaiting_release
            (already_registered)
        heldBy:
          type: string
          description: legal_entity.awaiting_release only, when known — the SMP host of
            the access point that still holds the address
        occurredAt:
          type: string
          format: date-time
    WebhookTestPingData:
      title: Test ping data (test.ping)
      type: object
      required:
        - message
      properties:
        message:
          type: string
          example: This is a test webhook from getpeppr.
    Transport:
      type: object
      properties:
        id:
          type: string
          description: Unique transport ID
        transportTypeCode:
          type: string
          description: Transport type code
        name:
          type: string
          description: Human-readable name
        status:
          type: string
          description: Transport status (e.g., "active", "inactive")
    TransportType:
      type: object
      required:
        - code
        - name
      properties:
        code:
          type: string
          description: Transport type code
          example: peppol
        name:
          type: string
          description: Human-readable name
          example: Peppol BIS 3.0
    ReceivedDocument:
      type: object
      description: |
        A Peppol document received on behalf of one of your Legal Entities.

        Every field except the identifiers is derived from the document the
        sender transmitted, so treat the text as third-party input.
      required:
        - id
        - invoiceNumber
        - documentType
        - dispatchStatus
        - environment
        - senderName
        - senderPeppolId
        - receivedAt
        - ublSizeBytes
        - ublAvailable
        - ublExpiresAt
        - legalEntityId
        - legalEntityName
        - currency
        - totalAmountMinor
        - issueDate
      properties:
        id:
          type: string
          format: uuid
        invoiceNumber:
          type:
            - string
            - "null"
          description: Document number as written by the sender. Not unique across senders.
          example: SUP-99
        documentType:
          type: string
          enum:
            - invoice
            - creditnote
            - unknown
          description: "`unknown` means the document reached you but its type is outside
            what getpeppr currently classifies — the original UBL is still
            retrievable."
        dispatchStatus:
          type: string
          enum:
            - pending
            - dispatched
            - no_subscriber
            - skipped_unknown_type
          description: State of the webhook that notifies you of this document. It
            describes OUR delivery to you, never the document's standing on the
            network.
        environment:
          type: string
          enum:
            - sandbox
            - production
          description: Same value as the root environment; the root field (GPR-1329) is
            the documented one.
        senderName:
          type:
            - string
            - "null"
          description: The supplier's registered name (BT-27) as the document states it,
            or its trade name (BT-28) when it states no registered one. Null
            when the document names neither, and null together with
            `senderPeppolId` when the document states no supplier endpoint.
        senderPeppolId:
          type:
            - string
            - "null"
          description: The supplier's Peppol participant identifier, `scheme:value`, read
            from the document's own
            `cac:AccountingSupplierParty/cac:Party/cbc:EndpointID` and that
            element's `schemeID` — from the document and nowhere else, so it is
            never inferred from what the provider knows about the sender. The
            scheme is its numeric EAS code when it is in the CEF EAS code list,
            and the scheme as declared otherwise; `to.peppolId` accepts both
            that code and the scheme's symbolic alias, so compare the two
            canonically rather than as strings. Null when the document states no
            supplier endpoint unambiguously, which a conformant Peppol document
            always does.
          example: 0208:0999999999
        receivedAt:
          type: string
          format: date-time
          description: When the document reached getpeppr (UTC).
        ublSizeBytes:
          type:
            - integer
            - "null"
          description: Size of the original UBL. Null on list responses.
        ublAvailable:
          type: boolean
          description: Whether `GET /received-documents/{id}/as/xml` still returns the
            original. False once it has been removed under the retention rule
            (that call then answers `410 Gone`); every other field stays.
        ublExpiresAt:
          type: string
          format: date-time
          description: "When the original is due for removal: 90 days after `receivedAt`,
            or the end of your Platform contract's exit window if that comes
            first. Removal runs daily, so it happens within a day after this
            time."
        legalEntityId:
          type: string
          format: uuid
          description: The Legal Entity this document was addressed to.
        legalEntityName:
          type:
            - string
            - "null"
        currency:
          type: string
          description: ISO 4217 code taken from the document; defaults to EUR when absent.
          example: EUR
        totalAmountMinor:
          type:
            - integer
            - "null"
          description: Total including VAT, in minor units of `currency`. **Null when the
            amount could not be derived** from the UBL — never a silent zero,
            which would be indistinguishable from a genuine zero-rated total.
        issueDate:
          type: string
          description: Issue date as written by the sender; empty string when absent.
          example: 2026-06-18
    EventEntry:
      type: object
      required:
        - id
        - eventType
        - createdAt
      properties:
        id:
          type: string
          format: uuid
          description: Unique event ID
          example: 3f9e1a7c-4b2d-4e8f-9c1a-7b6d5e4f3a2b
        eventType:
          type: string
          description: Event type (e.g., "invoice.sent", "invoice.accepted",
            "inbound.invoice.received")
          example: invoice.sent
        documentId:
          type:
            - string
            - "null"
          description: Associated invoice/document ID (null for non-document events)
          example: "12345"
        metadata:
          type:
            - object
            - "null"
          description: Additional event metadata (varies by event type, may be null)
        createdAt:
          type: string
          format: date-time
          description: Event timestamp
          example: 2026-02-26T10:30:00Z
  responses:
    Unauthorized:
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            missing:
              summary: Missing Authorization header
              value:
                error: Missing or invalid Authorization header
            invalid:
              summary: Invalid API key
              value:
                error: Invalid API key
    ProviderUnavailable:
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
      description: |
        The Peppol provider refused, was unreachable, or answered something
        getpeppr does not recognise.

        This is a failure between getpeppr and the provider, never a problem
        with your API key: the provider's own status is deliberately NOT passed
        through, because an unrecognised upstream status is the one most likely
        to carry detail that was never inspected. The statuses that describe
        YOUR request — 404, 409, 422, 429 — are preserved and documented on the
        individual operations.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Provider service temporarily unavailable. Please retry.
    RateLimited:
      description: Rate limit exceeded
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
        Retry-After:
          description: Seconds to wait before retrying. Present when getpeppr applied the
            limit; ABSENT when the Peppol provider did, so treat it as a
            refinement of your own backoff rather than a guarantee.
          schema:
            type: integer
          example: 42
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Rate limit exceeded. Try again later.
    ProviderRetryLater:
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
      description: >
        The Peppol provider could not be reached just now

        (`Getpeppr-Result-Code: provider.retry_later`, `Getpeppr-Retryable:
        true`).

        Wait, then send the same request again — on the six idempotent
        endpoints,

        with the same `Idempotency-Key`. The one non-retryable `503` is

        `provider.not_configured`: the provider is not set up for this

        environment, and only support can fix it.


        Declared on the four operations that pass a provider failure through

        with its own status: `POST /invoices`, `POST /invoices/import`,

        `GET /invoices/{id}/as/{format}` and `GET
        /directory/{scheme}/{participantId}`.

        The other provider-backed operations never answer `503`. They pass the

        provider's verdict on your request through — `404`, `409`, `422`, `429`
        —

        and fold every upstream *failure* onto `502`; some answer `501` before

        calling out at all, and one (`GET /invoices/{id}` with
        `include=evidence`)

        answers `200` without the evidence rather than failing the read. Unlike
        `502`, a `503` is always a transient or configuration

        fault on our side, never a verdict on your document.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    PayloadTooLarge:
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
      description: >
        Request body exceeds the 1 MB limit.


        The headers and JSON body below are what **getpeppr** answers. Above

        roughly 4.5 MB the hosting platform refuses the request before our code

        runs, and answers its own `413`: `text/plain`, with none of the
        `Getpeppr-*`

        headers — no getpeppr request id and no result code. It carries the

        platform's own `x-vercel-id`, which is the id to quote. See "Responses
        getpeppr did not write" on

        https://getpeppr.dev/docs/api-results/#not-written-by-getpeppr — a
        client that parses this

        status as JSON must handle that case.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: "Payload too large. Maximum size: 1024KB"
    DirectoryUpstreamUnavailable:
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
      description: "The public Peppol Directory could not be reached — an upstream
        timeout, a network failure, or the registry declining the request.
        Transient: retry shortly. `Getpeppr-Retryable` is `true`, unlike the 500
        next to it, which reports a fault on our side that retrying will not
        clear."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: The Peppol Directory could not be reached just now. Retry shortly.
    InternalError:
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Internal server error
    IdempotencyKeyBlank:
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
      description: >
        The `Idempotency-Key` header was sent but carries nothing usable — it is
        empty, or a transport reduced it to empty by stripping the whitespace at
        its edges. It is refused rather than ignored: a key that cannot protect
        anything is worse than no key, because you would believe you were
        covered. Send a non-blank key, or omit the header.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: idempotency_key_blank
            message: The Idempotency-Key header is present but blank, so it protects nothing
              — every retry would be executed as a new request. Send a non-blank
              key, or omit the header if you do not need idempotency.
    IdempotencyConcurrentRequest:
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
      description: >
        Another request carrying this `Idempotency-Key` is still running, or its
        result could not be read back. Either way this one was **not executed**:
        the key serialises requests rather than letting both write. Retry
        shortly.


        Once the first request finishes, an identical retry replays its response
        if that request succeeded. If it was refused, the retry is executed
        normally — so no second resource is created either way.


        Note the difference from the `422` below: that one means the key was
        already used for a *different* request, which needs a new key. This one
        means the *same* request is still in progress, and retrying is correct.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Duplicate request in progress. Please retry.
    IdempotencyKeyReuse:
      headers:
        Getpeppr-Request-Id:
          $ref: "#/components/headers/GetpepprRequestId"
        Getpeppr-Result-Code:
          $ref: "#/components/headers/GetpepprResultCode"
        Getpeppr-Result-Message:
          $ref: "#/components/headers/GetpepprResultMessage"
        Getpeppr-Retryable:
          $ref: "#/components/headers/GetpepprRetryable"
        Getpeppr-Remediation:
          $ref: "#/components/headers/GetpepprRemediation"
        Getpeppr-Result-Docs:
          $ref: "#/components/headers/GetpepprResultDocs"
      description: >
        The `Idempotency-Key` was already used for a different request — another
        endpoint or a different request body. A key replays the original
        response only for an identical retry; use a new key for each distinct
        request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: idempotency_key_reuse
            message: This Idempotency-Key was already used for another request (a different
              endpoint). A key replays the original response only for an
              identical retry — use a new key for each distinct request.
