Skip to content

Engineering blog

What a certified Peppol Access Point project requires — and what readiness tooling can validate before integration

· Jithox engineering

Jithox is not a certified Peppol Access Point provider and does not claim to be one; we build read-only readiness tooling. That is why the distinction below is worth drawing carefully: two very different jobs get sold under the same word.

One is operating a certified Access Point: accreditation, OpenPeppol membership, a signed transport infrastructure agreement, a certificate under the Peppol PKI, and conformance re-testing at every release. That is a provider's business. The other is the readiness work before you connect to any of it — does your document parse, does it satisfy the rule set the receiving corner will run, are your identifiers resolvable, does your ERP carry the fields those rules demand. That one is yours, it is largely automatable, and skipping it is how projects discover in week nine that the source system has no field for a mandatory element.

To restate the boundary in the product's own words: our e-invoice MCP server validates and looks up (src/content/site/mcp-catalog.ts: "Read-only: it never sends an invoice and has no send capability").

Where an Access Point sits

Peppol moves a document between four corners: C1 the sender, C2 the sender's Access Point, C3 the receiver's, C4 the receiver. C1 and C4 never talk to each other — each contracts one Access Point. What C2 and C3 owe each other is the part requiring accreditation: a certificate under the Peppol PKI, the AS4 transport profile, and demonstrated conformance to current releases. That is an operating business with an audit trail, not a library you import. If a vendor offers to "make you a Peppol endpoint" without naming which accredited provider sits at C2, ask again.

Before C2 can send anything it must answer a question unrelated to the invoice: where does this receiver live, and will it accept this document type at all? The SML turns a participant identifier into the address of the SMP holding its metadata; the SMP answers which document types, endpoint and certificate apply. Two things follow. "The customer is on Peppol" is a fact about a specific document type, so a bare boolean is not enough. And discovery is a live network dependency: our participant-lookup contract has three outcomes — registered, not_registered and unavailable — so an outage does not fold into not_registered. Collapsing "I could not ask" into "the answer is no" is how you reject a real customer.

National localisations are not cosmetic

Peppol BIS Billing 3.0 is a CIUS of EN 16931 — a narrowing of the European norm. A great deal of tooling silently assumes the world is BIS 3.0 plus local dialect. It is not: outside Europe, OpenPeppol publishes PINT, and countries localise it.

For the UAE the ruleset is PINT AE, CustomizationID urn:peppol:pint:billing-1@ae-1 — read from the OpenPeppol specification index on 2026-08-24, and worth re-resolving there rather than from this article. PINT AE is a localisation of global PINT — it is not a CIUS of BIS Billing 3.0. An invoice that passes BIS 3.0 validation can still fail AE rules, on elements BIS 3.0 never asks about. "It validated" is only meaningful once you say which rulebook validated it.

Every UAE regulatory detail in this article should be verified against the UAE Ministry of Finance and Federal Tax Authority publications, not this article and not a vendor blog; programme details have moved before. With that said: the topology differs too. The UAE model is five-corner, usually called DCTCE — decentralised continuous transaction control and exchange — where a tax data report goes to the authority's platform alongside the exchange. That reporting leg is part of the flow, not a nightly batch afterwards.

What readiness validation can actually check

None of the following requires an Access Point contract, a certificate, or a network connection. All of it runs against files.

Syntax and schema. UBL 2.1 well-formedness is a real gate: XML that does not parse, a root that is not Invoice in the UBL Invoice-2 namespace, an unescaped & in a company name. That last is not hypothetical — any generator concatenating strings eventually meets a customer called "Smith & Sons", which is why our formatter escapes every text and attribute value rather than trusting inputs (src/features/mcp-peppol/tools/format-peppol-invoice.ts).

Business rules. EN 16931 carries a rule set with stable identifiers; the profile layers its own on top. Rule identity is what makes a finding actionable: our published validate_invoice contract specifies blocking findings carrying the rule code, so a caller gets BR-06 — Seller name is required rather than "invalid invoice" (src/content/site/mcp-catalog.ts) — the documented contract; the rule engine itself runs behind the MCP endpoint and is not in this repository. Be honest about depth: a hand-rolled subset is a pre-filter, not a conformance claim. Our formatter says so in its own comment: it "does NOT claim full EN 16931 schematron conformance (that needs the official validator)".

CustomizationID and ProfileID. These decide which rulebook runs and which process C3 applies. Get them wrong and you get a clean pass from the wrong rulebook, or a rejection for a fine document. Our formatter hardcodes the EU pair — CustomizationID urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0, ProfileID urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 — a limitation you can verify by reading the file: anything from that path is a BIS 3.0 document and is not a PINT AE document. These strings are versioned, and versioned strings drift.

