Back to blog
Technical

Integrating E-Invoice Compliance Into Your ERP

How to add a compliance layer to your ERP product: architecture, integration patterns, code examples, and the Evidence Pack workflow.

Invoice Navigator TeamApril 8, 20267 min read
erpintegrationapidevelopercompliancepipeline

Your ERP generates invoices. Your customers need those invoices to be compliant with EN 16931, Peppol BIS, XRechnung, and whatever country-specific rules apply to their receivers. You can build all of this in-house — or you can add a compliance layer and ship it next quarter.

Why ERP Vendors Need a Compliance Layer

Three forces are converging:

Customer demand. "Can your ERP send Peppol invoices?" is now a standard question in procurement evaluations across Europe. Belgium's B2B mandate went live January 2026. Germany is phasing in B2B requirements. France is next. Your customers need this to work.

Mandate pressure. Each new country mandate adds a CIUS with its own validation rules, identifier requirements, and format nuances. Belgium needs KBO/BCE numbers with scheme ID 0208. Germany needs Leitweg-IDs in BT-10. The Netherlands uses KVK numbers with scheme 0106. Each country is a new set of rules to implement and maintain.

The alternative. Building e-invoicing compliance in-house means implementing the four validation layers, maintaining Schematron rules for every supported country, handling rule updates 2-3 times per year, and testing against every format combination. For a single country, this is feasible. For 10+ EU markets, it's a dedicated team.

A compliance API turns months of development into a single integration point.

Architecture: Where the Gate Sits

The compliance gate is an API call positioned between your ERP's invoice export and the delivery channel (Peppol Access Point, government portal, or email):

ERP Invoice Export → [Compliance Gate API] → Access Point → Peppol Network
                          ↓ (if invalid)
                     Error Queue / UI

The gate is stateless. It receives an invoice, validates it, optionally remediates structural issues, and returns the result. It doesn't store invoices. It doesn't manage state. Your ERP remains the system of record.

This means the integration is lightweight — a single HTTP call in your invoice export pipeline. No database changes, no schema migrations, no new infrastructure to manage.

Integration Pattern: Synchronous API Call

The most common pattern. Validate every invoice before sending it to the Access Point.

import { InvoiceNavigator } from '@invoice-navigator/sdk';

const client = new InvoiceNavigator({
  apiKey: process.env.INVOICE_NAVIGATOR_API_KEY
});

async function processInvoice(invoiceXml: Buffer) {
  // Validate + remediate in one call
  const result = await client.process({
    invoice: invoiceXml,
    remediate: true,
    format: 'auto'
  });

  switch (result.status) {
    case 'compliant':
      // Invoice is valid — send to Access Point
      await sendToAccessPoint(result.invoice);
      // Store Evidence Pack for audit
      await storeEvidencePack(result.evidencePack);
      break;

    case 'input_required':
      // Missing data — queue for user input
      await queueForInput(result.inputFields);
      break;

    case 'blocked':
      // Financial field issues — route for review
      await routeForReview(result.blockedErrors);
      break;
  }
}

Error Handling

async function processWithRetry(invoiceXml: Buffer) {
  try {
    const result = await client.process({
      invoice: invoiceXml,
      remediate: true
    });
    return result;
  } catch (error) {
    if (error.status === 429) {
      // Rate limited — wait and retry
      await delay(error.retryAfter * 1000);
      return processWithRetry(invoiceXml);
    }
    if (error.status >= 500) {
      // API unavailable — queue for later
      await queueForRetry(invoiceXml);
      return null;
    }
    throw error;
  }
}

The key design principle: if the compliance API is temporarily unavailable, queue the invoice for retry rather than sending it unvalidated. A delayed compliant invoice is always better than a rejected non-compliant one.

Integration Pattern: Async Batch

For high-volume pipelines where latency isn't critical, batch processing reduces API calls and simplifies error handling.

async function processBatch(invoices: Buffer[]) {
  const results = await client.processBatch({
    invoices: invoices.map(xml => ({
      invoice: xml,
      remediate: true,
      format: 'auto'
    }))
  });

  const compliant = results.filter(r => r.status === 'compliant');
  const needsInput = results.filter(r => r.status === 'input_required');
  const blocked = results.filter(r => r.status === 'blocked');

  // Send compliant invoices to Access Point
  await Promise.all(compliant.map(r => sendToAccessPoint(r.invoice)));

  // Queue the rest for handling
  await queueForInput(needsInput);
  await routeForReview(blocked);
}

