Peppol PINT (Singapore, UAE), Factur-X and Access Points¶
KhataOS writes three e-invoice formats from a saved Sales or Credit Note
voucher, all on the EN 16931 model of crates/khata-en16931:
| Format | Who | Module | Command |
|---|---|---|---|
| PINT SG Billing (UBL 2.1) for Singapore's InvoiceNow | companies in SG | khata_en16931::pint |
pint_sg_xml(voucher_id) |
| PINT AE Billing (UBL 2.1) for the UAE Electronic Invoicing System | companies in AE | khata_en16931::pint |
pint_ae_xml(voucher_id) |
| Factur-X / ZUGFeRD PDF (EN 16931 profile) | companies in Germany, France, the Netherlands and Spain | khata_en16931::facturx |
facturx_pdf(voucher_id) |
Plus peppol_participant_id() (the company's Peppol id; no screen uses it
yet) and a generic
Access Point client (peppol_ap_settings, set_peppol_ap_settings,
peppol_ap_send). The desktop side is apps/desktop/src-tauri/src/peppol.rs;
the export actions are in apps/desktop/src/screens/peppol.ts.
The research behind it, with sources and confidence, is
docs/research/einvoicing-peppol.md.
Nothing here has been run through the official Schematron or a PDF/A
validator (see "What to verify").
Identifiers¶
| Singapore | UAE | |
|---|---|---|
| CustomizationID (BT-24) | urn:peppol:pint:billing-1@sg-1 |
urn:peppol:pint:billing-1@ae-1 |
| ProfileID (BT-23) | urn:peppol:bis:billing |
urn:peppol:bis:billing |
| Tax scheme | GST |
VAT |
| Participant id | 0195:SGUEN<UEN> |
0235:<TIN> (first 10 digits of the TRN) |
| UUID | cbc:UUID (BT-SG-003) |
cbc:UUID (BTAE-07) |
| Extra | — | cbc:ProfileExecutionID (BTAE-02, 8 flags) |
The UUID is the voucher's own id, so re-exporting a voucher gives the same
UUID. Peppol document type id (for Access Points):
urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@sg-1::2.1
(CreditNote likewise) [UNCERTAIN: check the receiver's SMP entry].
Tax categories from the books¶
Singapore (sg_line): the line's tax class and the rate on file
(khata_sg::gst::treatment): standard → SR, zero-rated → ZR, exempt →
ESN33, out of scope → OS. A class named after a GST category code
(ES33, DS, SRRC, NA, …) is that code. A company with no GST
registration number issues NG lines (no GST, no rate). A sale with no
GST to a party abroad is ZR. Rates are written for every category except
NG (BR-48-GST-SG): 9% (or the rate on file) for SR, 0 for the others.
UAE (ae_line): khata_ae::vat::mechanism and treatment: standard →
S (must be 5%, ibr-190-ae), zero-rated → Z, EXEMPT… → E with the
exemption code named in the class (EXEMPT DL8.46.2; ibr-167-ae requires
one of DL8.46.1–4), OUT-OF-SCOPE… → O, RC:… → AE. A sale with no
VAT to a party abroad is Z and an export (ProfileExecutionID
00000001), delivered to the buyer's address (ibr-152-ae). Exempt and
out-of-scope lines carry no rate (ibr-119/121-ae, aligned-ibrp-e-05).
EU / Factur-X (eu_category): EXEMPT… → E (reason "Exempt from
VAT"), REVERSE…/RC… → AE, 0% → Z, else S. A sale with no VAT to a party
abroad: EU business with a VAT id → K (goods) / AE (services); outside the
EU → G (goods) / O (services; note BR-O-02 forbids the seller's VAT id with
O, so such an invoice reports an error to fix by hand).
A discount (a ledger on the party's side) becomes a document allowance in the category of the lines; a voucher whose lines have several categories and a discount is refused.
Where the data comes from¶
Until the company profile and ledger masters hold these fields,
peppol.rs reads "Key: value" lines:
| Where | Lines |
|---|---|
| Company address | UEN:, Peppol ID: scheme:value, Trade licence:, Licensing authority:, Emirates ID:, Tel:, Email:, Contact:, IBAN:, BIC:, Bank account:, PayNow: |
| Party mailing address | Peppol ID:, UEN:, Trade licence:, Licensing authority: |
| Voucher narration | Buyer reference: / Your reference:, PO: / Order:, Payment terms:, Reason: (UAE credit note code); other lines are the invoice note |
Other sources: seller name = company name; GST no / TRN / VAT id = the company tax registration; UAE seller emirate = the company's tax region; buyer = party ledger (name, tax id, mailing address, post code, region, country, e-mail); due date = the bill's due date or the party's credit days (else the invoice date, with a warning); preceding invoice of a credit note = its "Against" bill reference.
Singapore addresses: the 6-digit postal code is found in a line such as "Singapore 048616" and the city is Singapore. UAE: the last plain line is the city; the buyer's emirate is the mailing region (or guessed from the city). EU: a "PLZ City" line ("10115 Berlin").
UAE buyers: TRN → 0235:<TIN>; abroad without a Peppol id →
0235:9900000099; in the UAE without TRN or Peppol id →
0235:9900000098 with a warning to issue a paper/PDF tax invoice too.
Credit notes default to reason DL8.61.1.D (goods returned) with a warning.
Rules checked (PintInvoice::validate)¶
Shared PINT: ibr-002, 006, 007, 009, 011, 016, 021, 023, 025, 027, 029,
033, 038, 049, 062, 063, 080, 081, ibr-co-10 to 16, 19, 26, ibr-091,
121–125, ibr-cl-04, ibr-sr-58; breakdown sums and tax (KhataOS ids
KHATA-PINT-*, BR-CO-17-GST-SG / aligned-ibrp-s/z-09). By construction:
one Note (ibr-sr-51), CustomizationID/ProfileID (R004-SG, R007-SG,
aligned-ibrp-001/002-ae), the 8-flag code (ibr-154-ae), item description,
gross price and base quantity, line amounts (ibr-104/125/126/194-ae).
Singapore: PEPPOL-EN16931-R003-SG, BR-CO-25-SG, BR-48, BR-105 to 109,
BR-111 to 113, BR-NG-01/02/11/12/13 (-GST-SG).
UAE: ibr-001, 010, 012, 055, 101, 103, 119, 124, 127, 128, 132, 134, 135,
141, 143, 144, 149, 150, 151, 152, 158, 159, 166, 167, 172, 180, 181,
190, 191, 192, 193 (-ae), aligned-ibrp-s/z/e/o/ae-05/06 and s-10;
unsupported transaction types (free zone, margin scheme, summary without
period, disclosed agent, e-commerce) are refused.
Factur-X: EN 16931 core rules (rules::validate_for(…, En16931)).
Not supported¶
- Foreign-currency invoices (SG: GST in SGD, BT-SG-001/002; UAE: exchange rate BTAE-04 and AED totals).
- UAE reverse-charge supplies (need the goods type BTAE-09 and a GTIN), margin scheme (category N), free zone, deemed supplies, summary, continuous supply, agent and e-commerce flags, item classification (HS / service accounting codes), out-of-scope invoice types 480/81, self-billing, the Tax Data Document (the ASP files it).
- SG: customer accounting (SRCA-*), GIRO (Z01), the B2G variant.
- Line allowances and charges; a discount across several tax categories.
- Factur-X profiles other than EN 16931 (MINIMUM, BASIC, EXTENDED,
XRECHNUNG); national CIUS rules (XRechnung for German public buyers is a
separate export, see
docs/germany-vat.md).
Factur-X PDF¶
facturx::pdf writes a PDF/A-3b file by hand (no PDF crate): a monospaced
text rendering of the invoice (Courier Prime, SIL OFL 1.1, embedded in
full; the licence is crates/khata-en16931/assets/CourierPrime-OFL.txt),
an sRGB output intent (the CC0 sRGB-v2-micro.icc of
saucecontrol/Compact-ICC-Profiles), XMP with pdfaid:part 3 /
conformance B, the Factur-X fx: properties (DocumentType INVOICE,
DocumentFileName factur-x.xml, Version 1.0, ConformanceLevel EN 16931)
and their PDF/A extension schema, and the CII XML (guideline
urn:cen.eu:en16931:2017) embedded as factur-x.xml (/Subtype
/text#2Fxml, /Params /ModDate, /AFRelationship /Alternative, listed in
the catalog /AF and /EmbeddedFiles). No Info dictionary, so it cannot
disagree with the XMP.
Conformance gaps (also returned to the UI as conformance_gaps):
- not validated with veraPDF or a Factur-X/ZUGFeRD validator (Mustang,
FNFE-MPE/FeRD Schematron) — neither is installed here; qpdf --check,
pdfinfo, pdffonts, pdfdetach and pdftotext read the sample file
without complaint;
- level B only (no tagged PDF, no ToUnicode map);
- only Windows-1252 characters can be shown, others print as ? (the XML
keeps them);
- the page is plain text, not the KhataOS invoice layout;
- the XML is CII D16B; Factur-X 1.08+ / ZUGFeRD 2.4+ publish D22B schemas
said to be backward compatible [UNCERTAIN];
- the whole font is embedded (about 40 KB per PDF);
- profile and AFRelationship choices for the French 2026 reform and German
B2B rules were checked only against the Factur-X reference
implementation.
write_binary_file(path, base64) saves the PDF; it accepts only .pdf
names and bytes starting with %PDF-.
Access Points (transmission)¶
KhataOS is corner 1 software. Sending goes through the company's Access Point (Singapore: an IMDA-accredited InvoiceNow Access Point / InvoiceNow- Ready Solution; UAE: an Accredited Service Provider, which also reports to the FTA). Every provider's API is different: sign-in, URL, payload wrapping, status callbacks. KhataOS ships one generic adapter and claims compatibility with no provider:
set_peppol_ap_settings({ endpoint, api_key, auth_header, auth_prefix })stores the URL (https only; http only to localhost for tests) and the key in<app data>/peppol/access-point.json, owner-only (0600, folder0700), never in the company file or its backups.peppol_ap_settingsreturns the key only as "set or not".peppol_ap_send(voucher_id, "pint-sg" | "pint-ae")builds the document, refuses it if it breaks PINT rules, and POSTs the UBL withContent-Type: application/xml, the key in the configured header (defaultAuthorization: Bearer …) and informativeX-Peppol-Sender-ID,X-Peppol-Receiver-ID,X-Peppol-Document-Type-ID,X-Peppol-Process-IDheaders. It reports the HTTP status and the start of the answer; a 2xx means only that the Access Point took the document.- Tested against a local mock server (
peppol::tests), not a real provider.
A provider needing another request shape needs its own adapter (one module
per provider, like irp_client.rs for India). Status tracking, inbound
invoices and the UAE 14-day deadline queue are not built.
Where it is in the app¶
apps/desktop/src/screens/peppol.ts exports exportPintSg(shell, v),
exportPintAe(shell, v), exportFacturX(shell, v) (save dialog, then
api.writeGstJson or peppolApi.writeBinaryFile), and
sendViaAccessPoint(shell, v, "pint-sg" | "pint-ae") /
setUpAccessPoint(shell). The voucher export key (Ctrl+E or Alt+E on a
saved Sales invoice or Credit Note, screens/einvoice.ts exportVoucher)
offers them:
- Singapore: InvoiceNow e-invoice (PINT SG), Send via Access Point, Access Point settings;
- UAE: UAE e-invoice (PINT AE), Send via Access Point, Access Point settings;
- Germany: XRechnung (UBL or CII) and Factur-X / ZUGFeRD PDF; France and the Netherlands: Factur-X / ZUGFeRD PDF; Spain: VeriFactu record and Factur-X / ZUGFeRD PDF.
The Access Point settings have no other menu row: they open from the Export menu, or on the first send.
What to verify¶
- Run the official PINT SG 1.4.x / PINT AE 1.0.4 Schematron (XSLT 2.0, e.g.
Saxon-HE) on real output; the samples in
pint::tests(PINT_SAMPLE_DIR=… cargo test -p khata-en16931 pint) validate against the UBL 2.1 XSDs shipped with PINT SG. - Run veraPDF (PDF/A-3B) and a Factur-X validator on
facturxoutput (FACTURX_SAMPLE=/path.pdf cargo test -p khata-en16931 facturx). - Confirm with IMDA / your Access Point the SG categories for customer accounting and NA, and whether 0% must be written for ES33/ESN33/OS.
- Confirm the UAE ASP's expectations (some take their own JSON, not UBL).