openapi: 3.1.0
info:
  title: EU e-invoice validation API
  version: 0.1.0
  description: >
    Validate structured e-invoices (Peppol BIS Billing 3.0, e-SLOG 2.0) against
    EU national and network rule sets, and check EU VAT numbers via VIES.
    Results are informational; no guarantee of acceptance by any tax authority or
    network. Not legal or tax advice.

    **Site endpoints** (public, unversioned, not part of this API contract):
    `/robots.txt`, `/sitemap.xml`, `/llms.txt`, `/rules`, `/rules/{id}`,
    `/rules/{id}.md`, `/{indexnow-key}.txt`.
  contact:
    name: API owner
    email: no-reply@example.com
servers:
  - url: https://api.example.com
    description: Production (EU region)
tags:
  - name: validation
  - name: vat
  - name: meta

paths:
  /health:
    get:
      tags: [meta]
      summary: Readiness probe (unauthenticated)
      responses:
        "200":
          description: Service is up; component detail included.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
        "503":
          description: Service is not ready (engine and/or database unavailable).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"

  /v1/formats:
    get:
      tags: [meta]
      summary: List supported formats and rule-set versions
      responses:
        "200":
          description: Supported formats.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FormatsResponse"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/validate:
    post:
      tags: [validation]
      summary: Validate an e-invoice XML document
      description: >
        Send raw XML (application/xml or text/xml) or a multipart file upload
        (field `file`). Max 5 MB. The response is 200 whenever validation ran,
        regardless of whether the document is valid.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: format
          schema:
            type: string
            enum: [peppol-bis-3, eslog-2]
          description: Force a format instead of auto-detecting.
        - in: query
          name: ruleset
          schema:
            type: string
          description: Pin a rule-set version (e.g. 2026.5). Default latest.
        - in: query
          name: severityThreshold
          schema:
            type: string
            enum: [fatal, warning]
            default: fatal
          description: Which severity flips `valid` to false.
      requestBody:
        required: true
        content:
          application/xml:
            schema:
              type: string
          text/xml:
            schema:
              type: string
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Validation ran (document may be valid or invalid).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ValidationResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/QuotaExhausted"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/UnprocessableXml"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/vat/{countryCode}/{vatNumber}:
    get:
      tags: [vat]
      summary: Validate an EU VAT number via VIES
      description: >
        Backed by the European Commission VIES service. Upstream outages return
        status `vies_unavailable` (never `invalid`).
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: countryCode
          required: true
          schema: { type: string, minLength: 2, maxLength: 2 }
        - in: path
          name: vatNumber
          required: true
          schema: { type: string }
        - in: query
          name: requesterCountryCode
          schema: { type: string }
        - in: query
          name: requesterVatNumber
          schema: { type: string }
      responses:
        "200":
          description: VIES result (valid / invalid / vies_unavailable).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VatResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/QuotaExhausted"
        "429":
          $ref: "#/components/responses/RateLimited"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key: Authorization: Bearer eiv_live_… (or eiv_test_…)."

  schemas:
    SeverityCounts:
      type: object
      required: [fatal, warning]
      properties:
        fatal: { type: integer }
        warning: { type: integer }

    ValidationError:
      type: object
      required: [ruleId, severity, message, location, layer, docsUrl, messageQuality]
      properties:
        ruleId: { type: string, example: BR-CO-10 }
        severity: { type: string, enum: [fatal, warning] }
        message: { type: string }
        location: { type: string, description: XPath (line/column appended where available). }
        layer: { type: string, enum: [syntax, xsd, en16931, peppol, eslog] }
        specRef: { type: string }
        docsUrl: { type: string, format: uri }
        messageQuality: { type: string, enum: [catalogue, spec] }
        line: { type: integer }
        column: { type: integer }
        test: { type: string }

    ValidationResponse:
      type: object
      required: [valid, format, documentType, rulesetVersion, counts, errors, meta]
      properties:
        valid: { type: boolean }
        format: { type: string, enum: [peppol-bis-3, eslog-2] }
        documentType: { type: string, enum: [invoice, credit-note, unknown] }
        rulesetVersion: { type: string }
        counts: { $ref: "#/components/schemas/SeverityCounts" }
        errors:
          type: array
          items: { $ref: "#/components/schemas/ValidationError" }
        test: { type: boolean, description: Present and true for eiv_test_ keys. }
        meta:
          type: object
          required: [requestId, durationMs]
          properties:
            requestId: { type: string }
            durationMs: { type: integer }
            engine:
              type: object
              properties:
                layersExecuted:
                  type: array
                  items: { type: string }
                layersSkipped:
                  type: array
                  items:
                    type: object
                    properties:
                      layer: { type: string }
                      reason: { type: string }

    FormatsResponse:
      type: object
      properties:
        formats:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              title: { type: string }
              syntax: { type: string }
              specRef: { type: string }
              layers:
                type: array
                items: { type: string }
              rulesets:
                type: array
                items:
                  type: object
                  properties:
                    version: { type: string }
                    releaseDate: { type: string }
                    status: { type: string, enum: [default, available, deprecated] }
                    default: { type: boolean }

    Health:
      type: object
      properties:
        status: { type: string, enum: [ok, unhealthy] }
        ready: { type: boolean }
        time: { type: string, format: date-time }
        version: { type: string }
        components:
          type: object
          properties:
            engine:
              type: object
              properties:
                schematronEngine: { type: string }
                xsdEngine: { type: string }
                rulesetsVendored: { type: boolean }
            db: { type: string }

    VatResponse:
      type: object
      properties:
        status: { type: string, enum: [valid, invalid, vies_unavailable] }
        countryCode: { type: string }
        vatNumber: { type: string }
        name: { type: string }
        address: { type: string }
        consultationNumber: { type: string }
        cachedAt: { type: string, format: date-time }
        retryAfterSeconds: { type: integer }

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, status, message, requestId]
          properties:
            type: { type: string }
            status: { type: integer }
            message: { type: string }
            requestId: { type: string }
          additionalProperties: true

  responses:
    BadRequest:
      description: Not XML / undetectable format / bad params. Carries a `hint`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    QuotaExhausted:
      description: >
        Metered quota exhausted. Body carries plan, cap, limit, resetsAt and
        upgradeUrl (never a bare message).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PayloadTooLarge:
      description: Body exceeds the 5 MB limit.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    UnprocessableXml:
      description: XML not well-formed. Body reports the parse error with line/column.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: >
        Rate limit exceeded (req/s). Body carries limitPerSecond and
        retryAfterSeconds; also sends a Retry-After header. Distinct from quota.
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    InternalError:
      description: Unexpected server error.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