When to use batch: overnight processing runs, hourly pipeline sweeps, or any scenario where invoices can wait minutes to hours before sending. Batch processing is also more cost-efficient — bulk API calls typically have lower per-invoice pricing.

Integration Pattern: Webhook

For event-driven architectures, webhooks let the compliance API call back to your system when processing is complete.

// 1. Submit invoice for async processing
const job = await client.submitAsync({
  invoice: invoiceXml,
  remediate: true,
  webhookUrl: 'https://your-erp.com/api/compliance-callback'
});

// 2. Your webhook endpoint receives the result
app.post('/api/compliance-callback', async (req, res) => {
  const { jobId, status, invoice, evidencePack, errors } = req.body;

  if (status === 'compliant') {
    await sendToAccessPoint(invoice);
    await storeEvidencePack(evidencePack);
  } else {
    await handleErrors(jobId, status, errors);
  }

  res.sendStatus(200);
});

When to use webhooks: microservice architectures where the invoice export service shouldn't block waiting for validation, or when processing takes longer (e.g., ZUGFeRD PDFs that require PDF/A-3 conformance checking).

Authentication & API Key Management

Use separate API keys for each environment:

| Environment | Purpose | Rate Limits | |-------------|---------|------------| | Development | Testing with sample invoices | Low (sandbox) | | Staging | Integration testing with real formats | Medium | | Production | Live invoice processing | Per your plan |

# Development
INVOICE_NAVIGATOR_API_KEY=inv_dev_xxxxxxxx

# Production
INVOICE_NAVIGATOR_API_KEY=inv_prod_xxxxxxxx

The sandbox environment accepts any API key prefixed with inv_dev_ and returns realistic validation results without counting against your production quota. Use it for:

  • CI/CD pipeline validation checks
  • Template regression testing
  • Developer onboarding

Evidence Pack Integration

The Evidence Pack is the compliance proof for every processed invoice. It contains the original invoice, validation report, remediation log, and revalidation result.

Storing Evidence Packs

Link each Evidence Pack to your invoice record:

async function storeEvidencePack(
  invoiceId: string,
  evidencePack: EvidencePack
) {
  // Store the pack (S3, database, or document management system)
  const packUrl = await storage.upload(
    `evidence-packs/${invoiceId}.json`,
    evidencePack
  );

  // Link to invoice record
  await db.invoices.update(invoiceId, {
    evidencePackUrl: packUrl,
    complianceStatus: 'verified',
    validatedAt: new Date()
  });
}

Audit Retrieval

When a tax authority or trading partner requests proof of compliance, the Evidence Pack provides:

  • The original invoice as submitted
  • Every validation rule checked and its result
  • Every auto-fix applied, with before/after values
  • The final revalidation confirming compliance

This is the difference between "we think this invoice was valid" and "here's the documented proof." For businesses operating under Belgium's mandate, Germany's B2G requirements, or any country where compliance is legally required, the Evidence Pack is the audit trail.

Get started with the API → or see pricing →

FAQ

How long does API integration take?

For a synchronous pre-submission gate, most teams ship the initial integration in 1-2 days. The API call itself is straightforward — the work is in wiring it into your existing invoice export flow and handling the three result states (compliant, input required, blocked).

What happens if the API is down?

Queue the invoice for retry. Never send an unvalidated invoice to the Access Point — the cost of a rejection far exceeds the cost of a brief delay. Our API has 99.9% uptime SLA on business plans.

Do I need to handle different API calls per country?

No. The API auto-detects the invoice format and applies the correct rule set. A single process() call handles Peppol BIS, XRechnung, ZUGFeRD, Factur-X, and all supported country CIUS rules. You don't need country-specific integration logic.

Can I use this for template testing in CI/CD?

Yes. Generate a test invoice from your template, call the validation API, and fail the build if errors are returned. This catches template regressions before they reach production. Use the sandbox API key (inv_dev_*) for CI/CD — it doesn't count against your production quota.

Check Your Compliance Status

Find out exactly what your business needs to do for e-invoicing compliance.

Use Obligation Finder