openapi: 3.2.0 info: title: Ironfang Finance E-Invoicing Generation API version: 1.4.0 summary: Generate UBL invoices and validate Invoice or CreditNote XML with PHIVE. description: 'Ironfang Finance validates UBL e-invoices against the exact EN 16931 and Peppol BIS Billing rule releases published by the standards bodies, and returns a result that names the immutable ruleset it used so the outcome can be reproduced later.' contact: name: Ironfang email: hello@ironfang.uk url: https://ironfang.com/finance termsOfService: https://ironfang.com/legal/terms license: name: Proprietary identifier: LicenseRef-Ironfang-Proprietary servers: - url: https://api.ironfang.com/finance description: Production. Paths below are relative to this product prefix. security: - {} - platformKey: [] tags: - name: generation description: Deterministic JSON-to-UBL generation, validated before success. paths: /v1/einvoices/generate: post: tags: - generation operationId: generateEInvoice summary: Generate and validate one UBL e-invoice description: 'Accepts generation-input/v1 JSON with decimal strings. Both ruleset (latest or an exact immutable ID) and profile are required; document_type comes from the body. Unknown or repeated query parameters, duplicate JSON keys, unknown fields, nulls, JSON numbers and inconsistent calculated totals are refused before acceptance. UTF-8 application/json only; uncompressed body at most 5 MiB. The operation pins the selected ruleset, generator/schema versions and engine image at acceptance. Only the exact generated XML bytes validated by that engine can succeed. Every validation layer must pass. Generated XML which fails the rules is an internal 500 generation_failed, indeterminate and uncharged; no artifact or customer-invalid verdict is returned. Dependency failures are 503/504. Validation has a 10-second deadline and result persistence a separate 5-second budget. Authenticated requests require finance:einvoices:write and optionally Idempotency-Key. The key is scoped to the organisation and generation endpoint. Input identity is financewolf-json/1: UTF-8 JSON decoded and re-encoded with sorted object keys, compact separators and Go JSON string escaping. Whitespace, object key order and equivalent string escapes do not change identity; decimal string spelling and array order do. An existing key replays the original bytes without execution or a second usage event even after latest or the deployed generator changes; different content or selectors produce 409. Anonymous requests cannot supply Idempotency-Key or a tenant header and create no stored operation or usage event. The successful response contains base64 of the exact XML, its SHA-256 and a nested validation verdict. One authenticated success records one einvoice.generation usage event. Source JSON is never retained. Authenticated generated XML and validation findings are retained together for 30 days, retrievable/deletable through /v1/einvoices/results/{id}; anonymous results are not retained. Deletion/expiry erases artifacts and findings but preserves the operation and usage identity, so a replay returns 410 and never runs or charges again. Responses use Cache-Control: no-store and the standard request correlation headers.' security: - {} - platformKey: [] x-ironfang-scopes: - finance:einvoices:write parameters: - $ref: '#/components/parameters/IdempotencyKey' - name: ruleset in: query required: true schema: $ref: '#/components/schemas/RulesetSelector' - name: profile in: query required: true schema: type: string enum: - peppol-bis-billing-3 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/GenerationRequest' responses: '200': description: Only a fully validated artifact, after atomic authenticated persistence. headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/json: schema: $ref: '#/components/schemas/GenerationResult' '400': $ref: '#/components/responses/MalformedRequest' '401': $ref: '#/components/responses/Unauthenticated' '402': $ref: '#/components/responses/PaymentRequired' '403': $ref: '#/components/responses/ScopeOrEligibility' '409': $ref: '#/components/responses/IdempotencyConflict' '410': $ref: '#/components/responses/ValidationGone' '413': $ref: '#/components/responses/PayloadTooLarge' '415': $ref: '#/components/responses/UnsupportedContentType' '422': description: Generation selection refused. headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - unknown_ruleset - unsupported_profile - unsupported_document_type status: const: 422 '429': $ref: '#/components/responses/ValidationRateLimited' '500': description: Indeterminate generation failure; no artifact is returned and nothing is charged. headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - required: - outcome - usage_charged properties: code: enum: - internal_error - generation_failed status: const: 500 '503': $ref: '#/components/responses/ValidatorUnavailable' '504': $ref: '#/components/responses/ValidationTimeout' components: schemas: BillingRefusal: type: object description: 'On a billing refusal only: the product and meter the billing account refused, and when the free allowance renews (the start of the next UTC month) where that is the reason.' additionalProperties: false properties: product: type: string enum: - finance meter: type: string enum: - finance.validation - finance.generation reset_at: type: string format: date-time GenerationDefId: type: string minLength: 1 maxLength: 128 pattern: "^[^--�-\uFFFF]*$" allOf: - pattern: \S - pattern: ^\S(?:[\s\S]*\S)?$ RequestID: type: string pattern: ^[A-Za-z0-9_-]{8,64}$ ProfileFamily: type: string description: A supported profile family. V1 supports Peppol BIS Billing 3. enum: - peppol-bis-billing-3 GenerationDefIdentifier: type: object additionalProperties: false properties: value: $ref: '#/components/schemas/GenerationDefId' scheme: type: string pattern: ^[A-Za-z0-9-]{1,16}$ required: - value FindingCounts: type: object additionalProperties: false required: - by_severity - by_layer properties: by_severity: type: object additionalProperties: false required: - fatal - warning properties: fatal: type: integer minimum: 0 warning: type: integer minimum: 0 by_layer: type: object additionalProperties: false required: - input - xml - xsd - en16931 - peppol properties: input: type: integer minimum: 0 xml: type: integer minimum: 0 xsd: type: integer minimum: 0 en16931: type: integer minimum: 0 peppol: type: integer minimum: 0 GenerationDefText: type: string minLength: 1 maxLength: 1024 pattern: "^[^--�-\uFFFF]*$" allOf: - pattern: \S Timestamp: type: string format: date-time description: RFC 3339, UTC. ValidationTiming: type: object additionalProperties: false required: - started_at - completed_at - duration_ms properties: started_at: $ref: '#/components/schemas/Timestamp' completed_at: $ref: '#/components/schemas/Timestamp' duration_ms: type: integer minimum: 0 maximum: 10000 description: Wall time, bounded by the 10-second deadline. ImageDigest: type: string description: The pinned validator engine image, as a sha256 digest. pattern: ^sha256:[0-9a-f]{64}$ GenerationDefNote: type: string minLength: 1 maxLength: 2000 pattern: "^[^--�-\uFFFF]*$" allOf: - pattern: \S FindingMessage: type: string description: 'One of Ironfang''s fixed finding messages, selected by the finding''s rule id (for the validator''s own FW-* rules) or by its layer. No message text from the validation engine or a rule set is ever returned: the rule id and the sanitised location carry the detail. The union below is exactly the closed catalogue (services/finance-validator/corpus/finding-messages.json); a result carrying any other message is refused before it is stored or written.' enum: - A Peppol BIS Billing 3.0 rule failed at this location. - An EN 16931 business rule failed at this location. - The document could not be read as XML within the contract limits. - The document does not conform to the UBL 2.1 schema at this location. - The document encoding, declaration or structure is not accepted. - The document has more elements than the contract limit of 250,000. - The document nests elements deeper than the contract limit of 64. - The input is empty or larger than the contract limit of 5,242,880 bytes. - The input was refused by the contract limits before parsing. LayerStatus: type: string enum: - passed - failed - skipped - error GenerationDefAmount: type: string pattern: ^-?(0|[1-9][0-9]{0,14})(\.[0-9]{1,2})?$ maxLength: 23 GenerationDefDocumentAdjustment: type: object additionalProperties: false properties: amount: $ref: '#/components/schemas/GenerationDefUnsignedAmount' reason: $ref: '#/components/schemas/GenerationDefText' reason_code: $ref: '#/components/schemas/GenerationDefId' base_amount: $ref: '#/components/schemas/GenerationDefUnsignedAmount' percentage: $ref: '#/components/schemas/GenerationDefUnsignedDecimal' tax: $ref: '#/components/schemas/GenerationDefTax' required: - amount - reason - tax dependentRequired: base_amount: - percentage percentage: - base_amount GenerationDefCountry: type: string pattern: ^[A-Z]{2}$ ValidationInput: type: object additionalProperties: false required: - sha256 - bytes - content_type - document_type properties: sha256: $ref: '#/components/schemas/SHA256' description: SHA-256 of the exact bytes received. bytes: type: integer minimum: 0 maximum: 5242880 content_type: type: string enum: - application/xml - text/xml document_type: type: string description: The document type found at the root element; `unknown` when the root was not recognised or the XML did not parse. enum: - invoice - credit_note - unknown GenerationDefAddress: type: object additionalProperties: false properties: street: $ref: '#/components/schemas/GenerationDefName' additional_street: $ref: '#/components/schemas/GenerationDefName' address_line: $ref: '#/components/schemas/GenerationDefName' city: $ref: '#/components/schemas/GenerationDefName' postal_code: $ref: '#/components/schemas/GenerationDefId' region: $ref: '#/components/schemas/GenerationDefName' country: $ref: '#/components/schemas/GenerationDefCountry' required: - country ProfileDetection: type: object description: The profile family the document was validated as, and how that was decided. additionalProperties: false required: - family - source properties: family: $ref: '#/components/schemas/ProfileFamily' source: type: string description: '`requested` (the request named the profile or an exact ruleset) or `detected` (from the document''s identifiers).' enum: - requested - detected customization_id: type: string description: The document's `CustomizationID`, a profile identifier - not invoice content. Present when parsed. maxLength: 256 profile_id: type: string description: The document's `ProfileID`. Present when parsed. maxLength: 256 GenerationDefDecimal: type: string pattern: ^-?(0|[1-9][0-9]{0,14})(\.[0-9]{1,6})?$ maxLength: 23 GenerationDefUnsignedDecimal: type: string pattern: ^(0|[1-9][0-9]{0,14})(\.[0-9]{1,6})?$ maxLength: 23 LayerResult: type: object additionalProperties: false required: - layer - status - fatal - warning properties: layer: $ref: '#/components/schemas/Layer' status: $ref: '#/components/schemas/LayerStatus' reason: $ref: '#/components/schemas/LayerReason' description: Required when `status` is `skipped` or `error`; absent otherwise. fatal: type: integer minimum: 0 description: Fatal findings this layer produced. warning: type: integer minimum: 0 allOf: - if: properties: status: enum: - skipped - error then: required: - reason properties: fatal: const: 0 warning: const: 0 else: not: required: - reason - if: properties: status: const: failed then: properties: fatal: minimum: 1 - if: properties: status: const: passed then: properties: fatal: const: 0 EngineInfo: type: object description: What ran, exactly, so the result can be reproduced. additionalProperties: false required: - name - image_digest properties: name: type: string description: The validator engine and version, e.g. `phive/12.1.0`. minLength: 1 maxLength: 64 image_digest: $ref: '#/components/schemas/ImageDigest' hint_pack_version: type: string description: The version of Ironfang's hint mappings applied, when any. Nonblank when present. minLength: 1 maxLength: 32 GenerationResult: type: object additionalProperties: false required: - schema - status - outcome - input - generator - artifact - validation properties: schema: type: string const: financewolf/einvoice/generation-result/v1 operation_id: $ref: '#/components/schemas/OpaqueID' status: type: string const: completed outcome: type: string const: valid input: type: object additionalProperties: false required: - sha256 - bytes - content_type - document_type - canonicalization - schema_version properties: sha256: $ref: '#/components/schemas/SHA256' description: SHA-256 of the exact bytes received. bytes: type: integer minimum: 0 maximum: 31457280 content_type: type: string const: application/json document_type: type: string enum: - invoice - credit_note canonicalization: type: string const: financewolf-json/1 schema_version: type: string const: financewolf/einvoice/generation-input/v1 generator: type: object additionalProperties: false required: - version - layer - status properties: version: type: string pattern: ^financewolf-ubl/[0-9]+[.][0-9]+[.][0-9]+$ layer: type: string const: generator status: type: string const: passed artifact: type: object additionalProperties: false required: - content_type - sha256 - bytes - data_base64 properties: content_type: type: string const: application/xml sha256: type: string pattern: ^[0-9a-f]{64}$ bytes: type: integer minimum: 1 maximum: 5242880 data_base64: type: string contentEncoding: base64 minLength: 4 maxLength: 6990508 pattern: ^[A-Za-z0-9+/]+={0,2}$ validation: allOf: - $ref: '#/components/schemas/ValidationVerdict' - properties: outcome: type: string const: valid layers: items: properties: status: type: string const: passed profile: properties: source: type: string const: requested usage: $ref: '#/components/schemas/Usage' dependentRequired: operation_id: - usage usage: - operation_id RulesetID: type: string description: An immutable ruleset identifier. Never reused, never edited. pattern: ^fwrs_[a-z0-9_]{1,80}$ Finding: type: object description: One normalised rule result. The official rule id and severity are preserved; message and location are bounded and never carry invoice values. Hints are Ironfang's own, versioned by `engine.hint_pack_version`, and may be absent. additionalProperties: false required: - severity - layer - rule_id - message - location_kind - ruleset_id properties: severity: $ref: '#/components/schemas/Severity' layer: $ref: '#/components/schemas/Layer' rule_id: type: string description: The official rule identifier, e.g. `PEPPOL-EN16931-R020`, `BR-CO-10`, or `cvc-complex-type.2.4.a` for schema errors. minLength: 1 maxLength: 64 pattern: ^[A-Za-z0-9._:-]+$ message: $ref: '#/components/schemas/FindingMessage' location: type: string description: A bounded, sanitised location. Absent when `location_kind` is `none`. minLength: 1 maxLength: 256 location_kind: $ref: '#/components/schemas/LocationKind' hint_code: type: string pattern: ^[a-z0-9-]{1,80}$ hint: type: string minLength: 1 maxLength: 512 source_url: $ref: '#/components/schemas/HTTPSURL' description: The official specification or rule source for this rule, when known. ruleset_id: $ref: '#/components/schemas/RulesetID' allOf: - if: properties: location_kind: const: none then: not: required: - location else: required: - location - if: required: - hint then: required: - hint_code GenerationRequest: title: Ironfang Finance e-invoice generation input v1 description: Deterministic UBL 2.1 Invoice (380) or CreditNote (381) input. Schema validation does not assert Peppol validity. Decimal strings are exact; amount reconciliation is an additional generator check. type: object additionalProperties: false properties: input_schema_version: const: financewolf/einvoice/generation-input/v1 document_type: enum: - invoice - credit_note id: $ref: '#/components/schemas/GenerationDefId' issue_date: $ref: '#/components/schemas/GenerationDefDate' due_date: $ref: '#/components/schemas/GenerationDefDate' tax_point_date: $ref: '#/components/schemas/GenerationDefDate' currency: type: string pattern: ^[A-Z]{3}$ buyer_reference: $ref: '#/components/schemas/GenerationDefId' purchase_order_reference: $ref: '#/components/schemas/GenerationDefId' contract_reference: $ref: '#/components/schemas/GenerationDefId' preceding_invoices: type: array items: $ref: '#/components/schemas/GenerationDefReference' minItems: 1 maxItems: 100 seller: $ref: '#/components/schemas/GenerationDefParty' buyer: $ref: '#/components/schemas/GenerationDefParty' lines: type: array items: $ref: '#/components/schemas/GenerationDefLine' minItems: 1 maxItems: 2000 allowances: type: array items: $ref: '#/components/schemas/GenerationDefDocumentAdjustment' minItems: 1 maxItems: 100 charges: type: array items: $ref: '#/components/schemas/GenerationDefDocumentAdjustment' minItems: 1 maxItems: 100 payment: $ref: '#/components/schemas/GenerationDefPayment' payment_terms: $ref: '#/components/schemas/GenerationDefText' delivery: $ref: '#/components/schemas/GenerationDefDelivery' note: $ref: '#/components/schemas/GenerationDefNote' accounting_cost: $ref: '#/components/schemas/GenerationDefId' prepaid_amount: $ref: '#/components/schemas/GenerationDefUnsignedAmount' rounding_amount: $ref: '#/components/schemas/GenerationDefAmount' totals: $ref: '#/components/schemas/GenerationDefTotals' tax_subtotals: type: array items: $ref: '#/components/schemas/GenerationDefTaxSubtotal' minItems: 1 maxItems: 100 required: - input_schema_version - document_type - id - issue_date - currency - seller - buyer - lines anyOf: - required: - buyer_reference - required: - purchase_order_reference allOf: - if: properties: document_type: const: credit_note then: not: required: - due_date GenerationDefPayment: type: object additionalProperties: false properties: code: enum: - '10' - '30' - '58' - '97' reference: $ref: '#/components/schemas/GenerationDefId' account: $ref: '#/components/schemas/GenerationDefAccount' required: - code allOf: - if: properties: code: enum: - '30' - '58' then: required: - account else: not: required: - account LayerReason: type: string description: 'Why a layer did not run to a verdict. `skipped_after_failure`: an earlier layer failed and this one cannot execute safely. `not_applicable`: the layer does not exist for the selected profile. `engine_error`: the validator failed inside this layer. `deadline_exceeded`: the deadline passed during this layer.' enum: - skipped_after_failure - not_applicable - engine_error - deadline_exceeded GenerationDefTotals: type: object additionalProperties: false properties: line_net_amount: $ref: '#/components/schemas/GenerationDefAmount' allowance_total: $ref: '#/components/schemas/GenerationDefAmount' charge_total: $ref: '#/components/schemas/GenerationDefAmount' tax_exclusive_amount: $ref: '#/components/schemas/GenerationDefAmount' tax_amount: $ref: '#/components/schemas/GenerationDefAmount' tax_inclusive_amount: $ref: '#/components/schemas/GenerationDefAmount' prepaid_amount: $ref: '#/components/schemas/GenerationDefAmount' rounding_amount: $ref: '#/components/schemas/GenerationDefAmount' payable_amount: $ref: '#/components/schemas/GenerationDefAmount' required: - line_net_amount - allowance_total - charge_total - tax_exclusive_amount - tax_amount - tax_inclusive_amount - prepaid_amount - rounding_amount - payable_amount FieldError: type: object additionalProperties: false required: - field - code - message properties: field: type: string description: The option or parameter at fault, e.g. `ruleset`, `options.document_type`. minLength: 1 maxLength: 128 code: type: string enum: - required - invalid_value - unknown_field - too_long message: type: string minLength: 1 maxLength: 256 RulesetSelector: type: string description: '`latest`, or an exact ruleset id in any validatable state.' default: latest pattern: ^(latest|fwrs_[a-z0-9_]{1,80})$ GenerationDefUnsignedAmount: type: string pattern: ^(0|[1-9][0-9]{0,14})(\.[0-9]{1,2})?$ maxLength: 23 GenerationDefTaxSubtotal: type: object additionalProperties: false properties: tax: $ref: '#/components/schemas/GenerationDefTax' taxable_amount: $ref: '#/components/schemas/GenerationDefAmount' tax_amount: $ref: '#/components/schemas/GenerationDefAmount' required: - tax - taxable_amount - tax_amount GenerationDefLineAdjustment: type: object additionalProperties: false properties: amount: $ref: '#/components/schemas/GenerationDefUnsignedAmount' reason: $ref: '#/components/schemas/GenerationDefText' reason_code: $ref: '#/components/schemas/GenerationDefId' base_amount: $ref: '#/components/schemas/GenerationDefUnsignedAmount' percentage: $ref: '#/components/schemas/GenerationDefUnsignedDecimal' required: - amount - reason dependentRequired: base_amount: - percentage percentage: - base_amount SHA256: type: string description: Lowercase hex SHA-256. pattern: ^[0-9a-f]{64}$ GenerationDefAccount: type: object additionalProperties: false properties: id: $ref: '#/components/schemas/GenerationDefId' name: $ref: '#/components/schemas/GenerationDefName' branch_id: $ref: '#/components/schemas/GenerationDefId' required: - id FindingsSummary: type: object description: Truncation is explicit. `returned` is the length of `findings`; `total` is how many exist; `cap` is the inline maximum; `truncated` is `total > returned`, and a truncated result returns exactly `cap`. additionalProperties: false required: - returned - total - cap - truncated properties: returned: type: integer minimum: 0 maximum: 1000 total: type: integer minimum: 0 cap: type: integer const: 1000 truncated: type: boolean allOf: - if: properties: truncated: const: true then: properties: returned: const: 1000 Outcome: type: string description: The standard's verdict. `indeterminate` means Ironfang Finance could not decide; it is never converted to `invalid` and never billed. enum: - valid - invalid - indeterminate GenerationDefName: type: string minLength: 1 maxLength: 256 pattern: "^[^--�-\uFFFF]*$" allOf: - pattern: \S ValidationResult: type: object description: 'The general result model: what a validation produced, whether or not it reached a verdict. Retained deliberately broad so a stored or asynchronous operation (later slices) can represent an indeterminate run. The synchronous `200` response uses the narrower `ValidationVerdict`.' additionalProperties: false required: - schema - status - outcome - input - ruleset - profile - layers - counts - findings - findings_summary - timing - engine properties: schema: type: string description: The version of this result document. const: financewolf/einvoice/validation-result/v1 operation_id: $ref: '#/components/schemas/OpaqueID' description: Present for authenticated requests, which create an operation record. Absent for anonymous validation. document_id: $ref: '#/components/schemas/OpaqueID' description: Present when the authenticated organisation's retention policy stored the document. Absent otherwise. status: $ref: '#/components/schemas/OperationStatus' outcome: $ref: '#/components/schemas/Outcome' input: $ref: '#/components/schemas/ValidationInput' ruleset: $ref: '#/components/schemas/RulesetSelection' profile: $ref: '#/components/schemas/ProfileDetection' layers: type: array description: Every layer, always in this order, always present. minItems: 5 maxItems: 5 prefixItems: - allOf: - $ref: '#/components/schemas/LayerResult' - properties: layer: const: input - allOf: - $ref: '#/components/schemas/LayerResult' - properties: layer: const: xml - allOf: - $ref: '#/components/schemas/LayerResult' - properties: layer: const: xsd - allOf: - $ref: '#/components/schemas/LayerResult' - properties: layer: const: en16931 - allOf: - $ref: '#/components/schemas/LayerResult' - properties: layer: const: peppol items: $ref: '#/components/schemas/LayerResult' counts: $ref: '#/components/schemas/FindingCounts' findings: type: array description: Normalised findings in deterministic order. Empty, never null. maxItems: 1000 items: $ref: '#/components/schemas/Finding' findings_summary: $ref: '#/components/schemas/FindingsSummary' timing: $ref: '#/components/schemas/ValidationTiming' engine: $ref: '#/components/schemas/EngineInfo' links: $ref: '#/components/schemas/ResultLinks' usage: $ref: '#/components/schemas/Usage' allOf: - if: properties: status: const: completed then: properties: outcome: enum: - valid - invalid else: properties: outcome: const: indeterminate - if: properties: outcome: const: valid then: properties: layers: items: properties: status: enum: - passed - skipped counts: properties: by_severity: properties: fatal: const: 0 - if: properties: outcome: const: invalid then: properties: layers: contains: properties: status: const: failed items: properties: status: enum: - passed - failed - skipped counts: properties: by_severity: properties: fatal: minimum: 1 - if: properties: outcome: const: indeterminate then: properties: layers: contains: properties: status: const: error usage: properties: charged: const: false - if: anyOf: - required: - usage - required: - document_id - required: - links then: required: - operation_id - if: properties: ruleset: properties: selection_method: const: exact then: properties: ruleset: properties: requested: pattern: ^fwrs_ state: enum: - scheduled - sendable - historical profile: properties: source: const: requested - if: properties: ruleset: properties: requested: pattern: ^fwrs_ then: properties: ruleset: properties: selection_method: const: exact - if: properties: ruleset: properties: selection_method: enum: - latest - detected then: properties: ruleset: properties: requested: const: latest state: const: sendable - if: properties: ruleset: properties: selection_method: const: latest then: properties: profile: properties: source: const: requested - if: properties: ruleset: properties: selection_method: const: detected then: properties: profile: properties: source: const: detected - if: properties: findings: contains: required: - hint then: properties: engine: required: - hint_pack_version RulesetState: type: string description: Public lifecycle state of a release that may be validated against. `sendable` is current and is what `latest` resolves to; `scheduled` is the next release, not yet in force, validatable for migration testing; `historical` has been superseded but stays validatable for reproduction. Withdrawn (`disabled`) releases answer `410` and never appear in a body; candidate releases are internal and never appear. enum: - scheduled - sendable - historical ResultLinks: type: object description: Authorised links, present only on authenticated results whose retention policy keeps them. Time-limited. additionalProperties: false properties: report: $ref: '#/components/schemas/HTTPSURL' evidence: $ref: '#/components/schemas/HTTPSURL' GenerationDefReference: type: object additionalProperties: false properties: id: $ref: '#/components/schemas/GenerationDefId' issue_date: $ref: '#/components/schemas/GenerationDefDate' required: - id GenerationDefDate: type: string format: date pattern: ^[1-9][0-9]{3}-[0-9]{2}-[0-9]{2}$ OperationStatus: type: string description: How far execution got, independent of the verdict. enum: - completed - error GenerationDefParty: type: object additionalProperties: false properties: name: $ref: '#/components/schemas/GenerationDefName' trading_name: $ref: '#/components/schemas/GenerationDefName' endpoint: $ref: '#/components/schemas/GenerationDefEndpoint' identifier: $ref: '#/components/schemas/GenerationDefIdentifier' legal_identifier: $ref: '#/components/schemas/GenerationDefIdentifier' vat_identifier: $ref: '#/components/schemas/GenerationDefId' tax_registration_identifier: $ref: '#/components/schemas/GenerationDefId' address: $ref: '#/components/schemas/GenerationDefAddress' contact: $ref: '#/components/schemas/GenerationDefContact' required: - name - endpoint - address Layer: type: string description: A validation layer, in execution order. enum: - input - xml - xsd - en16931 - peppol GenerationDefTax: type: object additionalProperties: false properties: category: enum: - S - Z - E - AE - G - K - O - L - M rate: $ref: '#/components/schemas/GenerationDefUnsignedDecimal' exemption_reason: $ref: '#/components/schemas/GenerationDefText' exemption_code: type: string pattern: ^VATEX-[A-Z0-9-]{1,32}$ required: - category allOf: - if: properties: category: const: O then: not: required: - rate else: required: - rate GenerationDefItem: type: object additionalProperties: false properties: name: $ref: '#/components/schemas/GenerationDefName' description: $ref: '#/components/schemas/GenerationDefText' seller_id: $ref: '#/components/schemas/GenerationDefId' buyer_id: $ref: '#/components/schemas/GenerationDefId' standard_id: $ref: '#/components/schemas/GenerationDefStandardItemId' required: - name HTTPSURL: type: string format: uri description: 'A provenance URL under one narrow grammar: https, a lowercase DNS host of at least two labels, no userinfo, no IP literal, an optional port 1..65535, an optional path, query and fragment of unreserved and sub-delimiter characters or valid percent-escapes, no whitespace. The same expression is held by the registry review, the database and this document.' pattern: ^https://[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*\.[a-z]([a-z0-9-]{0,61}[a-z0-9])?(:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(/([A-Za-z0-9._~!$&()*+,;=:@/-]|%[0-9A-Fa-f]{2})*)?(\?([A-Za-z0-9._~!$&()*+,;=:@/?-]|%[0-9A-Fa-f]{2})*)?(#([A-Za-z0-9._~!$&()*+,;=:@/?-]|%[0-9A-Fa-f]{2})*)?$ GenerationDefStandardItemId: type: object additionalProperties: false properties: value: $ref: '#/components/schemas/GenerationDefId' scheme: type: string pattern: ^[0-9]{4}$ required: - value - scheme RulesetSelection: type: object description: What was asked for and what was used. additionalProperties: false required: - requested - id - selection_method - state properties: requested: $ref: '#/components/schemas/RulesetSelector' description: The selector the request carried (defaulted to `latest` if absent). An exact selector equals `id`. id: $ref: '#/components/schemas/RulesetID' description: The immutable ruleset the document was validated against. selection_method: $ref: '#/components/schemas/SelectionMethod' state: $ref: '#/components/schemas/RulesetState' description: The release's state at validation time. `latest` and `detected` always report `sendable`; `exact` reports whichever validatable state the release is in. vesid: type: string description: The engine's coordinate for the release, for cross-referencing official tooling. maxLength: 128 official_release: type: string maxLength: 128 OpaqueID: type: string description: An opaque Ironfang identifier. minLength: 1 maxLength: 64 pattern: ^[A-Za-z0-9_-]+$ LocationKind: type: string description: 'The vocabulary the validator and the findings store use: `none`, `xpath`, or `line-column`.' enum: - none - xpath - line-column SelectionMethod: type: string description: 'How the ruleset was chosen: `exact` (the request named the id), `latest` (resolved from the requested profile), `detected` (profile detected from the document, then resolved as latest).' enum: - exact - latest - detected Severity: type: string description: '`fatal` findings make the document invalid; `warning` findings never do and are never promoted.' enum: - fatal - warning GenerationDefContact: type: object additionalProperties: false properties: name: $ref: '#/components/schemas/GenerationDefName' telephone: $ref: '#/components/schemas/GenerationDefId' email: $ref: '#/components/schemas/GenerationDefName' required: [] minProperties: 1 Problem: type: object description: An RFC 9457 problem. `code` is the stable, machine-readable Ironfang identifier; `type` is its URL and `status` its one HTTP status, both fixed per code below. `detail` is safe to show and never carries parser output, invoice content or stack traces. Validation findings are never Problems. additionalProperties: false required: - type - title - status - detail - code - request_id properties: type: type: string format: uri pattern: ^https://ironfang\.com/problems/[a-z_]+$ title: type: string minLength: 1 maxLength: 128 status: type: integer minimum: 400 maximum: 599 detail: type: string minLength: 1 maxLength: 512 instance: type: string format: uri-reference maxLength: 256 code: type: string description: The stable Ironfang problem code. enum: - malformed_request - unauthenticated - insufficient_scope - idempotency_conflict - payload_too_large - unsupported_content_type - unknown_ruleset - ruleset_disabled - result_gone - unsupported_profile - profile_undetected - unsupported_document_type - not_found - rate_limited - quota_exhausted - billing_moved - free_allowance_exhausted - account_budget_exhausted - product_budget_exhausted - exemption_limit_reached - paid_usage_paused - payment_required - payment_action_required - account_restricted - account_closed - product_not_eligible - billing_temporarily_unavailable - validation_timeout - validator_unavailable - jobs_unavailable - delivery_unavailable - generation_failed - report_unavailable - report_incomplete - render_unavailable - render_timeout - internal_error request_id: $ref: '#/components/schemas/RequestID' errors: type: array description: Field-level errors, on `malformed_request` only. Bounded; empty is omitted. minItems: 1 maxItems: 50 items: $ref: '#/components/schemas/FieldError' operation_id: $ref: '#/components/schemas/OpaqueID' description: On an indeterminate validation problem only, the operation record that was opened, if any. outcome: type: string const: indeterminate usage_charged: type: boolean const: false retry_after_seconds: type: integer minimum: 1 maximum: 3600 description: Only on problems that invite a retry (`rate_limited`, `quota_exhausted`, `validator_unavailable`, `billing_temporarily_unavailable`). billing: $ref: '#/components/schemas/BillingRefusal' allOf: - if: properties: code: const: malformed_request then: properties: status: const: 400 type: const: https://ironfang.com/problems/malformed_request - if: properties: code: const: unauthenticated then: properties: status: const: 401 type: const: https://ironfang.com/problems/unauthenticated - if: properties: code: const: insufficient_scope then: properties: status: const: 403 type: const: https://ironfang.com/problems/insufficient_scope - if: properties: code: const: not_found then: properties: status: const: 404 type: const: https://ironfang.com/problems/not_found - if: properties: code: const: idempotency_conflict then: properties: status: const: 409 type: const: https://ironfang.com/problems/idempotency_conflict - if: properties: code: const: result_gone then: properties: status: const: 410 type: const: https://ironfang.com/problems/result_gone - if: properties: code: const: ruleset_disabled then: properties: status: const: 410 type: const: https://ironfang.com/problems/ruleset_disabled - if: properties: code: const: payload_too_large then: properties: status: const: 413 type: const: https://ironfang.com/problems/payload_too_large - if: properties: code: const: unsupported_content_type then: properties: status: const: 415 type: const: https://ironfang.com/problems/unsupported_content_type - if: properties: code: const: unknown_ruleset then: properties: status: const: 422 type: const: https://ironfang.com/problems/unknown_ruleset - if: properties: code: const: unsupported_profile then: properties: status: const: 422 type: const: https://ironfang.com/problems/unsupported_profile - if: properties: code: const: profile_undetected then: properties: status: const: 422 type: const: https://ironfang.com/problems/profile_undetected - if: properties: code: const: unsupported_document_type then: properties: status: const: 422 type: const: https://ironfang.com/problems/unsupported_document_type - if: properties: code: const: rate_limited then: properties: status: const: 429 type: const: https://ironfang.com/problems/rate_limited - if: properties: code: const: billing_moved then: properties: status: const: 410 type: const: https://ironfang.com/problems/billing_moved - if: properties: code: const: free_allowance_exhausted then: properties: status: const: 429 type: const: https://ironfang.com/problems/free_allowance_exhausted - if: properties: code: const: account_budget_exhausted then: properties: status: const: 429 type: const: https://ironfang.com/problems/account_budget_exhausted - if: properties: code: const: product_budget_exhausted then: properties: status: const: 429 type: const: https://ironfang.com/problems/product_budget_exhausted - if: properties: code: const: exemption_limit_reached then: properties: status: const: 429 type: const: https://ironfang.com/problems/exemption_limit_reached - if: properties: code: const: paid_usage_paused then: properties: status: const: 402 type: const: https://ironfang.com/problems/paid_usage_paused - if: properties: code: const: payment_required then: properties: status: const: 402 type: const: https://ironfang.com/problems/payment_required - if: properties: code: const: payment_action_required then: properties: status: const: 402 type: const: https://ironfang.com/problems/payment_action_required - if: properties: code: const: account_restricted then: properties: status: const: 402 type: const: https://ironfang.com/problems/account_restricted - if: properties: code: const: account_closed then: properties: status: const: 410 type: const: https://ironfang.com/problems/account_closed - if: properties: code: const: product_not_eligible then: properties: status: const: 403 type: const: https://ironfang.com/problems/product_not_eligible - if: properties: code: const: billing_temporarily_unavailable then: properties: status: const: 503 type: const: https://ironfang.com/problems/billing_temporarily_unavailable - if: properties: code: const: quota_exhausted then: properties: status: const: 429 type: const: https://ironfang.com/problems/quota_exhausted - if: properties: code: const: report_unavailable then: properties: status: const: 503 type: const: https://ironfang.com/problems/report_unavailable - if: properties: code: const: report_incomplete then: properties: status: const: 422 type: const: https://ironfang.com/problems/report_incomplete - if: properties: code: const: render_unavailable then: properties: status: const: 503 type: const: https://ironfang.com/problems/render_unavailable - if: properties: code: const: render_timeout then: properties: status: const: 504 type: const: https://ironfang.com/problems/render_timeout - if: properties: code: const: generation_failed then: required: - outcome - usage_charged properties: status: const: 500 type: const: https://ironfang.com/problems/generation_failed - if: properties: code: const: internal_error then: properties: status: const: 500 type: const: https://ironfang.com/problems/internal_error - if: properties: code: const: delivery_unavailable then: properties: status: const: 503 type: const: https://ironfang.com/problems/delivery_unavailable - if: properties: code: const: jobs_unavailable then: properties: status: const: 503 type: const: https://ironfang.com/problems/jobs_unavailable - if: properties: code: const: validator_unavailable then: properties: status: const: 503 type: const: https://ironfang.com/problems/validator_unavailable - if: properties: code: const: validation_timeout then: properties: status: const: 504 type: const: https://ironfang.com/problems/validation_timeout - if: properties: code: enum: - validation_timeout - validator_unavailable then: required: - outcome - usage_charged - if: not: properties: code: enum: - validation_timeout - validator_unavailable - internal_error - generation_failed then: not: anyOf: - required: - outcome - required: - usage_charged - required: - operation_id - if: anyOf: - required: - outcome - required: - usage_charged - required: - operation_id then: required: - outcome - usage_charged - if: required: - errors then: properties: code: const: malformed_request - if: required: - retry_after_seconds then: properties: code: enum: - rate_limited - quota_exhausted - validator_unavailable - jobs_unavailable - delivery_unavailable - billing_temporarily_unavailable - if: required: - billing then: properties: code: enum: - free_allowance_exhausted - account_budget_exhausted - product_budget_exhausted - exemption_limit_reached - paid_usage_paused - payment_required - payment_action_required - account_restricted - account_closed - product_not_eligible - billing_temporarily_unavailable GenerationDefLine: type: object additionalProperties: false properties: id: $ref: '#/components/schemas/GenerationDefId' quantity: $ref: '#/components/schemas/GenerationDefDecimal' unit: type: string pattern: ^[A-Z0-9]{2,3}$ price_amount: $ref: '#/components/schemas/GenerationDefUnsignedDecimal' price_base_quantity: $ref: '#/components/schemas/GenerationDefUnsignedDecimal' net_amount: $ref: '#/components/schemas/GenerationDefAmount' item: $ref: '#/components/schemas/GenerationDefItem' tax: $ref: '#/components/schemas/GenerationDefTax' allowances: type: array items: $ref: '#/components/schemas/GenerationDefLineAdjustment' minItems: 1 maxItems: 100 charges: type: array items: $ref: '#/components/schemas/GenerationDefLineAdjustment' minItems: 1 maxItems: 100 note: $ref: '#/components/schemas/GenerationDefText' order_line_reference: $ref: '#/components/schemas/GenerationDefId' accounting_cost: $ref: '#/components/schemas/GenerationDefId' required: - id - quantity - unit - price_amount - item - tax Usage: type: object description: Present only on authenticated results. Never reported for anonymous validation, which has no entitlement to report against. additionalProperties: false required: - charged - credits properties: charged: type: boolean description: Whether this validation consumed entitlement. Always `false` for indeterminate outcomes. credits: type: integer minimum: 0 description: Units of the operation's meter this operation used - 1 when charged, otherwise 0. remaining: type: integer minimum: 0 description: Only on results recorded before usage billing, returned as they were recorded. The month's usage is read at GET /v1/einvoices/usage. period_ends_at: $ref: '#/components/schemas/Timestamp' description: Only on results recorded before usage billing, returned as they were recorded. GenerationDefDelivery: type: object additionalProperties: false properties: date: $ref: '#/components/schemas/GenerationDefDate' address: $ref: '#/components/schemas/GenerationDefAddress' required: - date - address ValidationVerdict: description: 'The `200` response of the validate operation: a `ValidationResult` that reached a verdict. `status` is `completed`, `outcome` is `valid` or `invalid`, and no layer is in `error`. Indeterminate results are not representable here; they are Problems.' allOf: - $ref: '#/components/schemas/ValidationResult' - properties: status: const: completed outcome: enum: - valid - invalid layers: items: properties: status: enum: - passed - failed - skipped GenerationDefEndpoint: type: object additionalProperties: false properties: value: $ref: '#/components/schemas/GenerationDefId' scheme: type: string pattern: ^[0-9]{4}$ required: - value - scheme parameters: IdempotencyKey: name: Idempotency-Key in: header required: false description: Optional on authenticated requests. The same key with the same request replays the original operation; the same key with a different request (different document bytes, ruleset, profile or document type) is `409 idempotency_conflict`. Scoped to the organisation and the validation endpoint. Anonymous requests never use it. schema: type: string minLength: 8 maxLength: 128 pattern: ^[A-Za-z0-9_-]+$ responses: UnsupportedContentType: description: The body is not XML (an archive, a compressed body, or another media type). headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - unsupported_content_type status: const: 415 ValidationGone: description: The selected release is withdrawn, the original saved result expired or was deleted, or the billing account is closed. Result replay never executes again. headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - ruleset_disabled - result_gone - account_closed status: const: 410 Unauthenticated: description: A credential was presented and could not be verified. Anonymous access, where permitted, sends no credential at all. headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - unauthenticated status: const: 401 MalformedRequest: description: The request itself is wrong - not the document being validated. headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - malformed_request status: const: 400 examples: field-errors: summary: Bad option values externalValue: https://api.ironfang.com/finance/examples/v1-problem-field-errors.json PaymentRequired: description: 'The free allowance is used up and paid usage cannot continue until the billing account is put right: paused, no working payment method, a payment awaiting confirmation, or restricted. No work or usage was accepted.' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - paid_usage_paused - payment_required - payment_action_required - account_restricted status: const: 402 ValidationTimeout: description: 'The 10-second validation deadline passed. Indeterminate: `outcome: indeterminate`, `usage_charged: false`.' headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - validation_timeout status: const: 504 examples: timeout: summary: Deadline exceeded externalValue: https://api.ironfang.com/finance/examples/v1-problem-indeterminate-timeout.json IdempotencyConflict: description: The `Idempotency-Key` was already used by this organisation, on the validation endpoint, for a request with different content. The original operation is left untouched; send a new key for a new request. headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - idempotency_conflict status: const: 409 ValidationRateLimited: description: 'Per-address or per-key rate limit (`rate_limited`), which asks for a retry, or the billing account can admit no more of this work this month: the free allowance used up without paid usage, or a monthly limit reached.' headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' Retry-After: $ref: '#/components/headers/RetryAfter' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - rate_limited - free_allowance_exhausted - account_budget_exhausted - product_budget_exhausted - exemption_limit_reached status: const: 429 PayloadTooLarge: description: The body exceeds 5 MiB. headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - payload_too_large status: const: 413 ValidatorUnavailable: description: 'The validator crashed, was unavailable, or an internal dependency failed. Indeterminate: `outcome: indeterminate`, `usage_charged: false`. Or billing was briefly unavailable (`billing_temporarily_unavailable`), and nothing was accepted. Retry after the indicated delay.' headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' Retry-After: $ref: '#/components/headers/RetryAfter' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - validator_unavailable - billing_temporarily_unavailable status: const: 503 examples: unavailable: summary: Validator dependency failure externalValue: https://api.ironfang.com/finance/examples/v1-problem-indeterminate-unavailable.json ScopeOrEligibility: description: The key lacks the scope this operation needs, or paid Ironfang Finance usage needs an approved business and the free allowance is used up. No work or usage was accepted. headers: X-Ironfang-Request-ID: $ref: '#/components/headers/XIronfangRequestID' X-Request-ID: $ref: '#/components/headers/XRequestIDEcho' content: application/problem+json: schema: allOf: - $ref: '#/components/schemas/Problem' - properties: code: enum: - insufficient_scope - product_not_eligible status: const: 403 headers: XRequestIDEcho: description: 'Your own correlation id, echoed back when the request carried a well-formed `X-Request-ID`: visible ASCII only (bytes 0x21 to 0x7E, so no spaces or control characters), 1 to 128 bytes. Anything else is ignored, not echoed. Never used as Ironfang''s identifier for the request.' required: false schema: type: string minLength: 1 maxLength: 128 pattern: ^[\x21-\x7E]{1,128}$ XIronfangRequestID: description: Ironfang's internal request id, minted at the edge for every request and carried in every Problem body as `request_id`. Quote it in support requests. schema: $ref: '#/components/schemas/RequestID' RetryAfter: description: Seconds to wait before retrying. Mirrors `retry_after_seconds` in the body. schema: type: integer minimum: 1 securitySchemes: platformKey: type: http scheme: bearer description: An Ironfang platform API key minted in the portal. Scopes listed on an operation are the ones the key must carry. Keys are the only credential this boundary accepts in V1. externalDocs: description: Ironfang Finance documentation url: https://ironfang.com/docs/finance