openapi: 3.1.0
info:
  title: Irish eInvoice Readiness API
  version: 1.0.0
  description: |
    Free, source-linked readiness checks for UBL 2.1, CII D16B, canonical JSON and locally extracted English/Irish PDF invoice drafts.
    Results identify the versioned checks applied and any findings. No API key is currently required.
    Invoice submission and formal certification are outside the API's scope.
    Invoice contents and OCR text are discarded; direct-validation metadata and findings are retained for 90 days by default.
    Public v1 endpoints allow cross-origin browser clients. Validation is limited to 60 requests per source per hour by default.
    Stable client-facing error codes include malformed_xml, malformed_json, unsafe_xml,
    invalid_canonical_invoice, unsupported_xml, unsupported_format, unsupported_media_type,
    missing_invoice, invalid_invoice_profile, invalid_validation_profile, request_too_large,
    rate_limit_exceeded, result_not_found, sample_not_found, rule_not_found,
    validation_engine_unavailable and internal_error.
servers:
  - url: https://invoiceready.ie
paths:
  /api/v1/validate:
    options: {summary: CORS preflight, responses: {'204': {description: Preflight accepted}}}
    post:
      summary: Validate a structured invoice or an extracted PDF interpretation
      description: Subject to configurable per-source limits. PDF uses canonical readiness only and requires validation profile auto. A completed validation returns HTTP 200 even when rule errors are found.
      parameters:
        - name: X-Invoice-Profile
          in: header
          required: false
          description: Select full or simplified Irish VAT invoice checks. Defaults to full.
          schema: {enum: [full, simplified], default: full}
        - name: X-Validation-Profile
          in: header
          required: false
          description: Select auto detection, EN 16931, Peppol Billing, or Peppol Self-Billing document validation. Defaults to auto.
          schema: {enum: [auto, en16931, peppol-bis, peppol-self-billing], default: auto}
      requestBody:
        required: true
        content:
          application/xml: {schema: {type: string}, example: '<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">...</Invoice>'}
          text/xml: {schema: {type: string}}
          application/json: {schema: {$ref: '#/components/schemas/CanonicalInvoice'}}
          application/pdf: {schema: {type: string, format: binary}, description: The extracted interpretation is validated; the PDF is not treated as an EN 16931 syntax.}
          multipart/form-data: {schema: {type: object, required: [invoice], properties: {invoice: {type: string, format: binary}, invoice_profile: {enum: [full, simplified], default: full}, validation_profile: {enum: [auto, en16931, peppol-bis, peppol-self-billing], default: auto}}}}
      responses:
        '200':
          description: Validation completed
          headers:
            X-Request-ID: {schema: {type: string, format: uuid}, description: Correlation identifier for this API request}
            RateLimit-Limit: {schema: {type: integer}, description: Configured validation limit per source and hour}
            RateLimit-Remaining: {schema: {type: integer}, description: Remaining validations in the current hourly window}
            RateLimit-Reset: {schema: {type: integer}, description: Seconds until the hourly window resets}
          content: {application/json: {schema: {$ref: '#/components/schemas/ValidationResponse'}}}
        '400': {$ref: '#/components/responses/Error'}
        '413': {$ref: '#/components/responses/Error'}
        '415': {$ref: '#/components/responses/Error'}
        '422': {$ref: '#/components/responses/Error'}
        '429': {$ref: '#/components/responses/Error'}
        '500': {$ref: '#/components/responses/Error'}
        '503': {$ref: '#/components/responses/Error'}
  /api/v1/extract:
    options: {summary: CORS preflight, responses: {'204': {description: Preflight accepted}}}
    post:
      summary: Extract a reviewable canonical invoice draft from a PDF
      description: Uses embedded text first and local English/Irish OCR for scanned pages. The PDF, OCR text and draft are not persisted. Every result requires human review.
      requestBody:
        required: true
        content:
          application/pdf: {schema: {type: string, format: binary}}
          multipart/form-data: {schema: {type: object, required: [invoice], properties: {invoice: {type: string, format: binary}}}}
      responses:
        '200': {description: Non-persistent draft extracted, content: {application/json: {schema: {$ref: '#/components/schemas/PdfExtractionResponse'}}}}
        '400': {$ref: '#/components/responses/Error'}
        '413': {$ref: '#/components/responses/Error'}
        '415': {$ref: '#/components/responses/Error'}
        '422': {$ref: '#/components/responses/Error'}
        '429': {$ref: '#/components/responses/Error'}
        '503': {$ref: '#/components/responses/Error'}
  /api/v1/workflow/validate:
    options: {summary: CORS preflight, responses: {'204': {description: Preflight accepted}}}
    post:
      summary: Check a Peppol Invoice Response or SBDH envelope
      description: Applies published Invoice Response and enveloping readiness rules. It does not transmit the document or verify Peppol network registration. Document contents are not persisted.
      requestBody:
        required: true
        content:
          application/xml: {schema: {type: string}, example: '<ApplicationResponse xmlns="urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2">...</ApplicationResponse>'}
          text/xml: {schema: {type: string}}
      responses:
        '200': {description: Workflow validation completed, content: {application/json: {schema: {$ref: '#/components/schemas/WorkflowResponse'}}}}
        '413': {$ref: '#/components/responses/Error'}
        '415': {$ref: '#/components/responses/Error'}
        '422': {$ref: '#/components/responses/Error'}
        '429': {$ref: '#/components/responses/Error'}
  /api/v1/samples:
    get:
      summary: List fictitious validation samples
      description: Returns metadata only. Fetch a sample by ID to obtain its XML or JSON content.
      responses:
        '200': {description: Sample catalogue, content: {application/json: {schema: {$ref: '#/components/schemas/SampleList'}}}}
  /api/v1/samples/{id}:
    get:
      summary: Retrieve a fictitious XML or JSON sample
      parameters: [{name: id, in: path, required: true, schema: {type: string}, example: json-arithmetic}]
      responses:
        '200': {description: Sample and expected readiness status, content: {application/json: {schema: {$ref: '#/components/schemas/SampleResponse'}}}}
        '404': {$ref: '#/components/responses/Error'}
  /api/v1/results/{uuid}:
    get:
      summary: Retrieve a retained result
      parameters: [{name: uuid, in: path, required: true, schema: {type: string, format: uuid}}]
      responses: {'200': {description: Retained validation result, content: {application/json: {schema: {$ref: '#/components/schemas/ValidationResponse'}}}}, '404': {$ref: '#/components/responses/Error'}}
  /api/v1/results/compare:
    post:
      summary: Compare two retained validation results
      description: Compares content-free findings only; invoice values are not retained or returned.
      requestBody:
        required: true
        content:
          application/json:
            schema: {type: object, required: [before_validation_id, after_validation_id], properties: {before_validation_id: {type: string, format: uuid}, after_validation_id: {type: string, format: uuid}}}
      responses:
        '200': {description: Resolved, introduced and unchanged findings, content: {application/json: {schema: {type: object}}}}
        '400': {$ref: '#/components/responses/Error'}
        '404': {$ref: '#/components/responses/Error'}
        '415': {$ref: '#/components/responses/Error'}
        '422': {$ref: '#/components/responses/Error'}
  /api/v1/phase1/requirements:
    get:
      summary: Published Phase 1 capability requirements and questions
      responses: {'200': {description: Source-traceable catalogue, content: {application/json: {schema: {$ref: '#/components/schemas/Phase1Catalogue'}}}}}
  /api/v1/phase1/assess:
    post:
      summary: Assess a business or software product against published Phase 1 capabilities
      description: Answers and reports are evaluated synchronously and never persisted. Pending Revenue specifications are reported separately.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subject, answers]
              properties:
                subject: {enum: [business, software_product]}
                answers: {type: object, additionalProperties: {enum: [yes, partial, no, unknown, not_applicable]}}
      responses:
        '200': {description: Non-persistent assessment report, content: {application/json: {schema: {$ref: '#/components/schemas/Phase1Assessment'}}}}
        '413': {$ref: '#/components/responses/Error'}
        '415': {$ref: '#/components/responses/Error'}
        '422': {$ref: '#/components/responses/Error'}
  /api/v1/readiness/requirements:
    get:
      summary: Phase 1 and Phase 2 capability requirements
      description: Returns source-linked published requirements, existing cross-border VAT conditions, and specifications that remain pending.
      responses:
        '200': {description: Versioned readiness catalogue, content: {application/json: {schema: {$ref: '#/components/schemas/ReadinessCatalogue'}}}}
  /api/v1/readiness/assess:
    post:
      summary: Assess Phase 1, Phase 2, or combined readiness
      description: Answers and reports are evaluated synchronously and never persisted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subject, phase, answers]
              properties:
                subject: {enum: [business, software_product]}
                phase: {enum: [phase1, phase2, both]}
                answers: {type: object, additionalProperties: {enum: [yes, partial, no, unknown, not_applicable]}}
            example: {subject: business, phase: both, answers: {P1-SCOPE-01: yes, P2-SCOPE-01: yes, P2-SCOPE-02: partial}}
      responses:
        '200': {description: Non-persistent phase-aware assessment, content: {application/json: {schema: {$ref: '#/components/schemas/ReadinessAssessment'}}}}
        '413': {$ref: '#/components/responses/Error'}
        '415': {$ref: '#/components/responses/Error'}
        '422': {$ref: '#/components/responses/Error'}
  /api/v1/rulesets:
    get: {summary: List active versioned rulesets, responses: {'200': {description: Rulesets, content: {application/json: {schema: {$ref: '#/components/schemas/RulesetList'}}}}}}
  /api/v1/rulesets/changelog:
    get: {summary: Ruleset release history, responses: {'200': {description: Versioned ruleset changes, content: {application/json: {schema: {type: object}}}}}}
  /api/v1/standards:
    get: {summary: Current standard editions, deployed artefacts and scheduled reviews, responses: {'200': {description: Versioned standards status, content: {application/json: {schema: {$ref: '#/components/schemas/StandardsResponse'}}}}}}
  /api/v1/test-packs:
    get: {summary: List downloadable conformance scenario packs, responses: {'200': {description: Test packs, content: {application/json: {schema: {$ref: '#/components/schemas/TestPackCatalogue'}}}}}}
  /api/v1/test-packs/core-2026.08:
    get: {summary: Retrieve the core 2026.08 scenario manifest, responses: {'200': {description: Test scenario manifest, content: {application/json: {schema: {$ref: '#/components/schemas/TestPackManifest'}}}}}}
  /api/v1/rules:
    get:
      summary: Search and list validation rules
      parameters:
        - {name: ruleset, in: query, schema: {type: string}}
        - {name: profile, in: query, schema: {enum: [full, simplified]}}
        - {name: q, in: query, schema: {type: string}}
        - {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
        - {name: per_page, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 50}}
      responses: {'200': {description: Paginated source-linked rule catalogue, content: {application/json: {schema: {$ref: '#/components/schemas/RulePage'}}}}}
  /api/v1/rules/{code}:
    get:
      summary: Retrieve one validation rule
      parameters: [{name: code, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Rule metadata, content: {application/json: {schema: {$ref: '#/components/schemas/RuleResponse'}}}}, '404': {$ref: '#/components/responses/Error'}}
  /api/v1/capabilities:
    get: {summary: List formats, limits, retention, and CORS policy, responses: {'200': {description: Capabilities, content: {application/json: {schema: {$ref: '#/components/schemas/Capabilities'}}}}}}
  /api/v1/vat/verify:
    post:
      summary: Verify one EU VAT number using European Commission VIES
      description: This explicit lookup is separate from invoice scoring. Values and responses are not persisted or logged by InvoiceReady.ie. Validity is point-in-time and does not prove a transaction.
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/VatVerificationRequest'}, example: {vat_number: IE6388047V}}}}
      responses:
        '200': {description: Valid, invalid, or undetermined result, content: {application/json: {schema: {$ref: '#/components/schemas/VatVerificationResponse'}}}}
        '400': {$ref: '#/components/responses/Error'}
        '413': {$ref: '#/components/responses/Error'}
        '415': {$ref: '#/components/responses/Error'}
        '422': {$ref: '#/components/responses/Error'}
        '429': {$ref: '#/components/responses/Error'}
        '503': {$ref: '#/components/responses/Error'}
  /api/v1/vat/status:
    get:
      summary: Report VIES availability
      responses: {'200': {description: Briefly cached content-free upstream state, content: {application/json: {schema: {$ref: '#/components/schemas/VatServiceStatus'}}}}, '503': {$ref: '#/components/responses/Error'}}
  /api/v1/health:
    get: {summary: Lightweight service health, responses: {'200': {description: Health, content: {application/json: {schema: {$ref: '#/components/schemas/Health'}}}}, '503': {description: Required component unavailable, content: {application/json: {schema: {$ref: '#/components/schemas/Health'}}}}}}
  /api/v1/schema/canonical-invoice:
    get:
      summary: Published canonical JSON Schema
      responses:
        '200': {description: JSON Schema 2020-12 document, content: {application/schema+json: {schema: {type: object}}}}
