Skip to content

Validation

Validate invoices before sending to catch errors instantly — no network call needed.

Prefer the terminal? Use getpeppr validate invoice.json to validate offline without writing code. See the CLI docs.

Client-side Validation

The SDK includes a built-in validator that checks your invoice against Peppol BIS 3.0 business rules before making any API call. This catches errors instantly and saves you a round-trip to the server.

How It Works

Call peppol.validate() with the same payload you'd pass to peppol.invoices.send(). The method returns a result object with valid, errors[], and warnings[].

Errors vs Warnings

  • Errors — blocking issues that prevent sending. Must be fixed.
  • Warnings — non-blocking suggestions for better compliance. Sending still works.
Validation is also run automatically when you call send(). If validation fails, a PeppolValidationError is thrown — see Error Handling.
import { Peppol } from "@getpeppr/sdk";

const peppol = new Peppol({ apiKey: "sk_sandbox_..." });

// Validate locally before sending — catches errors instantly
const result = peppol.validate({
  number: "INV-2026-042",
  to:   { name: "ACMEDIA", peppolId: "0208:0685660237", street: "Rue de la Loi 200", city: "Brussels", postalCode: "1000", country: "BE" },
  lines: [
    { description: "Consulting Q1", quantity: 40, unitPrice: 125, vatRate: 21 },
  ],
});

if (!result.valid) {
  for (const error of result.errors) {
    console.error(`[${error.field}] ${error.message}`);
    // e.g. "[to.street] Street address is required for the buyer"

    if (error.suggestion) {
      console.log(`  Tip: ${error.suggestion}`);
      // "Add the buyer's street address"
    }
  }
}

// Also returns warnings for non-blocking suggestions
for (const warning of result.warnings) {
  console.warn(`[${warning.field}] ${warning.message}`);
}

Response Format

The validation result contains structured error and warning objects with field paths that point to the exact location of each issue.

Error Object

  • field — dot-notation path (e.g. from.vatNumber, lines[0].unitPrice)
  • message — human-readable description of the issue
  • suggestion — optional fix recommendation
Use the field path to highlight specific form fields in your UI, giving users precise feedback on what to fix.
Validation Result
{
  "valid": false,
  "errors": [
    {
      "field": "from.vatNumber",
      "message": "VAT number recommended for invoices > EUR 400",
      "suggestion": "Add vatNumber to the seller party"
    }
  ],
  "warnings": [
    {
      "field": "paymentIban",
      "message": "IBAN recommended for faster payment processing"
    }
  ]
}

Server-side Validation

Use POST /v1/validate/server when you want the gateway to run the same SDK validation stack server-side. The endpoint does not send invoices to Storecove; it checks the payload, verifies UBL XML generation, and runs offline Peppol business-rule validation.

Validation findings return HTTP 200 with valid: false. Malformed JSON, missing minimum fields, auth failures, and rate limits still return non-2xx errors.
Validation requests use a dedicated per-key rate-limit bucket, separate from send/list traffic. Server-side validation caps embedded attachment content at 2 MB decoded per attachment before UBL generation.
Server validation
curl -X POST https://api.getpeppr.dev/v1/validate/server \
  -H "Authorization: Bearer sk_sandbox_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "number": "INV-2026-042",
    "to": { "name": "ACMEDIA", "peppolId": "0208:0685660237", "street": "Rue de la Loi 200", "city": "Brussels", "postalCode": "1000", "country": "BE" },
    "lines": [
      { "description": "Consulting Q1", "quantity": 40, "unitPrice": 125, "vatRate": 21 }
    ]
  }'
Server Validation Result
{
  "valid": false,
  "errors": [
    {
      "field": "to.street",
      "message": "Street address is required for the buyer",
      "ruleId": "BR-50"
    }
  ],
  "warnings": [],
  "ubl": {
    "valid": true,
    "errors": []
  },
  "xsd": {
    "valid": true,
    "errors": [],
    "note": "Deprecated compatibility field. This endpoint verifies UBL XML generation but does not run a standalone XSD validator."
  },
  "schematron": {
    "valid": true,
    "coverage": {
      "rulesChecked": 18,
      "ofNetworkFatalRules": "partial"
    },
    "errors": [],
    "warnings": []
  }
}

Validating a UBL document you produced yourself

If you build your own UBL, send it to POST /v1/validate/ubl for a verdict, or import it through POST /v1/invoices/import to have it checked and then delivered. In both cases we run the complete official OpenPeppol rulebooks against it. We never modify your document, and validating it never sends it.

One caveat if you seal your documents. We do not regenerate, normalise or repair what you send, and we forward your bytes to the network unchanged. Equality at the byte level is not guaranteed beyond that point, because the network re-serialises the document in transit. Measured on a test document in August 2026, namespace declarations came back reordered, numeric character references were resolved, whitespace inside tags was dropped, and no element was added, removed or altered. That is one document and four kinds of difference, so read it as indicative rather than as a promise of what survives. Hash a canonical form (C14N) rather than raw bytes. Every import receipt states this in its own body, so your code can read it rather than rely on this page: a successful POST /v1/invoices/import carries a transmission object, including when you skip validation, and this version of the API sets its bytePreservation to not_guaranteed. Read the value rather than assuming it. One case has none: replaying an idempotency key returns the body cached for it, and an entry cached before this field existed carries no transmission — absent means the response did not say, never that your bytes are safe. And do not read a canonical document surviving unchanged as a promise that yours will.

We check every fatal rule that applies to your document, national rulebooks included. A Belgian, French, British or Irish invoice is judged against 333 fatal rules. A German one is judged against 357, because Germany publishes 24 additional national rules of its own. Which set applies is decided by your document, not by us.

Rules flagged as warnings are reported but never block a send. Two German IBAN checksum rules are known false positives of the XPath engine, and are always reported as warnings for that reason.

Whenever we judge a document we name the rulebook we judged it against, so you can tell when we have fallen behind the network. Three responses carry it: the verdict returned by POST /v1/validate/ubl, the refusal that names a broken rule on import, and the receipt of an import that passed and was sent. A response that judged nothing carries no rulebook, because none was run — a malformed request, a document too large, a rate limit, or an import you sent with x-skip-validation: true. We are currently on release v3.0.20, last verified against the official OpenPeppol repository on 2026-08-17.