The Ironfang Finance API has two versions that run side by side. V1, under /finance/v1, is unchanged: its clients keep their Peppol shapes, and it never returns a V2 result. V2, under /finance/v2, validates more families and formats and reports exactly what was checked. Nothing has to move: a V1 integration keeps working, and V2 is where the other families are.
Which API serves what
| What | API |
|---|---|
| Peppol BIS Billing 3 validation (UBL) | V1. V2 has no Peppol release yet and answers 422 unknown_ruleset. |
| XRechnung 3.0.2 validation (UBL or CII) | V2 only (family=xrechnung). |
| ZUGFeRD 2.5.2 / Factur-X 1.09.2 validation (CII XML or hybrid PDF) | V2 only (family=zugferd-facturx). |
| Invoice generation from JSON | V1 only (POST /finance/v1/einvoices/generate). |
| Readable PDF of a generated invoice | V1 only (POST /finance/v1/einvoices/render). |
| Signed evidence reports | Both. POST /finance/v2/einvoices/reports signs a report of any retained result in that result's own version, V1 or V2; V1's POST /finance/v1/einvoices/reports signs V1 results only. POST /finance/v2/einvoices/reports/verify checks reports of either version. |
| Retained results, deliveries, jobs and batches | Both. V2 lists V1 and V2 records together; V1 lists only its own. |
| Destinations (webhooks and S3) | V1 routes (/finance/v1/einvoices/destinations); one destination receives V1 and V2 events. |
GET /finance/v2/einvoices/rulesets lists exactly which family, variant, syntax, document type and scope each active V2 release covers; nothing else is supported. The family guides say more: XRechnung and ZUGFeRD / Factur-X.
Requests
Both versions take the document as the request body, choosing with query parameters, or as multipart/form-data with one document part and an optional options part holding the same choices as JSON. Query parameters are not read for multipart requests, and an unknown or repeated parameter or part is 400 malformed_request in both.
| V1 | V2 | |
|---|---|---|
| Body | application/xml or text/xml | The same, or application/pdf for a hybrid invoice. The content is inspected; XML holding a PDF, or the reverse, is 415. |
| Choices | ruleset, profile, document_type | ruleset, family, variant, document_type, scope |
| Detection | Without a profile, from the document's CustomizationID and ProfileID | Without a family, from the document's declaration. A named family the document contradicts is 422 family_mismatch, with what it declares in detected. |
| Scope | XML only | xml for XML, hybrid_pdf for a PDF; any other combination is 422 scope_unavailable. |
| Size | XML up to 5 MiB | XML up to 5 MiB; a PDF up to 20 MiB, 200 pages and 20 embedded files, with its invoice XML up to 5 MiB. |
| Deadline | 10 seconds | 10 seconds for XML, 30 seconds for a PDF |
Results
A V1 result has the schema financewolf/einvoice/validation-result/v1, a V2 result ironfang/finance/einvoice/validation-result/v2. Both say outcome valid or invalid for a completed validation, decided by the fatal findings alone; anything that stopped a verdict is a Problem with outcome: indeterminate and nothing charged, in both. The fields that differ:
| Field | V1 | V2 |
|---|---|---|
input | sha256, bytes, content_type, document_type | media_type for content_type; for a PDF also embedded_xml, the XML that was checked |
ruleset | requested, id, selection_method, state, vesid, official_release | No vesid; adds family, variant, syntax, scope and technical_release |
profile | The profile family | Not present: ruleset.family and ruleset.variant |
declared | Not present | What the document says about itself, copied: syntax, root, specification and process identifiers, type code, and for a PDF what its metadata declares |
layers | Always the same five: input, xml, xsd, en16931, peppol | The family's own sequence from input, pdfa, hybrid_binding, xml, xsd, en16931, peppol, xrechnung and zugferd_profile |
groups | Not present | The layers grouped: invoice_xml always, and for a PDF pdfa and attachment_metadata before it |
coverage | Not present | The scope, the layers that ran, and not_checked: what the result cannot establish, such as a PDF that was not supplied |
engine / engines | One engine: name, image digest, hint pack version | An engines list, each with a role: xml, and pdf for a hybrid check |
links / next_actions | links | next_actions: view, and extract_xml for a PDF |
operation_id, status, counts, findings_summary, timing and usage are in both. counts in V2 also counts information findings.
Findings
| V1 | V2 | |
|---|---|---|
| Severity | fatal or warning | fatal, warning or information; rule_flag is the rule's own flag where the family's configuration sets another level |
| Location | location (a string) and location_kind: none, xpath or line-column | One location object with kind (also pdf, a PDF object path) and value |
| Source | An FW- rule id marks Ironfang's own check | rule_source: official or ironfang |
| Guidance | hint_code, hint, source_url, ruleset_id | hint_code, source_url |
In both, message is one of Ironfang's fixed messages: the official rule texts are not repeated, because they can carry invoice values. rule_id is the official identifier, unchanged.
Retained results
GET /finance/v2/einvoices/results lists validations and generations made through V1 and validations made through V2 together, each saying which API made it (api_version), the schema of its retained document (result_schema) and its family, with filters on each. GET /finance/v2/einvoices/results/{id} returns any of them in the schema it was made under, and DELETE erases either. V1's own list shows only V1 results, and reading a V2 result through V1 is 404: a V2 result is not a document V1's schema describes. Results are kept for 30 days in both.
Webhook events and deliveries
A finished operation queues one event, delivered to every enabled webhook or S3 destination and signed the same way whichever API made it. An operation made through V1 sends financewolf/einvoice/event/v1, whose result_url is under /finance/v1. An operation made through V2 sends ironfang/finance/einvoice/event/v2, whose result_url is under /finance/v2 and whose data also carries the family, variant and scope the result was made under. A receiver that takes both should branch on schema.
GET /finance/v2/einvoices/deliveries lists deliveries of both kinds, each with its api_version and its event as queued, and can filter on api_version. POST /finance/v2/einvoices/deliveries/{id}/retry retries a failed one of either kind. V1's delivery list, read and retry see only deliveries of V1 events.
Jobs and batches
POST /finance/v2/einvoices/jobs queues one V2 validation and POST /finance/v2/einvoices/batches 1 to 100 together, all or none. Each job carries document_base64, its media_type and the synchronous options. A document synchronous validation would refuse is refused on acceptance with the same Problem, and nothing is queued or charged. A job's result, usage and event are exactly a synchronous validation's.
| V1 | V2 | |
|---|---|---|
| Operations | Validation and generation | Validation |
| Document | XML up to 5 MiB | XML up to 5 MiB or a PDF up to 20 MiB |
| Job envelope | 8 MiB | 28 MiB |
| Batch | 1 to 100 jobs, envelope 32 MiB | 1 to 100 jobs, documents 25 MiB together, envelope 36 MiB |
V1 and V2 jobs share each organisation's queue: at most 100 unfinished jobs and 25 MiB of their documents, held encrypted until they finish, at most 24 hours. V1's job and batch lists show only V1 work; V2's show both.
What both share
- Ironfang platform API keys (
Authorization: Bearer) with the same scopes:finance:einvoices:writeto validate,finance:einvoices:readto read. - Anonymous validation without a key: nothing is stored, and tighter rate limits apply.
- An optional
Idempotency-Keyon authenticated requests: the same key and request return the first result and are not charged again. V2 keys are V2's own, apart from V1's. - Problems as RFC 9457
application/problem+json; branch oncode. - One Finance allowance: a completed validation through either API is one operation.
- Uploaded documents are not stored by synchronous validation.
Example
No single document goes through both APIs today, because Peppol has no V2 release. The request mechanics are the same, so this pairs a V1 Peppol call with a V2 XRechnung call, and adds the PDF only V2 takes.
# V1: a Peppol BIS Billing 3 invoice (UBL). Peppol is validated through V1.
curl --fail-with-body --silent --show-error \
"https://api.ironfang.uk/finance/v1/einvoices/validate?ruleset=latest&profile=peppol-bis-billing-3" \
-H "Authorization: Bearer $IRONFANG_API_KEY" \
-H "Content-Type: application/xml" \
-H "Idempotency-Key: peppol-invoice-0001" \
--data-binary @peppol-invoice.xml
# V2: an XRechnung invoice (UBL or CII). The same request mechanics,
# with the family named instead of a profile.
curl --fail-with-body --silent --show-error \
"https://api.ironfang.uk/finance/v2/einvoices/validate?ruleset=latest&family=xrechnung" \
-H "Authorization: Bearer $IRONFANG_API_KEY" \
-H "Content-Type: application/xml" \
-H "Idempotency-Key: xrechnung-invoice-0001" \
--data-binary @xrechnung-invoice.xml
# V2: a ZUGFeRD / Factur-X hybrid PDF. Only V2 takes a PDF.
curl --fail-with-body --silent --show-error \
"https://api.ironfang.uk/finance/v2/einvoices/validate?family=zugferd-facturx" \
-H "Authorization: Bearer $IRONFANG_API_KEY" \
-H "Content-Type: application/pdf" \
--data-binary @zugferd-invoice.pdf