Peppol Validator

How to Add Peppol Validation to Your CI/CD Pipeline

6 min read
peppolci-cdapidevelopersautomation

Why validate invoices in CI/CD

If your application generates e-invoices, a validation step in your build pipeline catches errors before they reach production. A missing mandatory field, an invalid tax calculation, or a malformed XML structure will fail Peppol validation at the Access Point and get rejected. Catching these issues in CI saves time and prevents delivery failures.

The Peppol Validator API is free, requires no signup or API key, and supports both UBL and CII invoice formats.

The API

The Peppol Validator exposes a simple REST endpoint for invoice validation.

Validate a single file

curl -X POST https://peppolvalidator.com/api/validate \
  -F "files=@invoice.xml"

The API accepts multipart/form-data with one or more files. It returns a session ID and redirects to a results page. For CI/CD, you will want to poll the status endpoint until validation completes.

Full validation flow

The validation is asynchronous. Here is the complete flow:

  1. Upload your invoice(s) via POST /api/validate
  2. Poll the session status via GET /api/status/{sessionId} until all files have status: "done"
  3. Fetch individual results via GET /api/results/{sessionId}/{fileId}

Response format

The status endpoint returns:

{
  "sessionId": "abc-123",
  "files": [
    {
      "fileId": "file-001",
      "filename": "invoice.xml",
      "status": "done"
    }
  ]
}

Each result contains:

{
  "valid": false,
  "errors": [
    {
      "id": "BR-01",
      "text": "An Invoice shall have a Specification identifier.",
      "severity": "error",
      "location": "/Invoice"
    }
  ],
  "warnings": [],
  "errorCount": 1,
  "warningCount": 0
}

GitHub Actions example

Here is a complete GitHub Actions workflow that validates sample invoices on every push:

name: Validate Peppol Invoices
 