components:
  responses:
    Error:
      description: API request error. A 429 response also includes Retry-After.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          examples:
            unsupportedMedia: {value: {error: {code: unsupported_media_type, message: 'Use XML, canonical JSON, or multipart form data.', request_id: '89a52c69-83fa-4fb2-a41c-fb258f30693f'}}}
            invalidCanonical: {value: {error: {code: invalid_canonical_invoice, message: 'The JSON does not satisfy the published canonical invoice structure.', request_id: '89a52c69-83fa-4fb2-a41c-fb258f30693f', details: [{field: '$.lines', message: 'The data must match the object type'}]}}}
  schemas:
    CanonicalInvoice:
      type: object
      description: Canonical readiness input. Field types and formats are enforced; missing invoice content is returned as readiness findings. Decimal strings are recommended to preserve exact sender values; JSON numbers remain accepted for v1 compatibility.
      required: [invoice, supplier, customer, lines, totals]
      properties:
        invoice: {type: object, properties: {number: {type: string}, issueDate: {type: string, format: date}, supplyDate: {type: string, format: date}, periodStart: {type: string, format: date}, periodEnd: {type: string, format: date}, dueDate: {type: string, format: date}, currency: {type: string, pattern: '^[A-Z]{3}$'}, taxCurrency: {type: string, pattern: '^[A-Z]{3}$'}, type: {type: string}, creditNoteReason: {type: string}, customizationId: {type: string}, profileId: {type: string}}}
        supplier: {$ref: '#/components/schemas/Party'}
        customer: {$ref: '#/components/schemas/Party'}
        payee: {$ref: '#/components/schemas/Party'}
        lines: {type: array, items: {type: object, properties: {id: {type: string}, description: {type: string}, quantity: {$ref: '#/components/schemas/Decimal'}, unit: {type: string}, unitPrice: {$ref: '#/components/schemas/Decimal'}, priceBaseQuantity: {$ref: '#/components/schemas/Decimal'}, priceBaseUnit: {type: string}, netAmount: {$ref: '#/components/schemas/Decimal'}, vatCategory: {type: string}, vatRate: {$ref: '#/components/schemas/Decimal'}, standardItemId: {type: string}, standardItemIdScheme: {type: string}, classificationCode: {type: string}, classificationScheme: {type: string}, orderLineReference: {type: string}, invoicedObjectId: {type: string}, invoicedObjectScheme: {type: string}, allowances: {type: array, items: {$ref: '#/components/schemas/Adjustment'}}, charges: {type: array, items: {$ref: '#/components/schemas/Adjustment'}}}}}
        allowances: {type: array, items: {$ref: '#/components/schemas/Adjustment'}}
        charges: {type: array, items: {$ref: '#/components/schemas/Adjustment'}}
        vatBreakdown: {type: array, items: {$ref: '#/components/schemas/VatBreakdown'}}
        totals: {$ref: '#/components/schemas/Totals'}
        payment: {type: object, properties: {method: {type: string}, iban: {type: string}, reference: {type: string}, terms: {type: string}, mandateReference: {type: string}, payerAccount: {type: string}, creditorIdentifier: {type: string}, cardPrimaryAccountNumber: {type: string}, cardHolderName: {type: string}}}
        references: {type: object, properties: {buyerReference: {type: string}, accountingCost: {type: string}, purchaseOrder: {type: string}, contract: {type: string}, previousInvoice: {type: string}}}
        delivery: {type: object, properties: {locationId: {type: string}, locationScheme: {type: string}, name: {type: string}, address: {type: string}, city: {type: string}, postalCode: {type: string}, countrySubdivision: {type: string}, country: {type: string}}}
        supportingDocuments: {type: array, items: {type: object, properties: {id: {type: string}, description: {type: string}, mimeCode: {type: string}, filename: {type: string}, externalUrl: {type: string}}}}
        taxTreatment: {type: object}
        notes: {type: array, items: {type: string}}
    Decimal: {oneOf: [{type: string, pattern: '^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$'}, {type: number}]}
    Adjustment: {type: object, required: [amount], properties: {amount: {$ref: '#/components/schemas/Decimal'}, baseAmount: {$ref: '#/components/schemas/Decimal'}, percentage: {$ref: '#/components/schemas/Decimal'}, reason: {type: string}, reasonCode: {type: string}, vatCategory: {type: string}, vatRate: {$ref: '#/components/schemas/Decimal'}}}
    VatBreakdown: {type: object, properties: {category: {type: string}, rate: {$ref: '#/components/schemas/Decimal'}, taxableAmount: {$ref: '#/components/schemas/Decimal'}, vatAmount: {$ref: '#/components/schemas/Decimal'}, exemptionReason: {type: string}, exemptionReasonCode: {type: string}}}
    Totals: {type: object, properties: {lineNetTotal: {$ref: '#/components/schemas/Decimal'}, allowancesTotal: {$ref: '#/components/schemas/Decimal'}, chargesTotal: {$ref: '#/components/schemas/Decimal'}, taxExclusiveTotal: {$ref: '#/components/schemas/Decimal'}, vatTotal: {$ref: '#/components/schemas/Decimal'}, taxCurrencyVatTotal: {$ref: '#/components/schemas/Decimal'}, taxInclusiveTotal: {$ref: '#/components/schemas/Decimal'}, prepaidAmount: {$ref: '#/components/schemas/Decimal'}, roundingAmount: {$ref: '#/components/schemas/Decimal'}, amountDue: {$ref: '#/components/schemas/Decimal'}}}
    Party: {type: object, properties: {name: {type: string}, tradingName: {type: string}, address: {type: string}, addressLines: {type: array, items: {type: string}}, city: {type: string}, postalCode: {type: string}, countrySubdivision: {type: string}, country: {type: string}, vatNumber: {type: string}, legalRegistrationId: {type: string}, legalRegistrationScheme: {type: string}, partyId: {type: string}, partyIdScheme: {type: string}, electronicAddress: {type: string}, electronicAddressScheme: {type: string}}}
    Sample:
      type: object
      required: [id, name, syntax, description, expected_status, invoice_profile, validation_profile, content_type]
      properties:
        id: {type: string, example: json-arithmetic}
        name: {type: string, example: 'JSON: incorrect line arithmetic'}
        syntax: {type: string, example: Canonical JSON}
        description: {type: string}
        expected_status: {type: string, example: needs_attention}
        invoice_profile: {enum: [full, simplified]}
        validation_profile: {enum: [auto, en16931, peppol-bis, peppol-self-billing]}
        content_type: {enum: [application/xml, application/json, application/pdf]}
        content_encoding: {enum: [utf-8, base64]}
        filename: {type: [string, 'null']}
        content: {type: string, description: Present only on the single-sample endpoint}
    SampleList:
      type: object
      required: [api_version, samples]
      properties:
        api_version: {const: '1'}
        samples: {type: array, items: {$ref: '#/components/schemas/Sample'}}
    SampleResponse:
      type: object
      required: [api_version, sample]
      properties:
        api_version: {const: '1'}
        sample: {$ref: '#/components/schemas/Sample'}
    ValidationResponse:
      type: object
      required: [validation_id, api_version, created_at, input, rulesets, summary, issues]
      properties:
        validation_id: {type: string, format: uuid}
        api_version: {const: '1'}
        created_at: {type: string, format: date-time}
        input: {type: object, required: [format, syntax, size, invoice_profile], properties: {format: {type: string}, syntax: {type: string}, size: {type: integer}, invoice_profile: {enum: [full, simplified]}}}
        rulesets: {type: array, items: {type: object, required: [name, version]}}
        summary:
          type: object
          required: [status, score, score_basis, passed, warnings, errors, info, undetermined]
          properties:
            status: {enum: [checks_passed, review_recommended, needs_attention]}
            score: {type: integer, minimum: 0, maximum: 100}
            score_basis: {const: implemented_published_checks}
            passed: {type: integer, minimum: 0}
            warnings: {type: integer, minimum: 0}
            errors: {type: integer, minimum: 0}
            info: {type: integer, minimum: 0}
            undetermined: {type: integer, minimum: 0}
        issues: {type: array, items: {$ref: '#/components/schemas/Issue'}}
        checks: {type: array, description: Transient passing and not-applicable native checks; retained results may omit this collection, items: {$ref: '#/components/schemas/Issue'}}
        validation_profile:
          type: object
          description: Requested, detected, and applied technical profile plus authoritative artefact versions.
          properties:
            invoice_profile: {enum: [full, simplified]}
            technical_profile_requested: {enum: [auto, en16931, peppol-bis, peppol-self-billing, legacy]}
            technical_profile_detected: {enum: [canonical-readiness, en16931, peppol-bis, peppol-self-billing, legacy]}
            technical_profile_applied: {enum: [canonical-readiness, en16931, peppol-bis, peppol-self-billing, legacy]}
            peppol_self_billing: {type: object, required: [applied, version], properties: {applied: {type: boolean}, version: {type: [string, 'null']}}}
        coverage: {type: object, description: Completed checks and specifications still pending publication}
        extraction: {$ref: '#/components/schemas/PdfExtractionMetadata'}
    PdfExtractionMetadata:
      type: object
      required: [engine_version, method, languages, page_count, confidence, confidence_band, status, fields, warnings]
      properties:
        engine_version: {type: string, example: 1.1.0}
        method: {enum: [embedded_text, ocr, mixed]}
        languages: {type: array, items: {enum: [eng, gle]}}
        page_count: {type: integer, minimum: 1, maximum: 10}
        confidence: {type: number, minimum: 0, maximum: 1}
        confidence_band: {enum: [low, medium, high]}
        status: {const: review_required}
        fields: {type: array, items: {type: object, required: [field, confidence, method], properties: {field: {type: string}, confidence: {type: number, minimum: 0, maximum: 1}, method: {enum: [embedded_text, ocr, mixed]}}}}
        warnings: {type: array, items: {type: object, required: [code, message], properties: {code: {type: string}, message: {type: string}, field: {type: string}}}}
    PdfExtractionResponse:
      type: object
      required: [request_id, api_version, created_at, input, draft, extraction, privacy]
      properties:
        request_id: {type: string, format: uuid}
        api_version: {const: '1'}
        created_at: {type: string, format: date-time}
        input: {type: object, required: [format, size, pages], properties: {format: {const: pdf}, size: {type: integer}, pages: {type: integer}}}
        draft: {$ref: '#/components/schemas/PdfCanonicalDraft'}
        extraction: {$ref: '#/components/schemas/PdfExtractionMetadata'}
        privacy: {type: object, required: [persisted], properties: {persisted: {const: false}}}
    PdfCanonicalDraft:
      type: object
      description: A potentially incomplete canonical draft. It can be submitted as CanonicalInvoice after review; missing content is returned as readiness findings.
      required: [invoice, supplier, customer, lines, totals]
      properties:
        invoice: {type: object}
        supplier: {type: object}
        customer: {type: object}
        lines: {type: array, items: {type: object}}
        totals: {type: object}
        payment: {type: object}
        payee: {type: object}
        references: {type: object}
        taxTreatment: {type: object}
        notes: {type: array, items: {type: string}}
        vatBreakdown: {type: array, items: {type: object}}
    Issue: {type: object, required: [rule, ruleset, severity, title, message, rule_url], properties: {rule: {type: string}, ruleset: {type: string}, severity: {enum: [pass, error, warning, info, undetermined, not_applicable]}, title: {type: string}, message: {type: string}, field: {type: [string, 'null']}, source: {type: [string, 'null']}, rule_url: {type: string}, details: {type: array, items: {type: object, required: [field, message], properties: {field: {type: string}, message: {type: string}}}}}}
    Error: {type: object, required: [error], properties: {error: {type: object, required: [code, message, request_id], properties: {code: {type: string, enum: [malformed_xml, malformed_json, malformed_pdf, empty_document, unsafe_xml, encrypted_pdf, pdf_page_limit_exceeded, pdf_not_interpretable, pdf_extraction_failed, pdf_extraction_timeout, pdf_output_limit_exceeded, extraction_busy, extraction_engine_unavailable, invalid_canonical_invoice, invalid_workflow_document, invalid_vat_request, invalid_vat_number, unsupported_xml, unsupported_format, unsupported_media_type, missing_invoice, invalid_invoice_profile, invalid_validation_profile, unsupported_validation_profile, request_too_large, rate_limit_exceeded, result_not_found, sample_not_found, rule_not_found, invalid_assessment, validation_engine_unavailable, vies_unavailable, internal_error]}, message: {type: string}, request_id: {type: string, format: uuid}, details: {type: array, description: Content-free field-level diagnostics when available, items: {type: object, required: [field, message], properties: {field: {type: string}, message: {type: string}}}}}}}}
    VatVerificationRequest: {type: object, required: [vat_number], additionalProperties: false, properties: {vat_number: {type: string, description: VAT number with two-letter state prefix}, requester_vat_number: {type: string, description: Optional requester number for a VIES request identifier}}}
    VatVerificationResponse:
      type: object
      required: [verification_id, api_version, checked_at, source, query, result, privacy, scope]
      properties:
        verification_id: {type: string, format: uuid}
        api_version: {const: '1'}
        checked_at: {type: string, format: date-time}
        source: {type: object}
        query: {type: object}
        result: {type: object, properties: {status: {enum: [valid, invalid, undetermined]}, valid: {type: [boolean, 'null']}, reason_code: {type: [string, 'null']}, request_date: {type: [string, 'null']}, request_identifier: {type: [string, 'null']}, name: {type: [string, 'null']}, address: {type: [string, 'null']}}}
        privacy: {type: object, properties: {values_persisted: {const: false}, values_logged: {const: false}}}
        scope: {type: object, properties: {invoice_score_affected: {const: false}, transaction_proven: {const: false}}}
    VatServiceStatus: {type: object, required: [api_version, checked_at, service, status], properties: {api_version: {const: '1'}, checked_at: {type: string, format: date-time}, service: {type: string}, status: {enum: [available, partially_available, unavailable]}, countries: {type: object}}}
    WorkflowResponse:
      type: object
      required: [api_version, created_at, input, rulesets, summary, issues, workflow, coverage]
      properties:
        api_version: {const: '1'}
        created_at: {type: string, format: date-time}
        input: {type: object, required: [syntax], properties: {syntax: {enum: [peppol-invoice-response, peppol-sbdh]}}}
        rulesets: {type: array, minItems: 1, items: {type: object, required: [name, version], properties: {name: {const: peppol-workflow}, version: {const: 2026.08.1}}}}
        summary: {type: object, required: [status, errors, warnings], properties: {status: {enum: [checks_passed, needs_attention]}, errors: {type: integer, minimum: 0}, warnings: {type: integer, minimum: 0}}}
        issues: {type: array, items: {type: object, required: [rule, ruleset, severity, title, message, field, source]}}
        workflow: {type: object, required: [specification]}
        coverage: {type: object, required: [transport_performed, document_contents_persisted], properties: {transport_performed: {const: false}, document_contents_persisted: {const: false}}}
    StandardsResponse:
      type: object
      required: [api_version, registry_version, generated_at, baseline, next_review, sources, last_automated_check]
      properties:
        api_version: {const: '1'}
        registry_version: {type: string}
        generated_at: {type: string, format: date-time}
        baseline: {type: object, required: [standard, validation_artefacts, peppol_bis_billing, peppol_self_billing, peppol_workflow]}
        next_review: {type: object}
        sources: {type: array, items: {type: object, required: [name, url]}}
        last_automated_check: {type: [object, 'null']}
    TestPackCatalogue:
      type: object
      required: [api_version, packs]
      properties: {api_version: {const: '1'}, packs: {type: array, items: {type: object, required: [id, name, version, description, download_url, manifest_url]}}}
    TestPackManifest:
      type: object
      required: [api_version, id, version, generated_at, scenarios]
      properties: {api_version: {const: '1'}, id: {type: string}, version: {type: string}, generated_at: {type: string, format: date-time}, scenarios: {type: array, items: {type: object, required: [sample_id, file, expected_status, focus]}}}
    RulesetList:
      type: object
      required: [api_version, rulesets]
      properties:
        api_version: {const: '1'}
        rulesets: {type: array, items: {$ref: '#/components/schemas/Ruleset'}}
    Source:
      type: object
      required: [name, url]
      properties: {id: {type: string}, name: {type: string}, url: {type: string}, accessed: {type: string, format: date}}
    Ruleset:
      type: object
      required: [name, version, title, description, source]
      properties: {name: {type: string}, version: {type: string}, title: {type: string}, description: {type: string}, source: {$ref: '#/components/schemas/Source'}}
    Pagination:
      type: object
      required: [page, per_page, total]
      properties: {page: {type: integer, minimum: 1}, per_page: {type: integer, minimum: 1, maximum: 100}, total: {type: integer, minimum: 0}}
    RuleMetadata:
      type: object
      required: [code, title, description, severity, ruleset, ruleset_version, profiles, source, applicability, remediation, rule_url]
      properties:
        code: {type: string, example: CODE-001}
        title: {type: string}
        description: {type: string}
        severity: {enum: [error, warning, info]}
        ruleset: {type: string}
        ruleset_version: {type: string}
        profiles: {type: array, items: {enum: [full, simplified]}}
        effective_from: {type: [string, 'null'], format: date}
        effective_to: {type: [string, 'null'], format: date}
        source: {$ref: '#/components/schemas/Source'}
        applicability: {type: string}
        remediation: {type: string}
        rule_url: {type: string}
    RulePage:
      type: object
      required: [api_version, pagination, rules]
      properties: {api_version: {const: '1'}, pagination: {$ref: '#/components/schemas/Pagination'}, rules: {type: array, items: {$ref: '#/components/schemas/RuleMetadata'}}}
    RuleResponse:
      type: object
      required: [api_version, rule]
      properties: {api_version: {const: '1'}, rule: {$ref: '#/components/schemas/RuleMetadata'}}
    Capabilities:
      type: object
      required: [api_version, formats, invoice_profiles, validation_profiles, controlled_codes, consistency, arithmetic, content_types, canonical_json_schema, max_upload_bytes, rate_limit_validations_per_hour, retention_days, cors]
      properties:
        api_version: {const: '1'}
        formats: {type: array, items: {type: string}}
        invoice_profiles: {type: array, items: {enum: [full, simplified]}}
        validation_profiles: {type: array, items: {enum: [auto, en16931, peppol-bis, peppol-self-billing]}}
        controlled_codes: {type: object, required: [ruleset_version, lists], properties: {ruleset_version: {const: 2026.08.4}, lists: {type: array, items: {type: string}}}}
        consistency: {type: object, required: [ruleset_version, checks], properties: {ruleset_version: {const:  1.3.1}, checks: {type: array, items: {type: string}}}}
        arithmetic: {type: object, required: [ruleset_version, decimal_engine, tolerance, checks], properties: {ruleset_version: {const: 1.2.0}, decimal_engine: {const: brick/math}, tolerance: {const: '0.01'}, checks: {type: array, items: {type: string}}}}
        content_types: {type: array, items: {type: string}}
        canonical_json_schema: {const: /api/v1/schema/canonical-invoice}
        max_upload_bytes: {type: integer}
        rate_limit_validations_per_hour: {type: integer}
        retention_days: {type: integer}
        cors: {type: string}
    Health:
      type: object
      required: [status, api_version, database, en16931_engine, en16931_version, peppol_engine, peppol_version, self_billing_engine, self_billing_version, vies_configured, vies_status_endpoint]
      properties:
        status: {enum: [ok, degraded]}
        api_version: {const: '1'}
        database: {enum: [available, unavailable]}
        en16931_engine: {enum: [available, unavailable]}
        en16931_version: {type: string}
        peppol_engine: {enum: [available, unavailable]}
        peppol_version: {type: string}
        self_billing_engine: {enum: [available, unavailable]}
        self_billing_version: {type: string}
        vies_configured: {type: boolean}
        vies_status_endpoint: {const: /api/v1/vat/status}
    Phase1Assessment:
      type: object
      required: [assessment_id, api_version, subject, scope, summary, findings, coverage, privacy]
      properties:
        assessment_id: {type: string, format: uuid}
        api_version: {const: '1'}
        subject: {enum: [business, software_product]}
        scope: {type: object}
        summary: {type: object}
        findings: {type: array, items: {type: object}}
        coverage: {type: object}
        privacy: {type: object, properties: {answers_persisted: {const: false}, result_persisted: {const: false}}}
    Requirement:
      type: object
      required: [code, title, category, kind, subjects, status]
      properties: {code: {type: string}, phase: {enum: [phase1, phase2]}, title: {type: string}, category: {type: string}, kind: {type: string}, subjects: {type: array, items: {enum: [business, software_product]}}, status: {enum: [published, published-outline, pending]}, question: {type: [string, 'null']}, source: {$ref: '#/components/schemas/Source'}}
    Phase1Catalogue:
      type: object
      required: [ruleset, source, answer_values, requirements]
      properties:
        ruleset: {type: object, required: [name, version], properties: {name: {const: ireland-phase1-capability}, version: {type: string}}}
        source: {$ref: '#/components/schemas/Source'}
        answer_values: {type: array, items: {enum: [yes, partial, no, unknown, not_applicable]}}
        requirements: {type: array, items: {$ref: '#/components/schemas/Requirement'}}
    ReadinessCatalogue:
      type: object
      required: [api_version, catalogue_version, phases, answer_values, sources, requirements]
      properties:
        api_version: {const: '1'}
        catalogue_version: {type: string}
        phases: {type: array, items: {enum: [phase1, phase2]}}
        answer_values: {type: array, items: {type: string}}
        sources: {type: array, items: {$ref: '#/components/schemas/Source'}}
        requirements: {type: array, items: {$ref: '#/components/schemas/Requirement'}}
    ReadinessAssessment:
      type: object
      required: [assessment_id, api_version, subject, phase, rulesets, scopes, summary, findings, coverage, privacy]
      properties:
        assessment_id: {type: string, format: uuid}
        api_version: {const: '1'}
        subject: {enum: [business, software_product]}
        phase: {enum: [phase1, phase2, both]}
        rulesets: {type: array, items: {type: object}}
        scopes: {type: object}
        summary: {type: object}
        findings: {type: array, items: {type: object}}
        coverage: {type: object}
        privacy: {type: object, properties: {answers_persisted: {const: false}, result_persisted: {const: false}}}
