---
title: "Generate and validate one UBL e-invoice"
method: POST
path: "/v1/einvoices/generate"
tags: ["generation"]
---

# Generate and validate one UBL e-invoice

`POST /v1/einvoices/generate`

## Request

- Base URL: `https://api.ironfang.com/finance`
- URL: `https://api.ironfang.com/finance/v1/einvoices/generate`
- Auth: one of:
  - none
  - HTTP bearer

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.

## Query parameters

- `ruleset` string, required: `latest`, or an exact ruleset id in any validatable state.
- `profile` 'peppol-bis-billing-3', required

## Headers

- `Idempotency-Key` string

## Request body

- GenerationRequest (unresolved $ref)

## Response `200`

Only a fully validated artifact, after atomic authenticated persistence.

- GenerationResult
  - `artifact` object, required
    - `bytes` integer, required
    - `content_type` 'application/xml', required
    - `data_base64` string, byte, required
    - `sha256` string, required
  - `generator` object, required
    - `layer` 'generator', required
    - `status` 'passed', required
    - `version` string, required
  - `input` object, required
    - `bytes` integer, required
    - `canonicalization` 'financewolf-json/1', required
    - `content_type` 'application/json', required
    - `document_type` 'invoice' | 'credit_note', required
    - `schema_version` 'financewolf/einvoice/generation-input/v1', required
    - `sha256` string, required: Lowercase hex SHA-256.
  - `operation_id` string: An opaque Ironfang identifier.
  - `outcome` 'valid', required
  - `schema` 'financewolf/einvoice/generation-result/v1', required
  - `status` 'completed', required
  - `usage` Usage: Present only on authenticated results. Never reported for anonymous validation, which has no entitlement to report against.
    - `charged` boolean, required: Whether this validation consumed entitlement. Always `false` for indeterminate outcomes.
    - `credits` integer, required: Units of the operation's meter this operation used - 1 when charged, otherwise 0.
    - `period_ends_at` string, date-time: RFC 3339, UTC.
    - `remaining` integer: Only on results recorded before usage billing, returned as they were recorded. The month's usage is read at GET /v1/einvoices/usage.
  - `validation` object, required: 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.
    - `counts` FindingCounts, required
      - `by_layer` object, required
        - `en16931` integer, required
        - `input` integer, required
        - `peppol` integer, required
        - `xml` integer, required
        - `xsd` integer, required
      - `by_severity` object, required
        - `fatal` integer, required
        - `warning` integer, required
    - `document_id` string: An opaque Ironfang identifier.
    - `engine` EngineInfo, required: What ran, exactly, so the result can be reproduced.
      - `hint_pack_version` string: The version of Ironfang's hint mappings applied, when any. Nonblank when present.
      - `image_digest` string, required: The pinned validator engine image, as a sha256 digest.
      - `name` string, required: The validator engine and version, e.g. `phive/12.1.0`.
    - `findings` Finding[], required: Normalised findings in deterministic order. Empty, never null.
      - `hint` string
      - `hint_code` string
      - `layer` 'input' | 'xml' | 'xsd' | 'en16931' | 'peppol', required: A validation layer, in execution order.
      - `location` string: A bounded, sanitised location. Absent when `location_kind` is `none`.
      - `location_kind` 'none' | 'xpath' | 'line-column', required: The vocabulary the validator and the findings store use: `none`, `xpath`, or `line-column`.
      - `message` '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.', required: 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.
      - `rule_id` string, required: The official rule identifier, e.g. `PEPPOL-EN16931-R020`, `BR-CO-10`, or `cvc-complex-type.2.4.a` for schema errors.
      - `ruleset_id` string, required: An immutable ruleset identifier. Never reused, never edited.
      - `severity` 'fatal' | 'warning', required: `fatal` findings make the document invalid; `warning` findings never do and are never promoted.
      - `source_url` string, uri: 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.
    - `findings_summary` FindingsSummary, required: 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`.
      - `cap` 1000, required
      - `returned` integer, required
      - `total` integer, required
      - `truncated` boolean, required
    - `input` ValidationInput, required
      - `bytes` integer, required
      - `content_type` 'application/xml' | 'text/xml', required
      - `document_type` 'invoice' | 'credit_note' | 'unknown', required: The document type found at the root element; `unknown` when the root was not recognised or the XML did not parse.
      - `sha256` string, required: Lowercase hex SHA-256.
    - `layers` object[], required: Every layer, always in this order, always present.
      - `fatal` integer, required: Fatal findings this layer produced.
      - `layer` 'input' | 'xml' | 'xsd' | 'en16931' | 'peppol', required: A validation layer, in execution order.
      - `reason` 'skipped_after_failure' | 'not_applicable' | 'engine_error' | 'deadline_exceeded': 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.
      - `status` 'passed', required
      - `warning` integer, required
    - `links` ResultLinks: Authorised links, present only on authenticated results whose retention policy keeps them. Time-limited.
      - `evidence` string, uri: 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.
      - `report` string, uri: 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.
    - `operation_id` string: An opaque Ironfang identifier.
    - `outcome` 'valid', required: The standard's verdict. `indeterminate` means Ironfang Finance could not decide; it is never converted to `invalid` and never billed.
    - `profile` object, required: The profile family the document was validated as, and how that was decided.
      - `customization_id` string: The document's `CustomizationID`, a profile identifier - not invoice content. Present when parsed.
      - `family` 'peppol-bis-billing-3', required: A supported profile family. V1 supports Peppol BIS Billing 3.
      - `profile_id` string: The document's `ProfileID`. Present when parsed.
      - `source` 'requested', required: `requested` (the request named the profile or an exact ruleset) or `detected` (from the document's identifiers).
    - `ruleset` RulesetSelection, required: What was asked for and what was used.
      - `id` string, required: An immutable ruleset identifier. Never reused, never edited.
      - `official_release` string
      - `requested` string, required: `latest`, or an exact ruleset id in any validatable state.
      - `selection_method` 'exact' | 'latest' | 'detected', required: 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).
      - `state` 'scheduled' | 'sendable' | 'historical', required: 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.
      - `vesid` string: The engine's coordinate for the release, for cross-referencing official tooling.
    - `schema` 'financewolf/einvoice/validation-result/v1', required: The version of this result document.
    - `status` 'completed', required: How far execution got, independent of the verdict.
    - `timing` ValidationTiming, required
      - `completed_at` string, date-time, required: RFC 3339, UTC.
      - `duration_ms` integer, required: Wall time, bounded by the 10-second deadline.
      - `started_at` string, date-time, required: RFC 3339, UTC.
    - `usage` Usage: Present only on authenticated results. Never reported for anonymous validation, which has no entitlement to report against.
      - `charged` boolean, required: Whether this validation consumed entitlement. Always `false` for indeterminate outcomes.
      - `credits` integer, required: Units of the operation's meter this operation used - 1 when charged, otherwise 0.
      - `period_ends_at` string, date-time: RFC 3339, UTC.
      - `remaining` integer: Only on results recorded before usage billing, returned as they were recorded. The month's usage is read at GET /v1/einvoices/usage.

## Other responses

- `400`: The request itself is wrong - not the document being validated.
- `401`: A credential was presented and could not be verified. Anonymous access, where permitted, sends no credential at all.
- `402`: 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.
- `403`: 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.
- `409`: 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.
- `410`: The selected release is withdrawn, the original saved result expired or was deleted, or the billing account is closed. Result replay never executes again.
- `413`: The body exceeds 5 MiB.
- `415`: The body is not XML (an archive, a compressed body, or another media type).
- `422`: Generation selection refused.
- `429`: 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.
- `500`: Indeterminate generation failure; no artifact is returned and nothing is charged.
- `503`: 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.
- `504`: The 10-second validation deadline passed. Indeterminate: `outcome: indeterminate`, `usage_charged: false`.

---

[API](https://skmtc.dev/ironfang/apis/ironfang-finance-e-invoicing-api.md) · [All operations](https://skmtc.dev/ironfang/apis/ironfang-finance-e-invoicing-api/llms.txt) · [OpenAPI document](https://skmtc.dev/ironfang/apis/ironfang-finance-e-invoicing-api/revisions/115e20013788?raw)