Participant identifier schemes. A participant identifier is a scheme plus a value, not a free-text company number: 0208:0403170701 is a Belgian enterprise number, and 0088 is the GLN scheme our formatter uses as the default schemeID on cbc:EndpointID. Scheme codes come from the EAS code list, which is versioned. Confirm the identifier your ERP stores is the one that scheme expects — a tax registration number, a commercial registration number and a customer reference are three different values, and only one resolves in discovery.

Mandatory field coverage, mapped from the ERP. This is where readiness work earns its budget and it is the least glamorous part. Map each mandatory element to a concrete field in a real export of a few hundred documents and count the misses. The findings are boring and expensive: no buyer reference, a legal registration name that exists only as a display name, a unit code column holding free text instead of UN/ECE Rec 20 codes. Use a real export — a sample you wrote yourself encodes the assumptions you are testing, so it will teach you nothing.

Tax categories and rounding. VAT category codes come from UNCL5305, a rich source of quiet defects. A real one from our history: an earlier input schema constrained the category with ^[A-Z]$, silently rejecting the valid two-letter code AE (reverse charge), while single-letter codes such as K were unaffected. The fix was to model the code list as an explicit enum — S, Z, E, AE, K, G, O, L, M — rather than as a shape the codes happened to have the day the regex was written. And AE in UNCL5305 means reverse charge; it has nothing whatsoever to do with the United Arab Emirates. Any mapping table built by pattern-matching two-letter strings gets that wrong at least once.

Rounding is the other reliable failure, checkable without a network: line amounts sum to LineExtensionAmount, each rate is exactly one TaxSubtotal, TaxInclusiveAmount equals TaxExclusiveAmount plus TaxAmount, and PayableAmount equals TaxInclusiveAmount minus prepaid plus rounding (BR-CO-16). Our formatter collapses that to payable = taxInclusive only because it models no prepayment and no document-level allowances or charges. Round once at two decimals and normalise negative zero away — a -0.00 is a rejection waiting to happen, and it only ever appears on the customer's data.

Negative cases, or you have proven nothing. A validator that has never rejected anything has not been tested, only run. Malformed input must be refused with a typed error rather than rendered into invalid XML, and adversarial values must not escape into the output. Our suite pins that pair — a typed validation error, and an XML-injection case (tests/mcp-peppol/mcp-peppol.test.ts). Note the limit: these are formatter-level refusals, not rule-engine conformance cases. We have no rule-code rejection suite, and saying so is the point.

What this does not tell you

  • It is not certification. No amount of local validation makes anyone an accredited Access Point, and it produces nothing an accreditation body accepts as evidence.
  • It is not a compliance opinion. It is a technical check against a rule set — not tax advice, not legal advice, not a statement that your process satisfies any jurisdiction's obligations.
  • It does not replace your provider's conformance testing. Readiness work makes those tests boring; it does not replace them.
  • A green pre-check is about the document, not the network. Discovery availability, transport failures, receiver-side rejections and the tax-reporting leg sit outside what a file-level validator sees.
  • It is dated. Rulesets, code lists and identifier strings are versioned, which is why the specification version belongs in the result, not a footnote.

That is the same list we apply to our own product surface: "Validation, not certification — no compliance guarantee and no fiscal or legal advice."

Where to start

Do the readiness half first, on real data: ERP field coverage is nearly always the long pole, found late. If you want the counterparty half answered by an API rather than by hand, our read-only e-invoice tools cover VAT verification through EU VIES and participant discovery through Peppol SML/SMP, with structural validation against BIS 3.0 expectations: Jithox E-Invoice. Outages documented as outages, not as absences.

Sources

  • src/features/mcp-peppol/tools/format-peppol-invoice.ts — the UBL 2.1 renderer: the BIS 3.0 identifier pair, the UNCL5305 enum, XML escaping, TaxSubtotal grouping.
  • src/content/site/mcp-catalog.ts — the E-Invoice manifest: tool contracts, outcome vocabularies, and the limitations quoted above.
  • tests/mcp-peppol/mcp-peppol.test.ts — the formatter's negative cases: a typed validation error and an XML-injection case.
  • https://docs.peppol.eu/poacc/billing/3.0/ — the CIUS our formatter targets.
  • https://docs.peppol.eu/ — the OpenPeppol specification index; resolve the current PINT, PINT AE and EAS releases here, not from memory.
  • https://mof.gov.ae/ and https://tax.gov.ae/ — verify every UAE regulatory specific against these.

Run this check on your own fleet

From nothing to a verified MCP call with a signed receipt — no card, no approval.