Validation
Validate invoices before sending to catch errors instantly — no network call needed.
getpeppr validate invoice.json to validate offline
without writing code. See the CLI docs.
Client-side Validation
The SDK includes offline validation utilities that catch common issues before making any API call. The client method checks structured input and country-specific advisories; the separate Schematron-style validator runs the registered partial pre-flight checks. Neither is a Peppol conformance verdict.
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[].
Import validateSchematron(input) when you also want the 40 registered checks. Some use exact network-rule IDs; the ten local checks renamed in SDK 5.2.0 use explicit GETPEPPR-* IDs. The legacy provider-capability diagnostic unsupported_vat_category can also appear, but is outside that count.
Errors vs Warnings
- Errors — blocking findings for this local pre-flight. Fix them before sending.
- Warnings — do not block the local pre-flight; network validation may still reject the invoice.
peppol.validate() also runs automatically
when you call send(). The validateSchematron() utility remains opt-in.
If the structured-input 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 issuesuggestion— optional fix recommendation
field path to highlight specific form fields in your UI,
giving users precise feedback on what to fix.
{
"valid": false,
"errors": [
{
"field": "to.street",
"message": "Street address is required for the buyer",
"suggestion": "e.g. \"123 Business Street\""
}
],
"warnings": [
{
"field": "to.vatNumber",
"message": "Buyer VAT number not provided. May be required for B2B."
},
{
"field": "dueDate",
"message": "No due date specified. Recommended for payment terms."
}
]
}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 the registered partial offline pre-flight checks.
200 with valid: false. Malformed JSON,
missing minimum fields, auth failures, and rate limits still return non-2xx errors.
valid: true and providerSendability: "not_checked", then a Standard JSON send can still return 422 — for example when Storecove rejects a VAT percentage for the seller's tax country and invoice date. Treat valid as an offline pre-flight result, never as sendability.
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 }
]
}'{
"valid": false,
"errors": [
{
"field": "to.street",
"message": "Street address is required for the buyer"
}
],
"warnings": [],
"countryRules": [],
"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": 40,
"ofNetworkFatalRules": "partial"
},
"errors": [],
"warnings": []
},
"providerSendability": "not_checked"
}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.
JSON send and UBL import deliberately have different provider contracts. A JSON send is mapped to Storecove Standard JSON, where Storecove validates each VAT percentage against the seller legal entity's tax country at the invoice date. Import uses Enveloped Mode and keeps the tax jurisdiction expressed by your UBL; getpeppr validates that UBL against the official rulebooks instead of copying the Standard JSON rate catalogue into the import path.
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 335 fatal rules. A German one is judged against 359, because Germany publishes 24 additional national rules of its own. Which set applies is decided by your document, not by us.
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.21, last verified against the official OpenPeppol repository on 2026-08-28.