openapi: 3.1.0
info:
  title: getpeppr API
  version: 1.0.0
  description: |
    Developer-first API gateway for Peppol e-invoices.

    Send JSON, receive Peppol-compliant UBL — getpeppr handles the XML translation,
    validation, and delivery to the Peppol network via Storecove.

    ## Authentication

    All endpoints require a Bearer token in the `Authorization` header.
    API keys follow the format `sk_sandbox_*` (sandbox) or `sk_live_*` (production).

    ```
    Authorization: Bearer sk_sandbox_abc123...
    ```

    ## Rate Limits

    Rate limits are enforced per API key and per account, based on your subscription tier:

    | Tier       | Per Key (req/min) | Per Account (req/min) |
    |------------|-------------------|-----------------------|
    | Sandbox    | 10                | 50                    |
    | Starter    | 60                | 300                   |
    | Pro        | 120               | 600                   |
    | Business   | 300               | 1500                  |

    Validation endpoints (`/validate` and `/validate/server`) use a separate per-key bucket:
    Sandbox 20 req/min, Starter 120 req/min, Pro 240 req/min, Business 600 req/min.
    They remain subject to the account-level safety limit above.

    When rate-limited, the response includes a `Retry-After` header with the number of seconds to wait.

    ## Idempotency

    POST endpoints support an optional `Idempotency-Key` header. If provided, the response
    is cached for 24 hours (scoped to your API key). Replaying the same key returns the
    cached response without re-executing the operation.

    ## Multi-Tenant Isolation

    All resources are scoped to your account. Requesting a resource owned by another account
    returns `404 Not Found` (not `403 Forbidden`) to prevent resource enumeration.
    Platform endpoints require a master key with the relevant `legal_entities:*` scope.
    Use `POST /legal-entities` to provision sub-tenants, then pass `sender.legalEntityId`
    or `sender.externalSubTenantId` to `POST /invoices` to send on their behalf.

    ## Environments

    Your API key determines the environment:
    - `sk_sandbox_*` keys hit the sandbox environment
    - `sk_live_*` keys hit the production environment
  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: Contacts
    description: Manage your address book of clients and providers.
  - name: Bank Accounts
    description: Manage bank accounts for payment information on invoices.
  - name: Directory
    description: Look up participants on the Peppol network.
  - name: Transports
    description: View transport documents and available transport types.
  - name: Validation
    description: Validate invoice data before sending.
  - name: Events
    description: View the event log for your invoices.
  - name: Onboarding
    description: Register your legal entity and Peppol identity before sending invoices.
  - name: Legal Entities
    description: Provision, list, authorise, and off-board platform sub-tenants.
  - name: Capabilities
    description: Discover which operations the active provider supports.
  - name: Health
    description: Public health check endpoint for uptime monitoring.
  - name: Newsletter
    description: Public newsletter signup, confirmation, and unsubscribe (no auth)
  - name: Webhooks
    description: |
      Receive real-time notifications about invoice lifecycle and platform sub-tenant lifecycle events.

      ## Setup

      Configure webhook endpoints via the getpeppr dashboard (Settings → Webhooks).
      Each endpoint receives a `whsec_*` secret shown once at creation.

      ## Event Types

      | Event | Description |
      |-------|-------------|
      | `invoice.sent` | Invoice successfully delivered to recipient's access point |
      | `invoice.accepted` | Recipient accepted the invoice |
      | `invoice.refused` | Recipient rejected the invoice |
      | `invoice.error` | Delivery failed (final state) |
      | `invoice.registered` | Cleared by tax authority (e.g., KSA, PT) |
      | `invoice.received` | Receipt acknowledged by recipient (outbound — a document you sent was acknowledged) |
      | `invoice.paid` | Payment confirmed by recipient |
      | `legal_entity.registered` | Platform sub-tenant reached a verified or active state |
      | `legal_entity.verification_failed` | Platform sub-tenant registry verification failed |
      | `legal_entity.awaiting_authz` | Platform sub-tenant authorisation email is awaiting customer action |
      | `legal_entity.registration_failed` | Platform sub-tenant identity verified but network (SMP) registration failed |
      | `peppol_identifier.verified` | A Peppol identifier completed registry verification |
      | `peppol_identifier.verification_failed` | A Peppol identifier failed registry verification |
      | `test.ping` | Test event sent during endpoint setup |
      | `inbound.invoice.received` | An invoice addressed to your Legal Entity was received from the Peppol network (pilot) |
      | `inbound.creditnote.received` | A credit note addressed to your Legal Entity was received from the Peppol network (pilot) |
      | `*` | Wildcard — subscribes to all events |

      > **Note:** `inbound.invoice.received` and `inbound.creditnote.received` are inbound reception
      > events (a document was sent _to_ your Legal Entity by a third party). They are distinct from
      > the outbound `invoice.received` event (a document _you sent_ was acknowledged by the recipient).
      > Inbound events are a pilot feature — contact support@getpeppr.dev to enable.

      ## Payload Format

      ```json
      {
        "id": "evt_abc123def456",
        "type": "invoice.sent",
        "data": {
          "invoiceId": "12345",
          "invoiceNumber": "INV-2026-001",
          "environment": "sandbox"
        },
        "createdAt": "2026-02-26T10:30:00.000Z"
      }
      ```

      ## Signature Verification

      Every request includes a `Getpeppr-Signature` header:

      ```
      Getpeppr-Signature: t=1709000000,s=a1b2c3d4e5...
      ```

      To verify:
      1. Extract `t` (Unix timestamp) and `s` (HMAC hex)
      2. Compute `HMAC-SHA256(secret, "${t}.${rawBody}")` where `rawBody` is the **raw request body string** (do NOT parse and re-serialize — use the bytes as received)
      3. Compare with `s` using constant-time comparison
      4. Reject if timestamp is older than 5 minutes (replay protection)

      ## Retry Policy

      Failed deliveries (non-2xx or timeout after 5s) are retried with exponential backoff:
      - Initial delivery: immediate (synchronous, 5s timeout)
      - Retry 1: after 1 minute
      - Retry 2: after 5 minutes
      - Retry 3: after 30 minutes
      - After 4 total attempts: marked as `failed`

