openapi: 3.1.0
info:
  title: getpeppr API - Invoices API
  version: 1.0.0
  description: Create, send, import, update, acknowledge, and download invoice
    documents, and read the documents you receive.
  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: Invoices
    description: Create, send, list, and manage Peppol e-invoices.
  - name: Received Documents
    description: >
      Read the Peppol documents received on behalf of your Legal Entities —
      list,

      detail, and the original UBL exactly as it arrived from the network.


      Reception is also pushed to you in real time as `inbound.*` webhook
      events.

      These endpoints are the pull counterpart: they let you re-read what you

      received, including documents whose webhook delivery you did not persist.


      **Retention.** getpeppr keeps the original UBL for 90 days after receipt

      (earlier if a Platform contract's exit window closes first), then removes

      it. The document's details stay listed; `ublAvailable` and `ublExpiresAt`

      say whether the original can still be downloaded and until when. getpeppr

      is not an archive: persist the original on your side, from the webhook or

      from `/as/xml`, before it expires.
security:
  - bearerAuth: []
paths:
  /invoices:
    post:
      operationId: createInvoice
      summary: Create or send an invoice
      description: >
        Creates and optionally sends a Peppol e-invoice. The invoice data is
        translated

        to UBL XML and forwarded to the Peppol network.


        Platform accounts can send on behalf of a sub-tenant by including
        `sender`

        with exactly one of `legalEntityId` or `externalSubTenantId`. The
        sender's

        supplier identity is derived from the verified legal entity; on the

        send-as path a payload `from` is stripped down to the seller contact

        (`contactName`, `phone`, `email` — see `SellerContact`), which domestic

        German invoices require.


        `sender` requires a master key. A standard key that sends the field is

        refused with `403`, before anything is dispatched — exactly as on

        `POST /invoices/import`.


        Storecove does not support drafts. Sending `_draft: true` returns `422`

        with `drafts_not_supported`; omit the flag to submit immediately.


        Supports idempotency via the `Idempotency-Key` header.
      tags:
        - Invoices
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: x-validate-recipient
          in: header
          description: >
            Validate recipient exists on the Peppol network before sending.

            Omit this header to skip validation (default).

            - `warn`: proceeds with sending even if recipient not found (logs
            for monitoring)

            - `strict`: rejects with 422 if recipient not found
          schema:
            type: string
            enum:
              - warn
              - strict
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendInvoiceInput"
            example:
              number: INV-2026-001
              date: 2026-02-26
              dueDate: 2026-03-28
              currency: EUR
              sender:
                externalSubTenantId: customer_8412
              to:
                name: ACMEDIA
                peppolId: 0208:0685660237
                country: BE
              lines:
                - description: API Integration Setup
                  quantity: 1
                  unitPrice: 500
                  vatRate: 21
              buyerReference: PO-2026-042
      responses:
        "201":
          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: Invoice accepted for submission to the Peppol network
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SendResult"
        "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 or missing required fields — or a blank
            `Idempotency-Key` (`idempotency_key_blank`): the header was sent but
            carries nothing usable, so it is refused rather than ignored. On a
            French declaration five more codes land here:
            `invoices.france_cadre_invalid` when the Cadre de Facturation is
            outside the published list, `invoices.france_invoice_number_invalid`
            when the invoice `number` is not a string or breaks French rule
            BR-FR-01 (at most 35 characters, only letters, digits and `+ - _ /`,
            no spaces) or the French tax platform's current ceiling of 20
            characters, which lifts on 2026-12-01 — judged before any other
            check on the number, `invoices.france_credit_note_reference_invalid`
            when a credit note does not give both `invoiceReference` and
            `invoiceReferenceDate` (the credited invoice's issue date,
            YYYY-MM-DD) as French rule BR-FR-CO-05 requires,
            `invoices.france_buyer_siren_missing` when the buyer's SIREN can be
            read neither from `to.companyId` (a SIREN under `0002`, or a SIRET
            under `0009`) nor from a French address in `to.peppolId`
            (`0225:<SIREN>[_suffix]`, `0002:<SIREN>` or `0009:<SIRET>`) — the
            French tax platform requires it, and
            `invoices.france_legal_mentions_invalid` when your own `note` or a
            `france.legalMentions` entry would make one of the legal-mention
            markers appear twice. getpeppr writes `#PMT#`, `#PMD#` and `#AAB#`
            itself, so do not repeat them; `#TXD#` is yours alone and a single
            occurrence of it is perfectly legal. French invoicing rule BR-FR-06
            allows one occurrence of each."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                missingFields:
                  value:
                    error: "Missing required fields: number (string), to (object), lines (array)"
                blankIdempotencyKey:
                  value:
                    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.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          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: Either a master key missing the `legal_entities:send_as` scope, or
            a standard key that declared a `sender`. Sending on behalf of a
            sub-tenant requires a master key — the field is never silently
            ignored.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Your API key does not have permission to perform this action
        "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: Sub-tenant sender was not found or is not owned by this platform
            (`send_as.sub_tenant_not_found`, deliberately indistinguishable). A
            `sender.legalEntityId` that is not in the getpeppr id format also
            returns `404`, but with `request.resource_id_malformed`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: not_found
        "409":
          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 same document was already sent moments ago, or a concurrent
            request carrying the same `Idempotency-Key` is still in flight.
            Nothing was sent to the Peppol network in either case.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                duplicateDocument:
                  summary: Same document, different Idempotency-Key
                  value:
                    error: duplicate_document
                    message: Invoice INV-2026-001 was already sent 4s ago (document
                      e8ec77a9-0000-4000-8000-000000000000). Sending it again
                      would deliver a second copy to the recipient — Peppol does
                      not de-duplicate. If this is a deliberate resend, retry
                      after 15 minutes; to retry a request whose response you
                      lost, reuse the original Idempotency-Key instead.
                    duplicateOf: e8ec77a9-0000-4000-8000-000000000000
                concurrentRequest:
                  summary: Same Idempotency-Key still in flight
                  value:
                    error: Duplicate request in progress. Please retry.
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          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: >
            Unprocessable invoice. The `Getpeppr-Result-Code` header names which

            refusal it is; each code has its page under `/docs/api-results/`.


            Payload refused by getpeppr before the document is submitted:

            `invoices.buyer_address_required`,
            `invoices.recipient_peppol_id_required`,

            `invoices.invalid_base_quantity`,

            `invoices.invalid_line_quantity`,
            `invoices.invalid_allowance_charge_amount`,

            `invoices.attachment_url_not_supported` (an attachment given only as

            a `url`: a Peppol document carries the file itself, not a link, so

            send `content` and `mimeType` instead),

            `invoices.non_finite_derived_amount`,
            `invoices.amount_out_of_range`,

            `invoices.invoice_number_too_long`, `invoices.field_too_long`,

            `invoices.invalid_country_code`, `invoices.invalid_vat_category`,

            `invoices.unsupported_vat_category`,
            `invoices.unsupported_payment_means`,

            `invoices.payment_mandate_required`,
            `invoices.payment_account_required`

            (a credit transfer, 30 or 58, without a non-empty `paymentIban` —
            rule BR-61),

            `invoices.country_rule_violation`

            (a national rule of the recipient's country, see
            `/docs/error-handling/`),

            `invoices.sender_tax_country_missing`,
            `invoices.sub_tenant_vat_missing`,

            and the unsupported `_draft` mode (`invoices.drafts_not_supported`).


            Sender-tax preflight — getpeppr checks the sender's exact registered

            tax-identifier inventory and refuses before submitting anything, so

            these are NOT provider refusals even though they were grouped under

            `provider.rejected` before 2026-09:

            `invoices.sender_tax_identifier_missing`,

            `invoices.outside_scope_sender_has_tax_identifier`,

            `invoices.sender_tax_role_unclassified`

            (see `/docs/error-handling/sender-vat-missing`).


            Production send gates: `invoices.sender_identity_incomplete`,

            `identity.verification_required`,
            `invoices.platform_billing_not_active`,

            `invoices.production_access_expired`,
            `send_as.sub_tenant_not_sendable`,

            and in the sandbox `send_as.sub_tenant_test_identity_required` (the

            sub-tenant's identity cannot send there; register a test sub-tenant

            under scheme `9915`).


            French régime — only when the request declares `france`:

            `invoices.france_regime_not_eligible`, when the identity issuing the

            document is not ready for the regime. Three things are required of

            it, and any one missing produces this code with a message naming

            which: registration in the French directory (scheme 0225, with a

            settled registry check and published to the network), a SIREN

            under scheme 0002, and that SIREN registered BEFORE any other

            non-tax identifier on the legal entity. The SIREN is a content

            requirement, not an addressing one — French invoicing rule BR-FR-10

            puts the seller SIREN on the document itself, and the network

            operator writes the first non-tax identifier registered on the

            legal entity there. We read that order just before submitting; to

            fix it, remove the identifiers registered ahead of the SIREN and add

            them back. A malformed Cadre de Facturation is

            refused earlier, as a 400 — see the `france` property. Both are

            decided before anything is submitted: what comes back from the

            regulated flow is a tax-administration outcome, so there is no

            retraction after the fact.


            `invoices.recipient_not_in_directory` sits between the two: with

            `x-validate-recipient: strict` we look the recipient up in the
            Peppol

            Directory first — a real network call on a cache miss — and refuse

            before submitting anything.


            Provider refusals passed through as 422:
            `invoices.invalid_provider_tax_rate`

            (a VAT percentage the provider does not accept for the seller's tax

            country and invoice date) and `provider.rejected`.


            And `idempotency_key_reuse` (`idempotency.key_reused`): an

            `Idempotency-Key` already used for a different endpoint or body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                recipientNotFound:
                  summary: Strict recipient validation failed
                  value:
                    error: Recipient 0208:0685660237 not found in Peppol Directory. The participant
                      may not be registered on the Peppol network.
                senderNotReady:
                  summary: Sub-tenant send gate failed
                  value:
                    error: peppol_identity_not_verified
                    code: attestation_required
                    message: Sub-tenant attestation is required before sending in production.
                    docs: https://getpeppr.dev/docs/platform/sending-and-webhooks/
                identityIncomplete:
                  summary: Production identity gate failed
                  value:
                    error: peppol_identity_incomplete
                billingNotActive:
                  summary: Platform billing gate failed
                  value:
                    error: platform_billing_not_active
                    code: billing_state_invalid
                accessExpired:
                  summary: Commercial production access expired
                  value:
                    error: production_access_expired
                missingBuyerAddress:
                  summary: Semantic validation failed
                  value:
                    error: "Buyer address is required: street, city, and postalCode must be provided
                      in the 'to' object"
                missingRecipientPeppolId:
                  summary: Missing or malformed recipient Peppol ID
                  value:
                    error: 'Recipient Peppol ID is required: to.peppolId must be provided (format
                      "scheme:id", e.g. "0208:0123456789"). Documents are
                      delivered via the Peppol network only — the contact email
                      is not a delivery channel.'
                invalidBaseQuantity:
                  summary: Base quantity is not a finite number greater than zero
                  value:
                    error: invalid_base_quantity
                    message: baseQuantity must be a finite number greater than zero.
                    field: lines[0].baseQuantity
                    rule: PEPPOL-EN16931-R121
                draftsNotSupported:
                  summary: Draft mode is unavailable with the current provider
                  value:
                    error: Storecove does not support draft invoices. Documents are submitted
                      immediately to the Peppol network. Remove the _draft flag
                      to send the invoice directly.
                    code: drafts_not_supported
                invalidProviderTaxRate:
                  summary: Storecove rejected the VAT percentage for the seller tax country
                  value:
                    error: invalid_provider_tax_rate
                    message: The VAT rate is not valid for the seller's tax country on the invoice
                      date. Check vatRate or use the correct sender legal
                      entity.
                    field: vatRate
                    rule: provider_tax_rate_catalogue
                    docs: https://getpeppr.dev/docs/error-handling/#provider-tax-rate
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
        "503":
          $ref: "#/components/responses/ProviderRetryLater"
    get:
      operationId: listInvoices
      summary: List invoices
      description: >
        Returns a paginated list of invoices owned by your account. The optional

        `number` parameter performs an exact, case-sensitive match and may
        return

        multiple rows because invoice numbers are not globally unique.


        Results are also scoped to the **type** of your API key: a standard key
        returns

        only the invoices of your own legal entity, while a master key also
        returns those

        sent on behalf of the sub-tenants it owns.
      tags:
        - Invoices
      parameters:
        - name: number
          in: query
          description: Exact, case-sensitive invoice number filter
          schema:
            type: string
            minLength: 1
        - $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 invoices
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoices:
                    type: array
                    items:
                      $ref: "#/components/schemas/InvoiceSummary"
                  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: The number filter is empty or repeated
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /invoices/{id}:
    get:
      operationId: getInvoice
      summary: Get invoice status
      description: >
        Returns the current status and details of an invoice. The invoice must
        be

        owned by your account.


        **Both identifiers reach the same document.** You can pass the provider

        document GUID (returned in the `id` field of `POST /invoices`) or the

        getpeppr submission ID (the `id` field of `GET /invoices`, and the

        `invoiceId` field of webhook payloads). An id you do not own answers

        `404 Not Found` either way — as does a document whose registration did

        not complete, whichever identifier you use for it.


        In the rare case where one id would name two different documents of

        yours — one by its provider GUID, another by its submission ID — the

        request answers `404 Not Found` rather than guessing which you meant,

        and we are alerted. Use the other identifier for that document.


        A standard key can only retrieve documents belonging to your own legal
        entity;

        an invoice sent on behalf of a sub-tenant answers `404 Not Found`,

        indistinguishable from an unknown id. Use a master key to retrieve it.
      tags:
        - Invoices
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
        - name: include
          in: query
          required: false
          description: "Comma-separated list of optional enrichments. `evidence` adds
            `peppolMessageId` by reading the sending evidence from the Peppol
            network, and — for an invoice sent under the French regime only —
            `clearing`, the proof that its data was reported to the DGFiP. It is
            opt-in because it costs a provider round trip, and it degrades
            silently: if the document has not gone out yet, or the read fails,
            the field is simply absent and the rest of the response is
            unaffected. Unknown values are ignored."
          schema:
            type: string
            example: evidence
      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: Invoice status and details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvoiceStatus"
        "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: Invoice not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Invoice not found
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
    put:
      operationId: updateInvoice
      summary: Update an invoice (unsupported by Storecove)
      description: |
        Storecove documents are immutable after submission, so the current
        provider always returns `501 Not Implemented`. Issue a credit note
        instead of attempting to update an existing document.

        Authentication, rate limiting and ownership checks still run before the
        provider refusal; an invoice not owned by the account therefore returns
        `404`, not `501`.
      tags:
        - Invoices
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendInvoiceInput"
      responses:
        "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"
        "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: Invoice not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Invoice not found
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "501":
          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: Invoice updates are not supported by the current provider
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "updateInvoice is not supported by the Storecove provider (capability:
                  updateInvoice). Storecove does not support invoice updates.
                  Documents are immutable after submission. Issue a credit note
                  instead."
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
    delete:
      operationId: deleteInvoice
      summary: Delete an invoice (unsupported by Storecove)
      description: |
        The current Storecove provider does not support invoice deletion and
        always returns `501 Not Implemented` after authentication, rate-limit,
        and ownership checks. Submitted documents remain immutable.
      tags:
        - Invoices
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
      responses:
        "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: Invoice not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Invoice not found
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "501":
          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: Invoice deletion is not supported by the current provider
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "deleteInvoice is not supported by the Storecove provider (capability:
                  deleteInvoice). Storecove does not support invoice deletion."
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
  /invoices/{id}/ack:
    post:
      operationId: acknowledgeInvoice
      summary: Acknowledge receipt (unsupported by Storecove)
      description: |
        The current Storecove provider does not support acknowledgement and
        returns `501 Not Implemented` after authentication, rate-limit,
        idempotency, and ownership checks.
      tags:
        - Invoices
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "400":
          $ref: "#/components/responses/IdempotencyKeyBlank"
        "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: Invoice not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Invoice not found
        "422":
          $ref: "#/components/responses/IdempotencyKeyReuse"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "501":
          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: Acknowledgement is not supported by the current provider
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "acknowledgeInvoice is not supported by the Storecove provider
                  (capability: acknowledgeInvoice). Storecove does not support
                  acknowledgement workflows."
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
  /invoices/{id}/as/{format}:
    get:
      operationId: getInvoiceAs
      summary: Download a stored invoice document
      description: >
        Downloads the stored document the requested format selects. Returns the

        file content with that format's media type in `Content-Type`, and a

        matching `Content-Disposition` filename.


        A standard key can only download documents belonging to your own legal
        entity;

        an invoice sent on behalf of a sub-tenant answers `404 Not Found`,

        indistinguishable from an unknown id. Use a master key to download it.


        **Timing.** For a short window after `POST /invoices` returns, this

        endpoint answers `404 Not Found` on a document that is perfectly
        healthy:

        the stored copy is still being registered. Retry rather than treating
        the

        first 404 as a failure — no bound on that window is guaranteed, so poll

        rather than assuming a single retry is enough.


        ⚠️ A `200` here does **not** mean the document was delivered. The stored

        copy is available well before the receiving access point has signed for

        it, so this endpoint says nothing about delivery. Read `GET

        /invoices/{id}` for the status, or subscribe to `invoice.sent`.


        A `200` carries a document whose media type is the one the requested

        format names — never an unrelated body. When the upstream response

        cannot be read as a document inventory, when the stored object comes

        back empty, or when it is served under a contradicting media type, the

        endpoint answers `502 provider.unavailable` — a retryable failure —

        rather than relaying those bytes under a success status. Write the body

        to a file only on `200`.


        A format that names an XML schema is checked against the **business

        document** before it is served. `xml.ubl.invoice.bis3` resolves only

        when that document is UBL, and `xml.facturae.3.2` only when it is

        Facturae; otherwise the endpoint answers `404` with

        `invoices.export_format_unavailable` and lists the

        `availableMimeTypes`.


        ⛔ The check looks THROUGH the Peppol envelope; the response body still

        carries it. On the Peppol network the transmitted document is a

        Standard Business Document (SBDH) wrapping the invoice, and `original`

        and `xml.ubl.invoice.bis3` both return it whole — their root element is

        `<sh:StandardBusinessDocument>`, not `<Invoice>`. **To hand bytes

        straight to a UBL validator, request `payload`**, the only format that

        returns the business document bare.


        ⚠️ `original` and `payload` name no schema, by design: they return the

        XML that was transmitted whatever it is written in — with the envelope

        for `original`, without it for `payload`. They are the formats to ask

        for when you simply want the document that left.
      tags:
        - Invoices
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
        - name: format
          in: path
          required: true
          description: >-
            Output format for the invoice. `payload` returns the business
            document with the Peppol SBDH envelope removed, for callers who
            supplied their own UBL and want to inspect what was transmitted. It
            is the document **as transmitted**, not the bytes you sent: the
            network re-serialises in transit, so byte equality with your own
            file is not guaranteed. Seal a canonical form (C14N) if you need to
            compare across the boundary.

            `pdf` is served only when the provider's sending evidence carries a
            PDF rendering. When it does not — the sandbox rarely renders one —
            the endpoint returns `404` with result code
            `invoices.export_format_unavailable`; the JSON body lists the
            `availableMimeTypes`. It never substitutes the UBL XML for the
            requested PDF.

            `xml.ubl.invoice.bis3` and `xml.facturae.3.2` name a schema, and
            each resolves only when the stored document's business document is
            written in it. On an invoice transmitted as UBL — which is what the
            Peppol network carries — `xml.facturae.3.2` therefore answers `404`,
            not the UBL. Ask for `original` or `payload` when you want the
            transmitted document whatever its schema.

            ⛔ `xml.ubl.invoice.bis3` returns the same bytes as `original`,
            Peppol SBDH envelope included: the schema check reads through the
            envelope, it does not remove it. Only `payload` returns the bare
            business document, and it is the one to validate against a schema.

            `clearing` returns the flow a French regulated send reported to the
            DGFiP — flow 1, the XML the administration received (root
            `<Invoice>`, customization
            `urn.cpro.gouv.fr:1p0:einvoicingextract`). It is NOT the invoice the
            buyer received: ask for `payload` for that. Any document without a
            DGFiP leg — every non-French send — answers `404` with
            `invoices.export_format_unavailable`.
          schema:
            type: string
            enum:
              - pdf
              - xml.ubl.invoice.bis3
              - xml.facturae.3.2
              - original
              - payload
              - clearing
      responses:
        "200":
          description: The stored document the requested format selects, served under that
            format's media type. For the two formats that name an XML schema,
            the business document inside is written in that schema — which is
            not the same as the body's own root element. See the `format`
            parameter.
          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"
            Content-Disposition:
              description: Suggested filename for the download
              schema:
                type: string
              example: inline; filename="invoice-12345.pdf"
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            application/xml:
              schema:
                type: string
                format: binary
        "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 format specified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: 'Invalid format "docx". Valid formats: pdf, xml.ubl.invoice.bis3,
                  xml.facturae.3.2, original, payload, clearing'
        "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: Invoice not found/not owned, or the requested representation is not
            present in its sending evidence. A stored XML document written in a
            schema other than the one the requested format names counts as
            absent, and answers this same `404`. Inspect `Getpeppr-Result-Code`
            to distinguish `invoices.not_found`, `provider.resource_not_found`,
            and `invoices.export_format_unavailable`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - type: object
                    required:
                      - error
                      - availableMimeTypes
                    properties:
                      error:
                        type: string
                      availableMimeTypes:
                        type: array
                        items:
                          type: string
                examples:
                  invoiceNotFound:
                    summary: Invoice is not visible to this API key
                    value:
                      error: Invoice not found
                  formatUnavailable:
                    summary: The invoice has no PDF rendering
                    value:
                      error: Format "pdf" is not available for this invoice
                      availableMimeTypes:
                        - application/xml
                  schemaUnavailable:
                    summary: The stored XML is not written in the requested schema — here, Facturae
                      asked for on an invoice transmitted as UBL
                    value:
                      error: Format "xml.facturae.3.2" is not available for this invoice
                      availableMimeTypes:
                        - application/xml
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
        "503":
          $ref: "#/components/responses/ProviderRetryLater"
  /invoices/{id}/mark-as:
    post:
      operationId: markInvoiceAs
      summary: Report a French CTC invoice as paid
      description: >
        For a qualifying French CTC invoice, `state: paid` files the regulatory

        payment notification through the provider. Other state transitions are

        not supported by Storecove and return `501 Not Implemented` after the

        route's validation and ownership checks.


        **What is declared.** The whole invoice: there is no amount to pass, and

        the accredited platform takes the VAT breakdown of the invoice as it was

        sent. A partial payment cannot be reported yet.


        **Which invoices qualify.** A document qualifies when it was sent with
        the

        `france` declaration, which records the French CTC mandate profile. Any

        other invoice answers `422 invoices.payment_reporting_not_applicable`
        and

        nothing is filed. Not yet proven end to end: the report is filed with
        the

        provider, and its acceptance by the tax administration is not yet
        proven.


        **Whose identity is checked.** The identity of the legal entity that

        issued the invoice. For an invoice a platform sent on behalf of a

        sub-tenant (`sender` on `POST /v1/invoices`), that is the sub-tenant:
        the

        call needs a master key, the platform's own identity is not consulted,

        and a sub-tenant that cannot send is refused with

        `send_as.sub_tenant_not_sendable` (in the sandbox, for an identity whose

        remedy is a `9915` test sub-tenant,

        `send_as.sub_tenant_test_identity_required`), as its sends are. A
        standard key

        receives `404` for a sub-tenant's invoice, as it does on every read.
      tags:
        - Invoices
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - state
              properties:
                state:
                  type: string
                  enum:
                    - draft
                    - sending
                    - sent
                    - received
                    - accepted
                    - paid
                    - refused
                    - cancelled
                    - corrected
                  description: Target state for the invoice
                commit:
                  type: string
                  enum:
                    - with_mail
                  description: Same option as `MarkAsOptions.commit` in `@getpeppr/sdk`, forwarded
                    to the provider unchanged. It has no effect today — no
                    implemented transition reads it.
                reason:
                  type: string
                  description: Reason for the state change (optional)
            example:
              state: paid
      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: Invoice status updated
          content:
            application/json:
              schema:
                type: object
                description: State change result from the provider
        "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 state or missing required field
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                missing:
                  summary: Missing state field
                  value:
                    error: "Missing required field: state (string)"
                invalid:
                  summary: Invalid state value
                  value:
                    error: 'Invalid state "archived". Must be one of: draft, sending, sent,
                      received, accepted, paid, refused, cancelled, corrected'
        "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: Invoice not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Invoice not found
        "409":
          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 payment report is not settled. `Getpeppr-Result-Code` names
            which case it is. `invoices.payment_report_in_progress` — another
            call for the same invoice is filing it right now.
            `invoices.payment_report_unconfirmed` — an earlier attempt ended
            without telling whether the report was filed, and it is too late to
            resolve it by calling again (30 minutes after the first attempt);
            calling again files nothing, contact support.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          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: "State change refused. Read which refusal it is from the
            `Getpeppr-Result-Code` header, which is set on every branch below;
            the body shape differs between them.
            `identity.verification_required` — the production identity gate on
            an invoice your own legal entity issued, the same gate as on `POST
            /v1/invoices`; its body carries a `code` field naming which identity
            reason applies, among those listed in
            https://getpeppr.dev/docs/error-handling/#platform-errors (for
            example `address_held_elsewhere` while a previous provider still
            holds the address, or `smp_registration_failed`).
            `send_as.sub_tenant_not_sendable` — the invoice was issued by one of
            your sub-tenants and that sub-tenant cannot send; same body as the
            refusal of a send made on that sub-tenant's behalf, in sandbox and
            production alike. `send_as.sub_tenant_test_identity_required` —
            sandbox only: the same refusal when its remedy is a test sub-tenant
            under scheme `9915` rather than support.
            `invoices.payment_reporting_not_applicable` — the
            payment-notification gate: the invoice was not sent with the
            `france` declaration, so it does not carry the French CTC mandate
            profile; its body carries an `error` message only.
            `invoices.payment_reporting_credit_note` — the document is a credit
            note: payment reporting applies to invoices, and reporting a refund
            against the credited invoice is not supported yet. Nothing is filed.
            `invoices.payment_report_provider_failed` — the provider reported
            the report as failed; whether the tax authority received it is not
            established. Calling again files nothing; contact support.
            `provider.rejected` — the provider refused the transition."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "501":
          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 requested state transition is not supported by the current
            provider
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "markInvoiceAs is not supported by the Storecove provider (capability:
                  markInvoiceAs). Storecove does not support manual status
                  transitions."
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
  /invoices/import:
    post:
      operationId: importInvoice
      summary: Import an invoice from a file
      description: >
        Imports a UBL Invoice or CreditNote from a base64-encoded file and sends
        it

        on the Peppol network. **getpeppr does not regenerate, normalise, or
        repair

        your document** — we forward the bytes you supplied, unchanged, to the

        network.


        **Byte-for-byte equality is not guaranteed**, because the network

        re-serialises the document in transit. Measured 2026-08-18 on a test

        document: namespace declarations came back reordered, numeric character

        references were resolved (`&#65;` became `A`), whitespace inside tags
        was

        dropped, and no element was added, removed or altered. That is one

        document and four kinds of difference, so treat it as indicative rather

        than as a warranty of what is preserved.


        If you seal your documents, hash a canonical form (C14N) rather than the

        raw bytes.


        Before transmission the document is validated against the complete
        official

        OpenPeppol rulebooks. A document violating any `fatal` rule is refused
        with a

        terminal `422` naming the rule, and is **not sent** — retrying the same

        document will fail identically. Send `x-skip-validation: true` to bypass

        validation for one call.


        The `to.peppolId` value remains the sole routing authority. getpeppr

        reads `AccountingCustomerParty/Party/EndpointID` only as a consistency

        check: when both complete identities disagree, the document is still

        submitted to `to.peppolId` during the warning-first rollout and the

        `201` receipt carries a PII-safe warning with

        `ruleId: recipient_endpoint_mismatch`. Verify the intended recipient and

        make both values agree before the next send. This check still runs under

        `x-skip-validation: true`; that header skips the Peppol rulebooks, not

        routing consistency.


        ## Sending on behalf of a sub-tenant


        A platform account sends for one of its own customers by naming that

        sub-tenant in `sender`, exactly as on `POST /invoices`: either

        `{ "legalEntityId": "…" }` or `{ "externalSubTenantId": "…" }` — one of

        the two, never both. This requires a **master key**; a standard key that

        sends `sender` is refused with `403`, never silently ignored.


        Without `sender` the document is sent under your account's own legal

        entity, and a document issued by anyone else is refused with

        `supplier_identity_not_owned`. Registering a sub-tenant does **not**

        change that: the ownership check compares against your own company

        unless you name the sub-tenant explicitly.


        When you do name one, the document and the envelope must agree: the

        supplier endpoint stated in the document must be the sub-tenant's own.

        getpeppr never rewrites your document, so a disagreement is refused with

        `supplier_identity_mismatch` rather than resolved for you.


        Your document needs that supplier endpoint in any case — the rulebook

        requires it (`PEPPOL-EN16931-R020`, *Seller electronic address MUST be

        provided*, fatal), so a document omitting it is refused at validation

        with `422 validation_failed` before identity is even considered. The

        only way one reaches the identity check without it is

        `x-skip-validation: true`, and it is then sent under the sub-tenant's

        identity.


        Two size caps apply, and the smaller one decides. The request body is

        capped at 4 MiB on the actual request bytes, including chunked bodies

        and requests with an omitted or under-declared `Content-Length`. The

        decoded document keeps its own 4 MiB defence-in-depth cap. Because the

        document travels base64-encoded inside JSON, which inflates it by about

        a third, the body cap is what you hit first: the largest document that

        fits is roughly 3 MB.


        Supports idempotency via the `Idempotency-Key` header.
      tags:
        - Invoices
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: x-skip-validation
          in: header
          required: false
          description: |
            Set to `true` to send the document without validating it. The escape
            hatch against a false positive of ours: being stricter than the
            network is the symmetric error of a silent death, and the worse of
            the two. Any other value, and the absence of the header, validate
            normally.

            Each use is logged, at the level of the account and nothing finer:
            when the opt-out is set no Peppol rulebook validation runs, so no
            Peppol rule fires and none can be named. The routing consistency
            check above still runs. The signal is coarse on purpose — an account
            opting out repeatedly is one worth a conversation, not a rule we can
            point at.
          schema:
            type: string
            enum:
              - "true"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ImportInvoiceRequest"
            example:
              file: PD94bWwgdmVyc2lvbj0iMS4wIj8+...
              filename: invoice-2026-001.xml
              to:
                peppolId: 0208:0685660237
      responses:
        "201":
          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: |
            Invoice imported and sent.

            The body is the standard send result plus up to four keys.
            `rulebook` names the rulebook release the document was judged
            against; it is present whenever validation actually ran, and absent
            when it did not — that is, when the call carried
            `x-skip-validation: true`. No Peppol rulebook was judged in that
            case, so naming one would claim a verdict that was never reached.

            `duplicateOf` appears only when this same document was already sent
            further back than the refusal window, and names the earlier one. Its
            absence on an ordinary send is the normal case — read the section
            *Duplicate documents* above for what it does and does not promise.

            `warnings` includes the non-blocking
            `recipient_endpoint_mismatch` signal when `to.peppolId` and the
            document's complete customer EndpointID disagree or are ambiguous.
            The warning contains no participant identifiers. The document was
            submitted using `to.peppolId`; verify the intended recipient and
            make both values agree before the next send. A replay of a receipt
            cached before this field existed may omit it for up to 24 hours.

            `transmission` states what getpeppr does **not** guarantee about
            your bytes, and this endpoint sends it on every `201` it produces —
            not on a body replayed from an `Idempotency-Key` cached before the
            field existed, which is why it is not declared `required`. getpeppr
            forwards the document you supplied unchanged, but the Peppol network
            re-serialises it in transit, so byte-for-byte equality is not
            something to rely on — seal a canonical form (C14N) rather than raw
            bytes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImportInvoiceResult"
        "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 or missing required fields.


            Two machine codes are returned here: `invalid_base64` (the `file`

            field does not decode to exactly the bytes it announces — getpeppr

            never repairs your document, so a lossy decode is refused rather
            than

            patched) and `missing_recipient` (`to.peppolId` is absent; it is

            never derived from the document, because routing is what decides

            delivery and the document is payload).


            A malformed `sender` lands here too, with the sentence

            `sender requires exactly one of legalEntityId or
            externalSubTenantId`

            — sending both, or an empty object, is a `400`, never a silent pick.

            This applies to **master keys**: a standard key is refused with
            `403`

            for naming a sub-tenant at all, before the shape is ever examined.


            A blank `Idempotency-Key` lands here too, with the code

            `idempotency_key_blank`: the header was sent but carries nothing

            usable, so it is refused rather than ignored. It is checked after

            the four production gates, so a request that fails one of those gets

            that failure instead.


            The remaining `400`s on this route are older refusals that carry a

            human sentence in `error` rather than a code — a missing `file` or

            `filename`, or a body that is not JSON. Match on the code when there

            is one; do not parse the sentences.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: invalid_base64
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          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: A standard key named a sub-tenant in `sender`. Sending on behalf of
            a sub-tenant requires a master key — the field is never silently
            ignored.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Your API key does not have permission to perform this action
        "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: The sub-tenant named in `sender` was not found, or is not owned by
            this platform. The two are deliberately indistinguishable
            (`send_as.sub_tenant_not_found`). A `sender.legalEntityId` that is
            not in the getpeppr id format also returns `404`, but with
            `request.resource_id_malformed`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: not_found
        "409":
          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: >-
            `duplicate_document` — the same document was already sent moments
            ago. Nothing was sent to the Peppol network. The response names the
            earlier document in `duplicateOf`; read its status rather than
            sending again.

            Unlike `POST /v1/invoices`, this endpoint holds no idempotency lock,
            so a concurrent request carrying the same `Idempotency-Key` does not
            produce a `409` here — this status has exactly one cause on this
            route.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: duplicate_document
                message: "Invoice INV-2026-001 was already sent 7s ago (document
                  e8ec77a9-0000-4000-8000-000000000000). Sending it again would
                  deliver a second copy to the recipient — Peppol does not
                  de-duplicate. That earlier document is named in `duplicateOf`:
                  read its status rather than sending again. If this is a
                  deliberate resend, retry after 15 minutes."
                duplicateOf: e8ec77a9-0000-4000-8000-000000000000
        "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. Because the document is
            base64-encoded inside JSON, this is reached at roughly 3 MB of
            document.

            The transport cap is measured on the actual request bytes, including
            chunked bodies and requests with an omitted or under-declared
            `Content-Length`. It 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"
        "422":
          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 document was NOT sent. Two unrelated families answer with this

            status, and **they do not retry the same way** — do not treat them

            alike. A third, `idempotency_key_reuse`, is neither: the

            `Idempotency-Key` was already used for a different endpoint or a

            different body, so use a new key.


            **Document rejected** (`validation_failed`, `not_ubl_document`,

            `document_too_complex`, `undecodable_document`,

            `unsupported_encoding`) is **terminal**: the same bytes fail

            identically forever, and a retry is pure waste. Fix the document —

            `validation_failed` names the rule that refused it.


            **Account may not send right now** (`peppol_identity_incomplete`,

            `peppol_identity_not_verified`, `platform_billing_not_active`,

            `production_access_expired`) is **not terminal**: it describes

            account state, and account state changes — a verification completes,

            a contract is activated. The identical document goes through once it

            does.


            They are fixed in

            different places. **The account is asked first**: a document is
            never

            judged on behalf of an account that may not send at all.


            *The account may not send* — `peppol_identity_incomplete` (no

            production Peppol identifier registered),

            `peppol_identity_not_verified` (an identifier exists but is not in a

            send-allowed state, or carries an active flag),

            `platform_billing_not_active` (the platform contract is suspended,
            or

            not live), `production_access_expired` (a

            complimentary grant has lapsed). These carry a `code` and often a

            `docs` link, never a `rulebook`.


            Three of the four are production-only, and sandbox is exempt from

            them. `peppol_identity_not_verified` is **not**: when you name a

            sub-tenant in `sender`, it also answers for THAT sub-tenant's

            identity, in sandbox as in production — a sub-tenant whose

            verification is still running, or whose registration failed, cannot

            issue a document either way. Its `code` tells the two apart.


            *The document is refused* — `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), `unreadable_document` (number, issue date, currency,

            customer name or country cannot be read from it),

            `malformed_xml` (not accepted as XML, or it carries a document type

            declaration), `not_ubl_document` (well-formed XML, 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, or send it with `x-skip-validation: true` if

            you have validated it yourself), `validation_failed` (at least one
            fatal Peppol rule is

            violated; fix the document, or set `x-skip-validation: true` to

            bypass validation for this call), `supplier_identity_not_owned` (the

            document is issued by a Peppol identifier this account has not

            registered — if one of your sub-tenants issued it, name that

            sub-tenant in `sender`, because registering it is not enough on its

            own), `supplier_identity_ambiguous` (the document names no

            supplier endpoint and the account holds none, or holds several),

            `supplier_identity_mismatch` (you named a sub-tenant in `sender` and

            the document states a different supplier; getpeppr never rewrites

            your document, so the two must agree).

            Only `validation_failed` carries `rulebook` and `violations`.
          content:
            application/json:
              schema:
                type: object
                required:
                  - error
                properties:
                  error:
                    type: string
                    enum:
                      - peppol_identity_incomplete
                      - peppol_identity_not_verified
                      - platform_billing_not_active
                      - production_access_expired
                      - undecodable_document
                      - unsupported_encoding
                      - unreadable_document
                      - malformed_xml
                      - not_ubl_document
                      - document_too_complex
                      - validation_failed
                      - supplier_identity_not_owned
                      - supplier_identity_ambiguous
                      - supplier_identity_mismatch
                  code:
                    type: string
                    description: |
                      Narrower machine reason, on the account-gate family only.
                  message:
                    type: string
                  docs:
                    type: string
                    format: uri
                    description: >
                      Where to go to clear the gate, on the account-gate family
                      only.
                  rulebook:
                    $ref: "#/components/schemas/UblValidationVerdict/properties/rulebook"
                  violations:
                    $ref: "#/components/schemas/UblValidationVerdict/properties/violations"
        "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: |
            The document was NOT sent.

            `validation_unavailable` is a deliberate fail-closed refusal, not a
            crash: the validator could not run, so getpeppr declines to send a
            document it was unable to check rather than letting it through
            unverified. It is safe to retry. Any other `500` carries a human
            sentence and is an ordinary internal error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: validation_unavailable
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
        "503":
          $ref: "#/components/responses/ProviderRetryLater"
  /invoices/send/{id}:
    post:
      operationId: sendInvoice
      summary: Send a draft invoice (unsupported by Storecove)
      description: |
        Storecove does not support draft invoices: `POST /invoices` with
        `_draft: true` returns `422`, and this endpoint returns `501 Not
        Implemented` after authentication, ownership and production gates.
        Submit the final invoice directly with `POST /invoices` instead.

        Supports idempotency via the `Idempotency-Key` header.
      tags:
        - Invoices
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "400":
          $ref: "#/components/responses/IdempotencyKeyBlank"
        "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: Invoice not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Invoice not found
        "422":
          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: "Production send gate failed — Peppol identity not verified,
            platform billing not active, or commercial production access
            expired. Same gates as POST /invoices. This status also covers
            `idempotency_key_reuse`: an `Idempotency-Key` already used for a
            different request."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                identityNotVerified:
                  value:
                    error: peppol_identity_not_verified
                    code: verification_pending
                billingNotActive:
                  value:
                    error: platform_billing_not_active
                    code: billing_state_invalid
                accessExpired:
                  value:
                    error: production_access_expired
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "501":
          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: Draft sending is not supported by the current provider
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "sendInvoiceById is not supported by the Storecove provider (capability:
                  sendDraftById)."
        "502":
          $ref: "#/components/responses/ProviderUnavailable"
  /received-documents:
    get:
      operationId: listReceivedDocuments
      summary: List received documents
      description: >
        Returns a paginated list of the Peppol documents received for your

        account, newest first.


        Results are scoped to your account **and to the environment of the API

        key** used to make the request — a sandbox key returns only sandbox

        documents, a production key only production ones. The environment is

        never taken from a query parameter.


        They are also scoped to the **type** of the key: a standard key returns
        only

        the documents addressed to your own legal entity, while a master key
        returns

        those of every sub-tenant it owns.
      tags:
        - Received Documents
      parameters:
        - name: legalEntityId
          in: query
          description: Return only the documents addressed to this Legal Entity. An
            unknown or unowned id simply yields an empty list (no 404); a value
            that is not a UUID is rejected with 400.
          schema:
            type: string
            format: uuid
        - $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 received documents
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ReceivedDocument"
                  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 `legalEntityId`
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: legalEntityId must be a UUID
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /received-documents/{id}:
    get:
      operationId: getReceivedDocument
      summary: Get a received document
      description: |
        Returns one received document.

        An unknown id, a document belonging to another account, and a document
        from the other environment are indistinguishable: all three answer
        `404`. This is deliberate — a `403` would confirm that the document
        exists. A sub-tenant's document read with a standard key answers `404`
        for the same reason — use a master key to retrieve it.
      tags:
        - Received Documents
      parameters:
        - name: id
          in: path
          required: true
          description: The received document id.
          schema:
            type: string
            format: uuid
      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: The received document
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReceivedDocument"
        "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: "No such document is visible to this API key
            (`received_documents.not_found`, deliberately indistinguishable). An
            id that is not in the getpeppr id format also returns `404`, but
            with `request.resource_id_malformed`: pass an id exactly as getpeppr
            returned it, not a reference from your own system."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Not found
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /received-documents/{id}/as/xml:
    get:
      operationId: getReceivedDocumentXml
      summary: Download the original UBL
      description: >
        Returns the document exactly as it arrived from the Peppol network,

        byte for byte. The stored bytes are the archive — getpeppr applies no

        transformation and no re-rendering.


        The guarantee is about our custody, not about the whole journey: what

        arrives may already have been re-serialised upstream, as documents sent

        through getpeppr are. What you download is byte-identical to what
        reached

        us, never a re-rendering of it.


        This is the artifact with evidential value: the JSON representation

        returned by the other endpoints is our reading of the document, not the

        document itself.


        A standard key can only download documents addressed to your own legal

        entity; a sub-tenant's document answers `404 Not Found`,
        indistinguishable

        from an unknown id. Use a master key to download it.


        The original is kept for 90 days after receipt, or until a Platform

        contract's exit window closes if that comes first. After that it answers

        `410 Gone`; the document itself stays readable through

        `GET /received-documents/{id}`.
      tags:
        - Received Documents
      parameters:
        - name: id
          in: path
          required: true
          description: The received document id.
          schema:
            type: string
            format: uuid
      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: |
            The original UBL. `Content-Disposition` names the file after the
            sender's document number, reduced to `[A-Za-z0-9_.-]`, falling back
            to the document id.
          content:
            application/xml:
              schema:
                type: string
                format: binary
        "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: "No such document is visible to this API key
            (`received_documents.not_found`, deliberately indistinguishable). An
            id that is not in the getpeppr id format also returns `404`, but
            with `request.resource_id_malformed`: pass an id exactly as getpeppr
            returned it, not a reference from your own system."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Not found
        "410":
          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 document is visible to this API key, but its original UBL is
            no longer retained (`received_documents.xml_expired`): 90 days have
            passed since receipt, or the account's Platform exit window has
            closed. Not retryable. `ublPurgedAt` gives the removal time."
          content:
            application/json:
              schema:
                type: object
                required:
                  - error
                  - ublPurgedAt
                properties:
                  error:
                    type: string
                  ublPurgedAt:
                    type: string
                    format: date-time
              example:
                error: Original UBL no longer retained
                ublPurgedAt: 2026-09-18T04:40:12.000Z
        "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.