on:
  push:
    paths:
      - 'invoices/**'
      - 'templates/**'
 
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Validate invoices
        run: |
          set -e
 
          # Upload all XML files in the invoices directory
          FILES=""
          for f in invoices/*.xml; do
            [ -f "$f" ] && FILES="$FILES -F files=@$f"
          done
 
          if [ -z "$FILES" ]; then
            echo "No invoice files found"
            exit 0
          fi
 
          # Upload and capture the redirect URL
          RESPONSE=$(curl -s -w "\n%{redirect_url}" \
            -X POST https://peppolvalidator.com/api/validate \
            $FILES)
 
          SESSION_URL=$(echo "$RESPONSE" | tail -1)
          SESSION_ID=$(echo "$SESSION_URL" | grep -oP 'r/\K[^/]+')
 
          echo "Session: $SESSION_ID"
 
          # Poll until all files are done
          for i in $(seq 1 30); do
            STATUS=$(curl -s "https://peppolvalidator.com/api/status/$SESSION_ID")
            PENDING=$(echo "$STATUS" | jq '[.files[] | select(.status != "done")] | length')
 
            if [ "$PENDING" -eq 0 ]; then
              break
            fi
 
            echo "Waiting for validation... ($PENDING files pending)"
            sleep 2
          done
 
          # Check results
          FAILED=0
          for FILE_ID in $(echo "$STATUS" | jq -r '.files[].fileId'); do
            RESULT=$(curl -s "https://peppolvalidator.com/api/results/$SESSION_ID/$FILE_ID")
            FILENAME=$(echo "$STATUS" | jq -r ".files[] | select(.fileId==\"$FILE_ID\") | .filename")
            ERRORS=$(echo "$RESULT" | jq '.errorCount')
            WARNINGS=$(echo "$RESULT" | jq '.warningCount')
 
            if [ "$ERRORS" -gt 0 ]; then
              echo "::error::$FILENAME: $ERRORS error(s), $WARNINGS warning(s)"
              echo "$RESULT" | jq -r '.errors[] | "  - [\(.id)] \(.text)"'
              FAILED=1
            else
              echo "::notice::$FILENAME: valid ($WARNINGS warning(s))"
            fi
          done
 
          exit $FAILED

This workflow:

  • Triggers when invoice files change
  • Uploads all XML files from the invoices/ directory
  • Polls until validation completes
  • Fails the build if any invoice has errors
  • Shows detailed error messages in the GitHub Actions log

GitLab CI example

validate-invoices:
  stage: test
  image: alpine:latest
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      FILES=""
      for f in invoices/*.xml; do
        [ -f "$f" ] && FILES="$FILES -F files=@$f"
      done
 
      RESPONSE=$(curl -s -w "\n%{redirect_url}" \
        -X POST https://peppolvalidator.com/api/validate $FILES)
      SESSION_ID=$(echo "$RESPONSE" | tail -1 | grep -oP 'r/\K[^/]+')
 
      # Poll for completion
      for i in $(seq 1 30); do
        STATUS=$(curl -s "https://peppolvalidator.com/api/status/$SESSION_ID")
        PENDING=$(echo "$STATUS" | jq '[.files[] | select(.status != "done")] | length')
        [ "$PENDING" -eq 0 ] && break
        sleep 2
      done
 
      # Check results
      echo "$STATUS" | jq -r '.files[].fileId' | while read FILE_ID; do
        RESULT=$(curl -s "https://peppolvalidator.com/api/results/$SESSION_ID/$FILE_ID")
        ERRORS=$(echo "$RESULT" | jq '.errorCount')
        [ "$ERRORS" -gt 0 ] && exit 1
      done
  only:
    changes:
      - invoices/**

Writing a validation script

For more control, wrap the API calls in a script. Here is a Node.js example:

const BASE_URL = "https://peppolvalidator.com";
 
async function validateInvoice(filePath: string): Promise<boolean> {
  const form = new FormData();
  form.append("files", new Blob([readFileSync(filePath)]), basename(filePath));
 
  const uploadRes = await fetch(`${BASE_URL}/api/validate`, {
    method: "POST",
    body: form,
    redirect: "manual",
  });
 
  const sessionUrl = uploadRes.headers.get("location") ?? "";
  const sessionId = sessionUrl.split("/r/")[1];
 
  // Poll for completion
  let status;
  for (let i = 0; i < 30; i++) {
    const res = await fetch(`${BASE_URL}/api/status/${sessionId}`);
    status = await res.json();
    const pending = status.files.filter((f: { status: string }) => f.status !== "done");
    if (pending.length === 0) break;
    await new Promise((r) => setTimeout(r, 2000));
  }
 
  // Check results
  let allValid = true;
  for (const file of status.files) {
    const res = await fetch(`${BASE_URL}/api/results/${sessionId}/${file.fileId}`);
    const result = await res.json();
    if (result.errorCount > 0) {
      console.error(`${file.filename}: ${result.errorCount} error(s)`);
      for (const err of result.errors) {
        console.error(`  [${err.id}] ${err.text}`);
      }
      allValid = false;
    }
  }
 
  return allValid;
}

Handling validation results

Errors vs warnings

The Peppol Validator distinguishes between errors and warnings:

  • Errors mean the invoice violates a mandatory rule and will be rejected by the Peppol network. Your CI pipeline should fail on errors.
  • Warnings indicate best-practice violations or informational messages. They should not block the build, but you may want to log them.

Common CI failures

These are the rules that most commonly fail in CI pipelines:

Rule Issue Fix
BR-01 Missing specification identifier Add cbc:CustomizationID
BR-CO-10 Line amounts do not sum to total Check your calculation logic
BR-S-08 VAT category code mismatch Ensure tax category matches line items
PEPPOL-EN16931-R004 Specification identifier not matching BIS 3.0 Use the correct CustomizationID value

See the full validation error reference for details on each rule.

Best practices

Test with real-world samples. Do not just validate a single template. Include edge cases: credit notes, zero-amount lines, multiple tax rates, international addresses.

Pin your test invoices. If your application generates invoices dynamically, save known-good samples as test fixtures. This makes CI deterministic and fast.

Validate early. Run validation on pull requests, not just on the main branch. Catch issues before they merge.

Monitor for rule updates. Peppol and EN16931 rules are updated periodically (typically twice a year). A previously valid invoice might fail after a rule update. Keep an eye on OpenPeppol release notes.


The Peppol Validator API is free and requires no authentication. Start integrating validation into your pipeline today.

Ready to validate your Peppol invoices?

Drag and drop your UBL XML files and get instant validation against BIS Billing 3.0 and EN16931 rules. Free, no signup required.

Validate now