security:
  - bearerAuth: []

paths:
  # ─── Invoices ───────────────────────────────────────────────

  /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; any `from`
        object in the payload is ignored on the send-as path.

        Set `_draft: true` to create a draft without sending. Drafts can be sent later
        via `POST /invoices/send/{id}`.

        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/InvoiceInput'
            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.00
                  vatRate: 21
              buyerReference: "PO-2026-042"
      responses:
        '201':
          description: Invoice created (and sent, unless `_draft` was true)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendResult'
        '400':
          description: Invalid request body or missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Missing required fields: number (string), to (object), lines (array)"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Master key is missing the legal_entities:send_as scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Your API key does not have permission to perform this action"
        '404':
          description: Sub-tenant sender was not found or is not owned by this platform
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "not_found"
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          description: Recipient not found in Peppol Directory, or sender sub-tenant is not send-ready
          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/"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    get:
      operationId: listInvoices
      summary: List invoices
      description: |
        Returns a paginated list of invoices owned by your account. Supports filtering
        by type (issued or received).
      tags: [Invoices]
      parameters:
        - name: type
          in: query
          description: Filter by invoice type
          schema:
            type: string
            enum: [issued, received]
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          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'
        '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.
      tags: [Invoices]
      parameters:
        - $ref: '#/components/parameters/InvoiceId'
      responses:
        '200':
          description: Invoice status and details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          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'

    put:
      operationId: updateInvoice
      summary: Update a draft invoice
      description: |
        Updates an existing invoice. The invoice must be owned by your account.
        Typically used to modify draft invoices before sending.
      tags: [Invoices]
      parameters:
        - $ref: '#/components/parameters/InvoiceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceInput'
      responses:
        '200':
          description: Invoice updated
          content:
            application/json:
              schema:
                type: object
                description: Updated invoice data from the provider
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          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'
    delete:
      operationId: deleteInvoice
      summary: Delete an invoice
      description: |
        Permanently deletes an invoice. The invoice must be owned by your account.
        The resource ownership record is also removed.
      tags: [Invoices]
      parameters:
        - $ref: '#/components/parameters/InvoiceId'
      responses:
        '200':
          description: Invoice deleted
          content:
            application/json:
              schema:
                type: object
                description: Deletion result from the provider
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          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'

  /invoices/{id}/ack:
    post:
      operationId: acknowledgeInvoice
      summary: Acknowledge receipt of an invoice
      description: |
        Acknowledges receipt of a received invoice. This updates the invoice status
        on the Peppol network.

        Supports idempotency via the `Idempotency-Key` header.
      tags: [Invoices]
      parameters:
        - $ref: '#/components/parameters/InvoiceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Invoice acknowledged successfully
          content:
            application/json:
              schema:
                type: object
                description: Acknowledgement result from the provider
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          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'

  /invoices/{id}/as/{format}:
    get:
      operationId: getInvoiceAs
      summary: Download invoice in a specific format
      description: |
        Downloads the invoice in the specified format. Returns the file content
        with appropriate `Content-Type` and `Content-Disposition` headers.
      tags: [Invoices]
      parameters:
        - $ref: '#/components/parameters/InvoiceId'
        - name: format
          in: path
          required: true
          description: Output format for the invoice
          schema:
            type: string
            enum: [pdf, xml.ubl.invoice.bis3, xml.facturae.3.2, original]
      responses:
        '200':
          description: Invoice file content
          headers:
            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':
          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'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          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'

  /invoices/{id}/mark-as:
    post:
      operationId: markInvoiceAs
      summary: Change invoice status
      description: |
        Manually changes the status of an invoice on the provider side.
        Useful for marking invoices as paid, accepted, or cancelled outside
        of the normal Peppol delivery flow.
      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: boolean
                  description: Whether to commit the state change on the provider
                reason:
                  type: string
                  description: Reason for the state change (optional)
            example:
              state: "paid"
      responses:
        '200':
          description: Invoice status updated
          content:
            application/json:
              schema:
                type: object
                description: State change result from the provider
        '400':
          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':
          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'

  /invoices/import:
    post:
      operationId: importInvoice
      summary: Import an invoice from a file
      description: |
        Imports an invoice from a base64-encoded file (XML, PDF, etc.). The file
        is forwarded to the provider for parsing and registration.

        Maximum file size: 10 MB.

        Supports idempotency via the `Idempotency-Key` header.
      tags: [Invoices]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportInvoiceRequest'
            example:
              file: "PD94bWwgdmVyc2lvbj0iMS4wIj8+..."
              filename: "invoice-2026-001.xml"
      responses:
        '201':
          description: Invoice imported successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendResult'
        '400':
          description: Invalid request body or missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Missing required field: file (base64-encoded string)"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: File exceeds 10 MB limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Payload too large. Maximum size: 10240KB"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /invoices/send/{id}:
    post:
      operationId: sendInvoice
      summary: Send a draft invoice
      description: |
        Sends a previously created draft invoice to the Peppol network.
        The invoice must have been created with `_draft: true`.

        Returns `204 No Content` on success.

        Supports idempotency via the `Idempotency-Key` header.
      tags: [Invoices]
      parameters:
        - $ref: '#/components/parameters/InvoiceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Invoice sent successfully (no content)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          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'

  # ─── Contacts ───────────────────────────────────────────────

  /contacts:
    get:
      operationId: listContacts
      summary: List contacts
      description: |
        Returns a paginated list of contacts (clients and providers) in your address book.
        Supports filtering by name, client status, and provider status.
      tags: [Contacts]
      parameters:
        - name: name
          in: query
          description: Filter contacts by name (case-insensitive partial match)
          schema:
            type: string
        - name: isClient
          in: query
          description: Filter by client status
          schema:
            type: boolean
        - name: isProvider
          in: query
          description: Filter by provider/supplier status
          schema:
            type: boolean
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Paginated list of contacts
          content:
            application/json:
              schema:
                type: object
                properties:
                  contacts:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createContact
      summary: Create a contact
      description: |
        Creates a new contact in your address book. The `name` field is required.

        Supports idempotency via the `Idempotency-Key` header.
      tags: [Contacts]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactInput'
            example:
              name: "ACMEDIA"
              peppolId: "0208:0685660237"
              vatNumber: "BE0685660237"
              country: "BE"
              isClient: true
      responses:
        '201':
          description: Contact created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          description: Invalid request body or missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Missing required field: name (string)"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /contacts/{id}:
    get:
      operationId: getContact
      summary: Get a contact
      description: Returns the details of a specific contact owned by your account.
      tags: [Contacts]
      parameters:
        - $ref: '#/components/parameters/ContactId'
      responses:
        '200':
          description: Contact details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Contact not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Contact not found"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      operationId: updateContact
      summary: Update a contact
      description: Updates an existing contact. All provided fields are overwritten.
      tags: [Contacts]
      parameters:
        - $ref: '#/components/parameters/ContactId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactInput'
      responses:
        '200':
          description: Contact updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Contact not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Contact not found"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteContact
      summary: Delete a contact
      description: Permanently deletes a contact from your address book.
      tags: [Contacts]
      parameters:
        - $ref: '#/components/parameters/ContactId'
      responses:
        '204':
          description: Contact deleted (no content)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Contact not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Contact not found"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  # ─── Bank Accounts ─────────────────────────────────────────

  /bank-accounts:
    get:
      operationId: listBankAccounts
      summary: List bank accounts
      description: Returns a paginated list of bank accounts owned by your account.
      tags: [Bank Accounts]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Paginated list of bank accounts
          content:
            application/json:
              schema:
                type: object
                properties:
                  bankAccounts:
                    type: array
                    items:
                      $ref: '#/components/schemas/BankAccount'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createBankAccount
      summary: Create a bank account
      description: |
        Creates a new bank account. The `name` field is required.

        Supports idempotency via the `Idempotency-Key` header.
      tags: [Bank Accounts]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BankAccountInput'
            example:
              name: "Main EUR Account"
              type: "iban"
              iban: "BE68539007547034"
              bic: "BBRUBEBB"
              country: "BE"
      responses:
        '201':
          description: Bank account created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BankAccount'
        '400':
          description: Invalid request body or missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Missing required field: name (string)"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /bank-accounts/{id}:
    get:
      operationId: getBankAccount
      summary: Get a bank account
      description: Returns the details of a specific bank account owned by your account.
      tags: [Bank Accounts]
      parameters:
        - $ref: '#/components/parameters/BankAccountId'
      responses:
        '200':
          description: Bank account details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BankAccount'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Bank account not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Bank account not found"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      operationId: updateBankAccount
      summary: Update a bank account
      description: Updates an existing bank account. All provided fields are overwritten.
      tags: [Bank Accounts]
      parameters:
        - $ref: '#/components/parameters/BankAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BankAccountInput'
      responses:
        '200':
          description: Bank account updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BankAccount'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Bank account not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Bank account not found"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteBankAccount
      summary: Delete a bank account
      description: Permanently deletes a bank account.
      tags: [Bank Accounts]
      parameters:
        - $ref: '#/components/parameters/BankAccountId'
      responses:
        '204':
          description: Bank account deleted (no content)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Bank account not found or not owned by your account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Bank account not found"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  # ─── Directory ──────────────────────────────────────────────

  /directory/search:
    get:
      tags:
        - Directory
      summary: Search the Peppol Directory
      description: |
        Search for participants in the Peppol Directory by name, country, or VAT number.
        At least one search criterion is required. Name searches require a minimum of 3 characters.
        Results are cached for 1 hour.
      operationId: searchDirectory
      parameters:
        - name: name
          in: query
          description: Business name to search (min 3 characters)
          schema:
            type: string
            minLength: 3
          example: "Acme"
        - name: country
          in: query
          description: Country filter (ISO 3166-1 alpha-2, uppercase)
          schema:
            type: string
            pattern: "^[A-Z]{2}$"
          example: "BE"
        - name: vatNumber
          in: query
          description: VAT number to search (country prefix is auto-stripped)
          schema:
            type: string
          example: "BE0685660237"
        - name: limit
          in: query
          description: Max results per page (default 20, max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          description: Offset for pagination (default 0)
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DirectoryEntry'
                  meta:
                    type: object
                    properties:
                      totalCount:
                        type: integer
                      offset:
                        type: integer
                      limit:
                        type: integer
                      hasMore:
                        type: boolean
        '400':
          description: Missing search criteria, name too short, or invalid params
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
      security:
        - bearerAuth: []

  /directory/{scheme}/{participantId}:
    get:
      operationId: lookupDirectory
      summary: Look up a Peppol participant
      description: |
        Look up a Peppol participant by their scheme and identifier.
        Returns enriched data from the Peppol Directory including business name, country,
        capabilities, registration date, VAT number, and contact info.
        Falls back to basic registration check if the Peppol Directory is unavailable.
        Results are cached for 24 hours.

        Two URL formats are supported:
        - `/directory/{scheme}/{id}` (e.g., `/directory/0208/0685660237`)
        - `/directory/{scheme}:{id}` (e.g., `/directory/0208:0685660237`)

        Directory lookups have an additional rate limit of 30 requests per minute per API key.
      tags: [Directory]
      parameters:
        - name: scheme
          in: path
          required: true
          description: Peppol participant scheme (e.g., "0208" for Belgian BCE)
          schema:
            type: string
          example: "0208"
        - name: participantId
          in: path
          required: true
          description: Participant identifier within the scheme
          schema:
            type: string
          example: "0685660237"
      responses:
        '200':
          description: Participant found in the Peppol directory
          content:
            application/json:
              schema:
                type: object
                properties:
                  participant:
                    type: object
                    properties:
                      registered:
                        type: boolean
                        description: Whether the participant is registered on Peppol
                      scheme:
                        type: string
                        description: Peppol scheme code
                        example: "0208"
                      id:
                        type: string
                        description: Participant identifier within the scheme
                        example: "0685660237"
                      name:
                        type: string
                      country:
                        type: string
                      capabilities:
                        type: array
                        items:
                          type: string
                      registrationDate:
                        type: string
                      vatNumber:
                        type: string
                      additionalIds:
                        type: array
                        items:
                          type: object
                          properties:
                            scheme:
                              type: string
                            value:
                              type: string
                      contactInfo:
                        type: object
                        properties:
                          name:
                            type: string
                          email:
                            type: string
                          phone:
                            type: string
                      website:
                        type: string
              example:
                participant:
                  registered: true
                  scheme: "0208"
                  id: "0685660237"
                  name: "ACMEDIA"
                  country: "BE"
                  capabilities:
                    - "invoice"
                    - "credit_note"
                  registrationDate: "2020-11-20"
        '404':
          description: Participant not found in the Peppol network
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Participant not found"
        '400':
          description: Invalid Peppol ID format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Invalid Peppol ID format. Expected: /directory/{scheme}/{id} or /directory/{scheme}:{id}"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  # ─── Transports ─────────────────────────────────────────────

  /transports:
    get:
      operationId: listTransports
      summary: List transports
      description: |
        Returns the list of active transports. Currently, only the Peppol AS4 transport
        (via Storecove) is available. Transport routing is managed automatically —
        write operations (POST, PUT, DELETE) return `405 Method Not Allowed`.
      tags: [Transports]
      responses:
        '200':
          description: List of transports
          content:
            application/json:
              schema:
                type: object
                properties:
                  transports:
                    type: array
                    items:
                      $ref: '#/components/schemas/Transport'
              example:
                transports:
                  - id: "peppol"
                    transportTypeCode: "peppol"
                    name: "Peppol AS4 (Storecove)"
                    status: "active"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /transports/{code}:
    get:
      operationId: getTransport
      summary: Get a transport by code
      description: |
        Returns details of a specific transport. Currently only `peppol` is a valid code.
      tags: [Transports]
      parameters:
        - name: code
          in: path
          required: true
          description: Transport code (e.g., "peppol")
          schema:
            type: string
          example: "peppol"
      responses:
        '200':
          description: Transport details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transport'
              example:
                id: "peppol"
                transportTypeCode: "peppol"
                name: "Peppol AS4 (Storecove)"
                status: "active"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Transport not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Transport not found"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /transports/types:
    get:
      operationId: listTransportTypes
      summary: List transport types
      description: |
        Returns the list of available transport types on the Peppol network
        (e.g., Peppol BIS 3.0, FatturaE). Results are cached for 30 minutes.
      tags: [Transports]
      responses:
        '200':
          description: List of transport types
          content:
            application/json:
              schema:
                type: object
                properties:
                  transportTypes:
                    type: array
                    items:
                      $ref: '#/components/schemas/TransportType'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  # ─── Validation ─────────────────────────────────────────────

  /validate:
    post:
      operationId: validateInvoice
      summary: Validate invoice data (client-side)
      description: |
        Performs client-side validation of invoice data without sending it.
        Checks required fields, line item completeness, and basic format rules.

        This is a fast, local validation. For gateway-side UBL generation checks
        and offline Peppol business-rule validation, use `POST /validate/server`.
      tags: [Validation]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceInput'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientValidationResult'
              example:
                valid: false
                errors:
                  - "Customer country (to.country) is required"
                  - "Line 1: unitPrice is required"
        '400':
          description: Invalid JSON body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /validate/server:
    post:
      operationId: validateInvoiceServer
      summary: Validate invoice data (server-side)
      description: |
        Performs server-side pre-flight validation in the getpeppr gateway without sending
        the invoice to Storecove. The invoice data is checked with the same SDK validation
        engine used by the CLI, converted to UBL XML as a format sanity check, and evaluated
        against getpeppr's offline Peppol business-rule validator.

        This is slower than client-side validation but catches Peppol-specific compliance issues.
        Validation failures return `200` with `valid: false` so SDK clients can inspect all
        errors without exception handling; transport, auth, rate-limit, and malformed-request
        failures still use non-2xx HTTP statuses.

        Embedded attachment content is rejected before UBL generation when the decoded content
        exceeds 2 MB for a single attachment.
      tags: [Validation]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceInput'
      responses:
        '200':
          description: Server validation result with SDK and Peppol business-rule details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerValidationResult'
              example:
                valid: false
                errors:
                  - field: "to.street"
                    message: "Street address is required for the buyer"
                    ruleId: "BR-50"
                    suggestion: "Add to.street, to.city, and to.postalCode for Peppol BG-8 compliance."
                warnings: []
                ubl:
                  valid: true
                  errors: []
                xsd:
                  valid: true
                  errors: []
                  note: "Deprecated compatibility field. This endpoint verifies UBL XML generation but does not run a standalone XSD validator."
                schematron:
                  valid: false
                  errors:
                    - severity: "error"
                      message: "Endpoint ID must use a valid Peppol participant identifier."
                      location: "to.peppolId"
                      ruleId: "PEPPOL-EN16931-R020"
                  warnings: []
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Missing required fields: number, to, and lines are required"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  # ─── Events ─────────────────────────────────────────────────

  /events:
    get:
      operationId: listEvents
      summary: List events
      description: |
        Returns a paginated event log for your account. Events include state changes,
        delivery confirmations, inbound reception, and other lifecycle events.

        Events are scoped to your account **and to the environment of the API key**
        used to make the request — a sandbox key returns only sandbox events, a
        production key only production events.
      tags: [Events]
      parameters:
        - name: invoiceId
          in: query
          description: Filter events by invoice ID
          schema:
            type: string
        - name: dateFrom
          in: query
          description: Filter events from this date (ISO 8601)
          schema:
            type: string
            format: date
          example: "2026-02-01"
        - name: dateTo
          in: query
          description: Filter events until this date (ISO 8601)
          schema:
            type: string
            format: date
          example: "2026-02-28"
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Paginated list of events
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EventEntry'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Specified invoiceId 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'

  # ─── Onboarding ─────────────────────────────────────────────

  /onboarding/legal-entity:
    post:
      operationId: registerLegalEntity
      summary: Register a legal entity for Peppol
      description: |
        Registers (or retrieves) a Storecove Legal Entity linked to your account,
        and optionally registers a Peppol Identifier on it.

        This is the first step before sending invoices — your account must have a
        Legal Entity with at least one Peppol ID to send documents on the Peppol network.

        If a Legal Entity already exists for this account and environment, it is returned
        (idempotent — `200` instead of `201`).
      tags: [Onboarding]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [partyName, line1, city, zip, country]
              properties:
                partyName:
                  type: string
                  description: Company name (2-64 characters)
                  example: "ACMEDIA"
                line1:
                  type: string
                  description: Street address (2-192 characters)
                  example: "Rue de la Loi 42"
                city:
                  type: string
                  description: City (2-64 characters)
                  example: "Brussels"
                zip:
                  type: string
                  description: Postal code (2-32 characters)
                  example: "1000"
                country:
                  type: string
                  description: ISO 3166-1 alpha-2 country code (2 characters)
                  example: "BE"
                peppolScheme:
                  type: string
                  description: 'Peppol scheme code (e.g., "0208" for Belgian BCE). Max 10 characters.'
                  example: "0208"
                peppolIdentifier:
                  type: string
                  description: 'Peppol participant identifier within the scheme. Max 64 characters.'
                  example: "0685660237"
            example:
              partyName: "ACMEDIA"
              line1: "Rue de la Loi 42"
              city: "Brussels"
              zip: "1000"
              country: "BE"
              peppolScheme: "0208"
              peppolIdentifier: "0685660237"
      responses:
        '200':
          description: Legal entity already existed for this account/environment (idempotent)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingResult'
        '201':
          description: Legal entity created and registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingResult'
        '400':
          description: Missing or invalid fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missingField:
                  summary: Missing required field
                  value:
                    error: "partyName is required and must be a string"
                validation:
                  summary: Length validation failure
                  value:
                    error: "Validation failed"
                    details: ["partyName must be at least 2 characters"]
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  # ─── Platform Legal Entities ───────────────────────────────

  /legal-entities:
    post:
      operationId: createLegalEntity
      summary: Create a platform sub-tenant legal entity
      description: |
        Creates a customer as a platform-managed Peppol legal entity. Requires a
        master key with the `legal_entities:create` scope.

        The call is idempotent by `externalId`: replaying the same `externalId`
        returns the existing legal entity with `200`.

        Registry verification runs asynchronously. Listen for `legal_entity.*`
        webhooks or poll `GET /legal-entities/{id}`.
      tags: [Legal Entities]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LegalEntityCreateRequest'
            example:
              externalId: "customer_8412"
              companyName: "Bright Health Ltd"
              country: "GB"
              address:
                line1: "10 King Street"
                city: "London"
                zip: "EC2V 8EA"
              identifier:
                scheme: "GB:CRN"
                value: "12345678"
      responses:
        '200':
          description: Legal entity already existed for this externalId
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegalEntity'
        '202':
          description: Legal entity accepted; verification runs asynchronously
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegalEntity'
        '400':
          description: Missing or invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidExternalId:
                  value:
                    error: "externalId is required and must be a string of 1-64 characters"
                validation:
                  value:
                    error: "Validation failed"
                    details: ["companyName must be at least 2 characters"]
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is not a master key, or lacks legal_entities:create
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Your API key does not have permission to perform this action"
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    get:
      operationId: listLegalEntities
      summary: List platform sub-tenant legal entities
      description: |
        Lists the sub-tenants owned by your platform account, newest first.
        Requires a master key with the `legal_entities:list` scope.
      tags: [Legal Entities]
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Maximum number of legal entities to return
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 100000
            default: 0
          description: Number of legal entities to skip
      responses:
        '200':
          description: Paginated legal entity list
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/LegalEntity'
                  pagination:
                    $ref: '#/components/schemas/LegalEntityPagination'
        '400':
          description: Offset exceeds the maximum allowed value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "offset must not exceed 100000"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is not a master key, or lacks legal_entities:list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Your API key does not have permission to perform this action"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /legal-entities/{id}:
    get:
      operationId: getLegalEntity
      summary: Retrieve a platform sub-tenant legal entity
      description: |
        Retrieves one sub-tenant by getpeppr legal entity id. Unknown, disabled,
        or cross-platform ids return `404` to preserve tenant isolation.
      tags: [Legal Entities]
      parameters:
        - $ref: '#/components/parameters/LegalEntityId'
      responses:
        '200':
          description: Legal entity details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegalEntity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is not a master key, or lacks legal_entities:list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Your API key does not have permission to perform this action"
        '404':
          description: Legal entity not found, disabled, or not owned by this platform
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Legal entity not found"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: archiveLegalEntity
      summary: Archive a platform sub-tenant legal entity
      description: |
        Archives a sub-tenant legal entity. Archived sub-tenants can no longer send.
        Requires a master key with the `legal_entities:archive` scope.
      tags: [Legal Entities]
      parameters:
        - $ref: '#/components/parameters/LegalEntityId'
      responses:
        '200':
          description: Legal entity archived
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegalEntityArchiveResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is not a master key, or lacks legal_entities:archive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Your API key does not have permission to perform this action"
        '404':
          description: Legal entity not found, disabled, or not owned by this platform
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Legal entity not found"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /legal-entities/{id}/attestation:
    post:
      operationId: requestLegalEntityAttestation
      summary: Request sub-tenant attestation
      description: |
        Sends or refreshes the customer authorisation email for a production
        sub-tenant. Requires a master key with the `legal_entities:attest` scope.

        Sandbox sub-tenants are auto-activated and do not use attestation.
      tags: [Legal Entities]
      parameters:
        - $ref: '#/components/parameters/LegalEntityId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LegalEntityAttestationRequest'
            example:
              contactEmail: "owner@brighthealth.co.uk"
              contactName: "Dr Jane Okafor"
      responses:
        '202':
          description: Authorisation email queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegalEntityAttestationResult'
        '400':
          description: Invalid request body, or attestation attempted in sandbox
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                sandbox:
                  value:
                    error: "Attestation applies to production sub-tenants only; sandbox entities are auto-activated"
                invalidEmail:
                  value:
                    error: "contactEmail is required and must be a valid email address"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is not a master key, or lacks legal_entities:attest
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Your API key does not have permission to perform this action"
        '404':
          description: Legal entity not found, disabled, or not owned by this platform
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Legal entity not found"
        '409':
          description: Sub-tenant is not verified yet, has no identifier, or already attested
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                notVerified:
                  value:
                    error: "Sub-tenant identity is not verified yet"
                    reason: "not_verified"
                alreadyAttested:
                  value:
                    error: "Sub-tenant has already attested"
                    reason: "already_attested"
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          description: Authorisation email could not be sent; retry may mint a fresh link
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'

  # ─── Capabilities ──────────────────────────────────────────

  /capabilities:
    get:
      operationId: getCapabilities
      summary: Get provider capabilities
      description: |
        Returns the active provider's capability matrix so SDK consumers can
        discover which operations are supported before calling them.

        The matrix includes support status for invoicing, contacts, bank accounts,
        directory lookups, validation, webhooks, and more.
      tags: [Capabilities]
      responses:
        '200':
          description: Provider capability matrix
          content:
            application/json:
              schema:
                type: object
                properties:
                  provider:
                    type: string
                    description: Active provider name
                    example: "storecove"
                  capabilities:
                    type: object
                    description: Capability matrix (operation → support status and notes)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  # ─── Health ────────────────────────────────────────────────

  /health:
    get:
      operationId: healthCheck
      summary: Health check
      description: |
        Public endpoint (no authentication required). Returns the health status
        of the API gateway, including database and Redis connectivity.

        Used by external uptime monitors (BetterStack, UptimeRobot).
        Returns `200` when healthy or degraded, `503` when the database is unreachable.
      tags: [Health]
      security: []
      responses:
        '200':
          description: Service is healthy or degraded (Redis down but DB up)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthStatus'
              examples:
                healthy:
                  summary: All systems operational
                  value:
                    status: "healthy"
                    checks:
                      db: true
                      redis: true
                degraded:
                  summary: Redis unavailable
                  value:
                    status: "degraded"
                    checks:
                      db: true
                      redis: false
        '503':
          description: Database unreachable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthStatus'
              example:
                status: "unhealthy"
                checks:
                  db: false
                  redis: false

  # ─── Newsletter ─────────────────────────────────────────────

  /newsletter/subscribe:
    post:
      summary: Subscribe to the newsletter
      description: |
        Public endpoint. Creates a pending subscriber row and triggers a double opt-in
        confirmation email. Rate-limited to 5 attempts per IP per hour. CORS open
        (Access-Control-Allow-Origin: *) but the form on getpeppr.dev/newsletter is
        the intended caller.
      operationId: newsletterSubscribe
      tags:
        - Newsletter
      security: []  # Public, no auth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, consent]
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
                  example: marie@example.com
                consent:
                  type: boolean
                  enum: [true]
                  description: Must be exactly true (UK GDPR explicit consent)
                _hp:
                  type: string
                  description: Honeypot field (leave empty — bot trap)
      responses:
        '200':
          description: Subscription accepted (or honeypot triggered, or already-confirmed silent)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [pending]
                  message:
                    type: string
                    example: Check your inbox
        '400':
          description: Invalid email, missing consent, or malformed body
        '429':
          description: Rate limit exceeded (5 attempts per IP per hour)
        '500':
          description: Internal error
    options:
      summary: CORS preflight
      operationId: newsletterSubscribeOptions
      tags:
        - Newsletter
      security: []
      responses:
        '204':
          description: Preflight OK

  /newsletter/confirm:
    get:
      summary: Confirm a newsletter subscription
      description: |
        Called when subscriber clicks the confirmation link in their email.
        Verifies HMAC token, transitions to confirmed (idempotent), sends welcome
        email if not already confirmed, redirects to /newsletter/confirmed on
        success or /newsletter/error on failure.
      operationId: newsletterConfirm
      tags:
        - Newsletter
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: HMAC-signed token from the confirmation email
      responses:
        '302':
          description: Redirect to success or error page
          headers:
            Location:
              schema:
                type: string

  /newsletter/unsubscribe:
    get:
      summary: Render the unsubscribe HTML confirmation page
      description: |
        Returns an HTML confirmation page (RFC 8058 + anti-prefetcher pattern).
        Does NOT mutate state — email scanners (Gmail, Outlook) auto-click links,
        so the actual unsubscribe happens via POST after user clicks the form button.
      operationId: newsletterUnsubscribePage
      tags:
        - Newsletter
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: HTML confirmation page
          content:
            text/html:
              schema:
                type: string
        '400':
          description: Missing or invalid token
    post:
      summary: Perform the newsletter unsubscribe
      description: |
        Performs the unsubscribe mutation. Accepts token via form-encoded body OR
        query param (RFC 8058 List-Unsubscribe-Post one-click). Idempotent.
      operationId: newsletterUnsubscribe
      tags:
        - Newsletter
      security: []
      parameters:
        - name: token
          in: query
          required: false
          schema:
            type: string
          description: Token via query param (RFC 8058 one-click)
      requestBody:
        required: false
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                token:
                  type: string
              description: Token via form body (HTML confirmation page submit)
      responses:
        '302':
          description: Redirect to /newsletter/unsubscribed
        '400':
          description: Missing or invalid token
        '500':
          description: Internal error

components:
  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
      schema:
        type: string
      example: "678"

    BankAccountId:
      name: id
      in: path
      required: true
      description: Bank account ID
      schema:
        type: string
      example: "456"

    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. If a request
        with the same key was already processed, the cached response is returned.
        Keys are scoped to your API key and expire after 24 hours.
      schema:
        type: string
        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:
    # ─── Error Responses ────────────────────────────────────────

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code when available
        message:
          type: string
          description: Additional developer-facing detail when available
        reason:
          type: string
          description: Lifecycle or conflict reason when available
        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"

    # ─── Pagination ─────────────────────────────────────────────

    PaginationMeta:
      type: object
      required: [totalCount, offset, limit, hasMore]
      properties:
        totalCount:
          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
        hasMore:
          type: boolean
          description: Whether more items exist beyond this page
          example: true
        truncated:
          type: boolean
          description: >-
            When true, the server hit its internal fetch limit before applying
            ownership filtering. Some records may be missing from the result.
            Use date filters or more specific query parameters to narrow results.
          example: false

    # ─── Party ──────────────────────────────────────────────────

    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. Standard API keys
        ignore this object; master keys use it to send as a sub-tenant.
      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"

    # ─── Invoice Types ──────────────────────────────────────────

    InvoiceLine:
      type: object
      required: [description, quantity, unitPrice, vatRate]
      properties:
        description:
          type: string
          description: Line item description
          example: "API Integration Setup"
        quantity:
          type: number
          description: Quantity
          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.00
        vatRate:
          type: number
          description: VAT rate in percent
          example: 21
        vatCategory:
          type: string
          enum: [S, Z, E, AE, K, G, O, L, M]
          default: "S"
          description: VAT category code
        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
          description: Base quantity for price calculation (e.g., 100 for "price per 100 units")
          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
          description: Amount (positive number)

    AllowanceCharge:
      type: object
      required: [reason, amount, vatRate]
      properties:
        reason:
          type: string
          description: Reason for the allowance/charge
          example: "Early payment discount"
        amount:
          type: number
          description: Amount (positive, exclusive of tax)
          example: 50.00
        vatRate:
          type: number
          description: VAT rate in percent
          example: 21
        vatCategory:
          type: string
          enum: [S, Z, E, AE, K, G, O, L, M]
          default: "S"

    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 (alternative to embedded content)

    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'
        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'
        lines:
          type: array
          items:
            $ref: '#/components/schemas/InvoiceLine'
          minItems: 1
          maxItems: 500
          description: Line items (at least one required)
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/Attachment'
          maxItems: 10
          description: Supporting documents/attachments. Server-side validation caps embedded content at 2 MB decoded per attachment.
        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: Payment means code (30=credit transfer, 58=SEPA, etc.)
        paymentIban:
          type: string
          description: IBAN for payment
        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: [380, 381, 383, 384, 386, 389, 751]
          description: |
            Invoice type code (UNCL1001):
            - 380: Commercial invoice (default)
            - 381: Credit note
            - 383: Debit note
            - 384: Corrective invoice
            - 386: Prepayment invoice
            - 389: Self-billed invoice
            - 751: Invoice for accounting purposes
        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)
        _draft:
          type: boolean
          description: Set to true to create a draft without sending
          default: false

    ImportInvoiceRequest:
      type: object
      required: [file, filename]
      properties:
        file:
          type: string
          description: Base64-encoded file content
        filename:
          type: string
          description: Original filename (e.g., "invoice.xml")
          example: "invoice-2026-001.xml"

    # ─── Response Schemas ───────────────────────────────────────

    SendResult:
      type: object
      required: [id, status, createdAt]
      properties:
        id:
          type: string
          description: Unique document ID
          example: "12345"
        status:
          $ref: '#/components/schemas/DocumentStatus'
        peppolMessageId:
          type: string
          description: Peppol message ID (assigned once sent)
        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
          example: "2026-02-26T10:30:00Z"

    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`: Delivery failed (final state)
        - `cleared`: Cleared by tax authority (e.g. KSA, PT)
        - `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
      properties:
        id:
          type: string
          description: Invoice ID
        number:
          type: string
          description: Invoice number
        status:
          $ref: '#/components/schemas/DocumentStatus'
        createdAt:
          type: string
          format: date-time

    InvoiceStatus:
      type: object
      description: Full invoice status and details as returned by the provider
      properties:
        id:
          type: string
        number:
          type: string
        status:
          $ref: '#/components/schemas/DocumentStatus'
        createdAt:
          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: 'Peppol rule ID (e.g., "BR-50")'
        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: 'Peppol rule ID (e.g., "BR-CO-17")'

    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]
      properties:
        valid:
          type: boolean
          description: Overall validation result
        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, errors, warnings]
          properties:
            valid:
              type: boolean
              description: Offline Peppol business-rule validation passed
            errors:
              type: array
              items:
                $ref: '#/components/schemas/ServerValidationMessage'
            warnings:
              type: array
              items:
                $ref: '#/components/schemas/ServerValidationMessage'

    # ─── Contact ────────────────────────────────────────────────

    Contact:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
          description: Unique contact ID
          example: "678"
        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
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp
        updatedAt:
          type: string
          format: date-time
          description: Last update timestamp

    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

    # ─── Bank Account ───────────────────────────────────────────

    BankAccount:
      type: object
      required: [id, name, type]
      properties:
        id:
          type: string
          description: Unique bank account ID
          example: "456"
        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 (required when type is "iban")
        number:
          type: string
          description: Account number (required when type is "number")
        bic:
          type: string
          description: BIC/SWIFT code
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code

    # ─── Directory ──────────────────────────────────────────────

    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"

    # ─── Onboarding ─────────────────────────────────────────────

    OnboardingResult:
      type: object
      required: [legalEntityId]
      properties:
        legalEntityId:
          type: string
          description: Legal entity identifier for your account
          example: "12345"
        peppolIdentifier:
          type: object
          nullable: true
          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

    # ─── Platform Legal Entities ───────────────────────────────

    LegalEntityCreateRequest:
      type: object
      required: [externalId, companyName, country, address, identifier]
      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:CRN"
            value:
              type: string
              minLength: 1
              maxLength: 64
              description: Identifier value
              example: "12345678"

    LegalEntity:
      type: object
      required: [id, externalId, companyName, country, identifier, status, environment, createdAt]
      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:CRN"
            value:
              type: string
              example: "12345678"
        status:
          type: string
          enum: [pending, verifying, verified, verification_failed, awaiting_authz, expired, attested, provisioning, active, provisioning_failed, archived]
          description: |
            Public sub-tenant lifecycle status. Production `GET /legal-entities/{id}`
            layers attestation status on top of registry verification.
          example: "active"
        verificationDetail:
          type: object
          properties:
            reason:
              type: string
              enum: [name_mismatch, not_found]
            checkedAt:
              type: string
              format: date-time
        environment:
          type: string
          enum: [sandbox, production]
          example: "production"
        createdAt:
          type: string
          format: date-time
          example: "2026-06-01T10:00:00.000Z"

    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"

    LegalEntityAttestationResult:
      type: object
      required: [id, externalId, status, expiresAt]
      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]
          example: "awaiting_authz"
        expiresAt:
          type: string
          format: date-time
          example: "2026-06-08T10:00:00.000Z"

    # ─── Health ──────────────────────────────────────────────────

    HealthStatus:
      type: object
      required: [status, checks]
      properties:
        status:
          type: string
          enum: [healthy, degraded, unhealthy]
          description: Overall health status
        checks:
          type: object
          required: [db, redis]
          properties:
            db:
              type: boolean
              description: Database connectivity
            redis:
              type: boolean
              description: Redis connectivity

    # ─── Webhook Payload ─────────────────────────────────────────

    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, legal_entity.registered, legal_entity.verification_failed, legal_entity.awaiting_authz, legal_entity.registration_failed, peppol_identifier.verified, peppol_identifier.verification_failed, test.ping, inbound.invoice.received, inbound.creditnote.received]
          description: 'Event type. The inbound.* values (pilot) represent documents received from the Peppol network addressed to your Legal Entity, distinct from the outbound invoice.received status (acknowledgement of a document you sent).'
          example: "invoice.sent"
        data:
          type: object
          description: Event-specific data (e.g., invoiceId, invoiceNumber, environment)
        createdAt:
          type: string
          format: date-time
          description: Event timestamp (ISO 8601)
          example: "2026-02-26T10:30:00.000Z"

    # ─── Transports ─────────────────────────────────────────────

    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"

    # ─── Events ─────────────────────────────────────────────────

    EventEntry:
      type: object
      required: [id, eventType, createdAt]
      properties:
        id:
          type: string
          description: Unique event ID
          example: "evt_abc123"
        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:
      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"

    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          description: Number of seconds to wait before retrying
          schema:
            type: integer
          example: 42
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "Rate limit exceeded. Try again later."

    PayloadTooLarge:
      description: Request body exceeds the 1 MB limit
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "Payload too large. Maximum size: 1024KB"

    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "Internal server error"
