How to Integrate E-Invoicing into Your ERP
Your ERP shouldn't need to understand CII, UBL, Schematron, or PDF/A-3. It should send JSON and receive JSON. Factur-X Engine is the translation layer that handles the rest.
Updated
The Problem
EN 16931 e-invoicing mandates are rolling out across Europe. Your ERP needs to handle multiple formats (Factur-X, ZUGFeRD, XRechnung), validate against Schematron rules, and generate PDF/A-3 compliant documents.
Building this in-house means maintaining XML parsers, tracking regulatory updates, and handling edge cases across hundreds of real-world invoices. The alternative: a dedicated middleware that speaks both ERP and e-invoicing.
Architecture
Factur-X Engine sits between your application and the outside world. It translates in both directions:
RECEIVE (Supplier invoices)
Supplier PDF/XML --> Factur-X Engine --> Clean JSON --> Your ERP / DB
| /v1/validate |
| /v1/extract |
| /v1/serialize|
SEND (Your invoices)
Your ERP JSON --> Factur-X Engine --> Compliant PDF/XML --> Client
| /v1/xml |
| /v1/convert |
| /v1/merge |
Receive — Ingesting Supplier Invoices
When your ERP receives a Factur-X or ZUGFeRD PDF from a supplier, three API calls turn it into usable data:
1. Validate — Compliance Gate
Check the invoice against EN 16931 Schematron rules before it enters your database.
curl -X POST "http://localhost:8000/v1/validate" \
-F "file=@supplier_invoice.pdf"
2. Extract — Heuristic Best-Effort JSON
Pull structured data from the embedded XML. Returns a standard JSON representation.
curl -X POST "http://localhost:8000/v1/extract" \
-F "file=@supplier_invoice.pdf"
3. Serialize — ERP-Ready JSON Pro
Returns a normalized, typed JSON object with a versioned schema — ready for direct database insertion.
curl -X POST "http://localhost:8000/v1/serialize" \
-F "file=@supplier_invoice.pdf"
Send — Generating Compliant Invoices
Your ERP has the business data. The engine transforms it into regulation-compliant XML or PDF:
4. Generate XML — Business Data to CII
Transform your ERP JSON into a Cross-Industry Invoice XML document.
curl -X POST "http://localhost:8000/v1/xml" \
-H "Content-Type: application/json" \
-d @invoice_data.json -o invoice.xml
5. Convert — One-Step PDF Generation
Generates XML from JSON metadata and embeds it into your PDF in a single call.
curl -X POST "http://localhost:8000/v1/convert" \
-F "pdf=@invoice.pdf" \
-F "metadata=@invoice_data.json" \
--output invoice_compliant.pdf
Docker Compose Setup
For production ERP deployments, run Factur-X Engine as a sidecar next to your application. A minimal
docker-compose.yml that pins the image
and exposes the engine only on the internal network:
services:
app:
build: .
environment:
FACTURX_ENGINE_URL: http://facturx-engine:8000
depends_on:
- facturx-engine
facturx-engine:
image: facturxengine/facturx-engine:1.7.2
environment:
LICENSE_KEY: ${FACTURX_LICENSE_KEY}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
retries: 3
# Not exposed externally — only reachable by app service
Pin to a specific image tag (e.g. 1.7.2) rather than latest
for reproducible deployments. Rotate the LICENSE_KEY via your secrets manager without rebuilding.
Error Handling & Retry Patterns
Factur-X Engine returns RFC 9457 Problem Details for all errors. Your ERP integration should handle three categories:
| HTTP Status | Cause | ERP Action |
|---|---|---|
| 400 | Malformed request, missing field | Log detail, reject, no retry |
| 409 / 422 | Already Factur-X / not PDF/A-3 | Route to manual review queue |
| 413 | File too large | Split invoice batches, no retry |
| 429 | Rate limit exceeded | Exponential backoff (2s, 4s, 8s) |
| 500 / 503 | Engine overload / restart | Retry 3× with jitter, then alert |
import requests, time, random
def engine_post(url, **kwargs, max_retries=3):
for attempt in range(max_retries):
r = requests.post(url, **kwargs, timeout=30)
if r.status_code < 500:
return r # 4xx: no retry
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
r.raise_for_status() # Final attempt failed
ERP-Specific Integration Patterns
SAP / Odoo — Webhook on Invoice Post
In SAP S/4HANA or Odoo, hook into the invoice posting event. When a customer invoice transitions
to Posted, call /v1/convert asynchronously (Celery task,
SAP Business Event). Store the resulting Factur-X PDF in the document management system (DMS) and
attach the URL to the invoice record. The ERP never touches XML directly.
Accounts Payable — Automated Compliance Gate
For inbound supplier invoices, add a validation step before the invoice enters the AP approval workflow.
Call /v1/validate. If valid: false,
route to a rejection queue with the Schematron errors pre-filled in the rejection reason. Only valid
invoices proceed to /v1/extract for data normalization. This pattern
eliminates manual XML inspection and makes compliance audits trivial — every rejection has a structured log.
Custom ERP — Batch Processing at End of Day
For custom ERP systems without event hooks, run a nightly batch that converts all invoices created
that day. Parallelise with a thread pool (8 workers covers most hardware). Each worker reads from a
job queue, calls /v1/convert, and writes the PDF back to object
storage (S3, MinIO). Failed jobs are requeued with the RFC 9457 error detail stored for triage.
Full Workflow — Python Example
A complete receive-and-send pipeline in Python using the requests library:
import requests, json
ENGINE = "http://localhost:8000"
# RECEIVE: Supplier sends a Factur-X PDF
def ingest_supplier_invoice(pdf_path):
# Step 1: Validate compliance
r = requests.post(f"{ENGINE}/v1/validate",
files={"file": open(pdf_path, "rb")})
report = r.json()
if not report["valid"]:
raise ValueError(f"Non-compliant: {report['errors']}")
# Step 2: Extract structured data
r = requests.post(f"{ENGINE}/v1/extract",
files={"file": open(pdf_path, "rb")})
return r.json()["invoice_json"]
# SEND: Generate a compliant invoice
def generate_invoice(pdf_path, metadata):
r = requests.post(f"{ENGINE}/v1/convert",
files={"pdf": open(pdf_path, "rb")},
data={"metadata": json.dumps(metadata)})
with open("output.pdf", "wb") as f:
f.write(r.content)
return "output.pdf"
See Also
Get Started in 30 Seconds
docker run -d -p 8000:8000 facturxengine/facturx-engine:latest