Guide / ERP Integration

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:

Data Flow Architecture

  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:

docker-compose.yml YAML
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
retry_client.py Python
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:

erp_pipeline.py Python
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