Integration playbooks
Short, opinionated, end-to-end recipes for the six tasks most teams do in their first month.
Sending invoices over Peppol
The happy path in four steps.
-
Register the sender
One-time. Creates the company, enriches it, and publishes it to the Peppol SMP.
curl -X POST …/v1/companies -d '{"vatNumber":"BE0123456789"}' -
Verify the recipient
Always verify before sending to avoid bounced deliveries.
curl -X POST …/v1/directory/verify \ -d '{"peppolId":"0208:9876543210","documentType":"INVOICE"}' -
Send
Pass a persistent
Idempotency-Key— we recommend your internal invoice row ID.curl -X POST …/v1/documents/send \ -H "Idempotency-Key: inv-row-42" \ -d @invoice.json -
Listen for
document.deliveredYour webhook will receive a signed POST as soon as the recipient's AP confirms.
POST /v1/documents/validate in CI. It catches BIS rule violations (BR-*), unreachable recipients, and currency mismatches without touching Peppol.
Receiving invoices
Incoming documents land as document.received webhooks. The workflow you want:
-
Subscribe once
curl -X POST …/v1/webhooks -d '{ "url":"https://example.com/hooks/peppol", "events":["document.received","document.updated","lifecycle.updated"] }' -
On delivery, verify the HMAC
See signing & verification. Constant-time compare, raw body.
-
Fetch the structured view
curl …/v1/documents/{id}/structuredPush that into your ERP, AP automation, or data warehouse.
-
Move the lifecycle along
Call
POST /v1/documents/{id}/lifecycleas the invoice is reviewed, approved, and paid. We handle regulatory reporting automatically.
Inbound: ERP webhooks → /v1/documents/send
POST /v1/documents/send doubles as Flowie's single inbound integration point. If your ERP, accounting platform, or homegrown system can fire an outbound webhook (every modern one can), point it at /v1/documents/send directly — or wire one Logic App / Power Automate flow / Lambda in between to translate the event payload. No "inbound webhook receiver" abstraction; the same endpoint that lets you send invoices over Peppol also accepts whatever your ERP fires at it.
- One mental model — your team learns "Flowie ingests at
/documents/send" and that's it. - Same idempotency, same auth, same lifecycle — whatever you push in flows through the regular pipeline (validation, Peppol routing where applicable, lifecycle state machine, webhooks back out to subscribers).
- Format flexibility — structured JSON, raw UBL XML, or a base64'd file (PDF / Factur-X / ZIP / image / proprietary). Sniff routes UBL through the validated path; everything else gets stored on the documents service with
deliveryStatus="stored".
The pattern
┌────────────────┐ webhook fires ┌─────────────────────┐ HTTPS POST ┌──────────────────┐
│ Your ERP / │ on invoice posted / │ Glue (Logic App, │ /v1/documents/send │ Flowie Exchange │
│ accounting SaaS│ ─────────────────────▶│ Power Automate, λ) │ ─────────────────▶│ (this API) │
└────────────────┘ PO confirmed, etc. └─────────────────────┘ bearer + idem └──────────────────┘
The glue layer is optional — many ERPs let you POST directly to a custom URL with a custom header. Use it when you need to map fields, transform payloads, or pull in attachments.
Pick a payload shape
| Choose | When | Body |
|---|---|---|
format=json |
You can map ERP fields (number, dates, lines, totals) to the Flowie schema. | { type, format:"json", from, to, document:{...} } |
format=ubl-xml |
Your ERP already renders Peppol BIS 3.0 / EN 16931 XML. | { type, format:"ubl-xml", from, to, xml:"<Invoice>..." } |
format=auto with file |
You have the rendered document (PDF, Factur-X PDF/A-3, attachment) and want Flowie to sniff the bytes — UBL XML routes through the validated pipeline; PDFs and images get stored as-is. | { type, format:"auto", from, to, file:{ content:<b64>, contentType, filename } } |
format=raw with file |
Audit-trail / archive / proprietary format you don't want Flowie to interpret. | Same as above; response carries deliveryStatus="stored" + fileId + storedFormat. |
Every combination — copy-paste recipes
Same endpoint, six document types, four payload shapes. The matrix below is exhaustive; pick the row that matches what your source system can produce.
| type | format | Body field | Response deliveryStatus | Sniff? |
|---|---|---|---|---|
invoice · credit-note · debit-note · purchase-order · sales-order · quote |
json | document | pending (Peppol-routed) | — |
| same six | ubl-xml | xml | pending (validated then routed) | — |
| same six | auto | file = UBL XML | pending (sniff → ubl-xml path) | UBL/CII detected |
| same six | auto | file = PDF / Factur-X | stored + fileId + storedFormat:"pdf" | %PDF- magic |
| same six | auto | file = PNG / JPEG | stored + storedFormat:"png"|"jpeg" | image magic bytes |
| same six | auto | file = ZIP | stored + storedFormat:"zip" | PK\x03\x04 magic |
| same six | auto | file = JSON / unknown | stored + storedFormat:"json"|"binary" | fall-through |
| same six | raw | file = anything | stored + storedFormat reflects bytes | none — never sniffed |
Shape A — structured JSON
You have the field-level data and want Flowie to render the UBL for you. Works for every type; only the type literal and a couple of cross-references change.
curl -X POST https://back.p2p-flowie.com/exchange/v1/documents/send \
-H "Authorization: Bearer $FLOWIE_API_KEY" \
-H "Idempotency-Key: invoice-row-12345" \
-H "Content-Type: application/json" \
-d '{
"type": "invoice", /* or credit-note | debit-note |
purchase-order | sales-order | quote */
"format": "json",
"from": "vat:BE0123456789",
"to": "0208:0123456789",
"document": {
"number": "INV-2026-0042",
"issueDate": "2026-04-30",
"dueDate": "2026-05-30",
"currency": "EUR",
"buyerReference": "PO-9988", /* aka Service Exécutant for FR PPF */
"orderReference": "QUO-1234", /* link to a quote / PO */
"seller": { "name": "ACME BVBA", "vatNumber": "BE0123456789" },
"buyer": { "name": "Globex SRL", "vatNumber": "IT12345678901" },
"payment": { "means": "credit_transfer", "iban": "BE68539007547034",
"bic": "GKCCBEBB", "reference": "INV-2026-0042" },
"lines": [
{ "description": "Consulting", "quantity": 10, "unit": "HUR",
"unitPrice": 150.00, "vatRate": 21 },
{ "description": "Travel", "quantity": 1, "unit": "C62",
"unitPrice": 320.00, "vatRate": 21 }
],
"allowances": [{ "amount": 50, "reason": "Loyalty discount" }],
"totals": { "netAmount": 1770.00, "vatAmount": 371.70,
"grossAmount": 2141.70 }
}
}'
Shape B — pre-rendered UBL / CII XML
Your ERP already emits Peppol BIS 3.0 / EN 16931 XML. Send the bytes inline; Flowie validates against the BIS schematron before delivery.
curl -X POST https://back.p2p-flowie.com/exchange/v1/documents/send \
-H "Authorization: Bearer $FLOWIE_API_KEY" \
-H "Idempotency-Key: invoice-row-12345" \
-d '{
"type": "invoice",
"format": "ubl-xml",
"from": "vat:BE0123456789",
"to": "0208:0123456789",
"xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\">...</Invoice>"
}'
CII (Cross-Industry Invoice) XML is also accepted — Flowie detects the namespace automatically. Validation errors come back as 422 with a schematronViolations list.
Shape C — file with format=auto (recommended)
The forgiving option. Encode any file as base64; Flowie sniffs the first 64 bytes for magic bytes and routes accordingly. UBL/CII XML auto-promotes to the validated pipeline; PDFs and images persist as-is. This is the right choice for ERP webhooks where you don't fully control what the source emits.
# PDF (typical AP/AR invoice scan or a Factur-X PDF/A-3)
curl -X POST https://back.p2p-flowie.com/exchange/v1/documents/send \
-H "Authorization: Bearer $FLOWIE_API_KEY" \
-H "Idempotency-Key: D365-{BusinessEventId}" \
-d '{
"type": "invoice",
"format": "auto",
"from": "vat:BE0123456789",
"to": "0208:0123456789",
"file": {
"content": "JVBERi0xLjQKJe...", // base64 PDF
"contentType": "application/pdf",
"filename": "INV-2026-0042.pdf"
}
}'
# Response (201):
# {
# "id": "doc_abc123",
# "status": "stored",
# "type": "invoice",
# "deliveryStatus": "stored",
# "fileId": "file_xyz",
# "storedFormat": "pdf",
# ...
# }
Same shape works for every supported file format — the sniffer outputs pdf, png, jpeg, gif, zip, ubl-xml, xml, json, or binary. When sniff returns ubl-xml the request transparently re-enters the UBL pipeline (validated, Peppol-routed) and the response is deliveryStatus="pending" instead.
Shape D — file with format=raw (archive only)
Skip the sniffer entirely — store the bytes verbatim. Useful for audit-trail copies, legacy formats Flowie shouldn't try to interpret, or when you simply want to park a document on the file API and retrieve it later via GET /v1/documents/{id}/pdf or /xml.
curl -X POST https://back.p2p-flowie.com/exchange/v1/documents/send \
-H "Authorization: Bearer $FLOWIE_API_KEY" \
-d '{
"type": "invoice",
"format": "raw",
"from": "vat:BE0123456789",
"to": "0208:0123456789",
"file": {
"content": "AQIDBAUG...", // base64 of anything
"contentType": "application/x-acme-format",
"filename": "legacy-export.acme"
}
}'
Six document types, one endpoint
Every shape above accepts any of the six document types. The type literal is the only thing that changes between them; lifecycle states differ accordingly (Quote → SO → PO → Invoice flow).
| type | Typical sender | Lifecycle entry | Cross-references |
|---|---|---|---|
invoice | Seller | issued | orderReference → PO |
credit-note | Seller | issued | originalInvoiceId → invoice |
debit-note | Seller | issued | originalInvoiceId → invoice |
purchase-order | Buyer | issued | orderReference → quote |
sales-order | Seller | issued | orderReference → PO |
quote | Seller | issued | — |
Batch — many docs in one call
The same endpoint exposes a batch sibling at POST /v1/documents/send/batch. Wrap up to 100 documents in a single request; each item gets its own idempotencyKey. The response carries per-item results so partial failures don't poison the whole batch.
curl -X POST https://back.p2p-flowie.com/exchange/v1/documents/send/batch \
-H "Authorization: Bearer $FLOWIE_API_KEY" \
-d '{
"documents": [
{ "idempotencyKey": "row-101", "type": "invoice",
"format": "auto", "from": "vat:BE0123456789", "to": "0208:0123456789",
"file": { "content": "JVBERi0xLjQK...", "contentType": "application/pdf",
"filename": "INV-101.pdf" } },
{ "idempotencyKey": "row-102", "type": "credit-note",
"format": "json", "from": "vat:BE0123456789", "to": "0208:0123456789",
"document": { /* ... */ } },
{ "idempotencyKey": "row-103", "type": "purchase-order",
"format": "ubl-xml", "from": "0208:0123456789", "to": "vat:FR12345678901",
"xml": "<?xml ..." }
]
}'
Microsoft Dynamics 365
D365 emits Business Events (VendorInvoicePostedV2, SalesInvoicePosted, FreeTextInvoiceCreated, SalesOrderConfirmed) that can target any HTTPS URL with OAuth or a custom header. Two flavours:
1. Power Automate flow — listens for the event, fetches the rendered invoice attachment, POSTs to Flowie:
POST https://back.p2p-flowie.com/exchange/v1/documents/send
Authorization: Bearer flw_live_…
Idempotency-Key: D365-{BusinessEventId}
{
"type": "invoice",
"format": "auto",
"from": "vat:FR12345678901",
"to": "0208:0123456789",
"file": {
"content": "JVBERi0xLjQKJ...", // base64 PDF or Factur-X
"contentType": "application/pdf",
"filename": "INV-{InvoiceNumber}.pdf"
}
}
2. Direct HTTPS target — register Flowie as a Business Event endpoint in D365 with an OAuth2 app registration. Use BusinessEventId as the idempotency key so retries are safe. If the customer prefers structured JSON over PDF, swap file for document.
SAP S/4HANA & Event Mesh
SAP Event Mesh emits topics like sap/s4/Invoice/Created/v1. Subscribe an HTTPS webhook target (or run a small consumer) and translate to /v1/documents/send. The structured JSON path is usually the right choice — S/4HANA's invoice payload maps cleanly to Flowie's document.lines.
@app.post("/sap/webhook")
async def sap_inbound(req: Request, x_event_type: str = Header()):
event = await req.json()
if x_event_type != "sap/s4/Invoice/Created/v1":
return Response(204)
payload = {
"type": "invoice",
"format": "json",
"from": f"vat:{event['SellingCompany']['VATId']}",
"to": f"vat:{event['BuyingCompany']['VATId']}",
"document": map_sap_to_flowie(event),
}
httpx.post(
"https://back.p2p-flowie.com/exchange/v1/documents/send",
json=payload,
headers={
"Authorization": f"Bearer {FLOWIE_API_KEY}",
"Idempotency-Key": f"SAP-{event['MessageId']}",
},
)
return Response(204)
NetSuite, Sage Intacct, custom
- NetSuite — User Event Script triggers on Record Type = Invoice. POST to Flowie from inside the SuiteScript using N/https. Use the NetSuite internal id as the idempotency key.
- Sage Intacct — Smart Events on Record Type = Invoice. Same shape.
- QuickBooks Online — webhook subscription on entity = Invoice. Pull the invoice via QBO API, then POST to Flowie.
- Custom / homegrown ERP — fire any HTTPS POST that ends up at
/v1/documents/send. As long as the bearer is valid and the body parses, Flowie ingests it.
Idempotency
Always set the Idempotency-Key header to a value derived from the source system — typically {system}-{eventId} (e.g. D365-{BusinessEventId}, SAP-{MessageId}, QBO-{webhookEventId}). Flowie caches the response for 24 hours, so a webhook retry with the same key returns the cached doc without duplicating it. Reference → Idempotency.
Retries & failures
Most ERPs retry on 5xx and stop on 4xx. Flowie returns:
- 201 — accepted; you have a doc id. Always idempotent on retry with the same
Idempotency-Key. - 400 / 422 — payload-level rejection (bad enum, missing required field, invalid base64, invalid date). Fix the mapping; retrying won't help.
- 413 — file exceeds 5 MiB. Strip the attachment or split.
- 429 — rate-limited. Honour
Retry-After. - 5xx — Flowie or downstream is degraded; safe to retry with backoff.
Every captured failure has a requestId in the body and is queryable at GET /v1/requests/{requestId} for the next 7 days — paste the id into a Slack thread and your teammate sees the same redacted envelope.
Test in sandbox
Bootstrap a sandbox key (POST /v1/sandbox/bootstrap) and point your ERP's webhook target at https://back.flowie.ink/exchange with the test bearer. Sandbox accepts every payload shape the production endpoint does and synthesises plausible doc ids without touching the live Peppol network — see sandbox shortcuts for the full list.
Three copy-pasteable end-to-end examples live in examples/ in the API repo. All bootstrap a sandbox key on the fly (or fall back to a long-lived sandbox key if the per-IP rate-limit kicks in), so they run zero-config:
| File | Scenarios | What it covers |
|---|---|---|
erp_inbound.py |
4 | Generic ERP inbound — one scenario per payload shape, mapped to D365 / SAP / NetSuite / QuickBooks. |
erp_inbound_pmu.py |
5 | PMU-specific — Hippodrome de Vincennes, Atos, Publicis, retail-point PO, audit event. Pinned to the PMU production org id. |
d365_event_inbound.py |
6 | D365 events that aren't invoices — SalesOrderConfirmed, PurchaseOrderApprovalDone, VendorPaymentJournalPosted, WorkflowCompletedV3, BetVolumeReported, BettingAgentRegistered. Uses format=raw to archive the JSON event payload. |
Run any of them with python examples/{file}.py; output shows the resulting doc_sbx_… ids and which shape was sniffed. Set FLOWIE_BASE + FLOWIE_API_KEY to point at production.
For PMU specifically: the step-by-step D365 admin guide at examples/d365-pmu-setup.md walks through which Business Events to activate, how to wire the HTTPS endpoint with OAuth, the Power Automate flows per event type, and the sandbox→prod cutover.
Tracking lifecycle, end to end
Status transitions are enforced — you can't skip from issued to paid. The happy path:
issued → under_review → approved → partially_paid? → paid
Side branches:
any-non-terminal → rejected (with reasonCode)
any-non-terminal → disputed (with reasonCode)
Keep your side in sync by acting on lifecycle.updated:
@app.post("/hooks/peppol")
async def hook(req: Request):
raw = await verify(req)
event = json.loads(raw)
if event["type"] == "lifecycle.updated":
d = event["data"]
db.execute(
"UPDATE invoices SET status=%s, updated_at=%s WHERE flowie_id=%s",
(d["currentStatus"], d["at"], d["documentId"]),
)
return Response(status_code=204)
Order integrations — Quote → SO → PO → Invoice
Flowie Exchange covers the full order-to-cash and source-to-pay chain. The same POST /v1/documents/send endpoint and lifecycle machinery handles every document type — only type and a couple of cross-references change. Six types are first-class:
| type | Direction | Lifecycle | Peppol BIS profile |
|---|---|---|---|
quote | Seller → Buyer | issued → accepted | rejected | — |
purchase-order | Buyer → Seller | issued → confirmed → fulfilled | urn:fdc:peppol.eu:poacc:trns:order:3 |
sales-order | Seller → Buyer | issued → confirmed → fulfilled | urn:fdc:peppol.eu:poacc:trns:order_response:3 |
invoice | Seller → Buyer | issued → under_review → approved → paid | urn:cen.eu:en16931:2017 (BIS 3.0) |
credit-note | Seller → Buyer | same as invoice | BIS 3.0 Credit Note |
debit-note | Seller → Buyer | same as invoice | BIS 3.0 Debit Note |
The chain
Each document references the previous one via orderReference (links a document to a PO/SO) or quoteReference (links a PO to its originating quote). Flowie carries those references through the whole chain so you can render an invoice and trace it back to the original quote in one query.
Typical S2P (source-to-pay) for a buyer:
quote (received)
↓ accepted → purchase-order (sent, orderReference="QUO-1234")
↓ confirmed → sales-order (received, orderReference="PO-5678")
↓ fulfilled → invoice (received, orderReference="PO-5678")
↓ approved → paid
Typical O2C (order-to-cash) for a seller:
quote (sent)
↓ accepted → purchase-order (received, quoteReference="QUO-1234")
↓ confirmed → sales-order (sent, orderReference="PO-5678")
↓ fulfilled → invoice (sent, orderReference="PO-5678")
Send a quote, then a PO
curl -X POST https://back.p2p-flowie.com/exchange/v1/documents/send \
-H "Authorization: Bearer $FLOWIE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "quote",
"from": "0009:FR12345678901",
"to": "0208:0123456789",
"document": {
"number": "QUO-2026-0042",
"issueDate": "2026-04-30",
"currency": "EUR",
"lines": [{ "description": "Consulting", "quantity": 10, "unitPrice": 150, "vatRate": 20 }]
}
}'
Once the buyer accepts and emits the PO, send it referencing the quote:
curl -X POST https://back.p2p-flowie.com/exchange/v1/documents/send \
-H "Authorization: Bearer $FLOWIE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "purchase-order",
"from": "0208:0123456789",
"to": "0009:FR12345678901",
"document": {
"number": "PO-2026-9988",
"issueDate": "2026-04-30",
"currency": "EUR",
"orderReference": "QUO-2026-0042",
"lines": [{ "description": "Consulting", "quantity": 10, "unitPrice": 150, "vatRate": 20 }]
}
}'
Three-way matching (PO ↔ SO ↔ Invoice)
When an invoice arrives that references a known PO, Flowie auto-matches lines by itemCode + quantity + unitPrice within a tolerance (configurable per organization). The result is exposed via the underlying tx-docs service:
curl https://back.p2p-flowie.com/exchange/v1/documents/{invoiceId}/structured \
-H "Authorization: Bearer $FLOWIE_API_KEY"
The response carries a matching object with per-line matchedQuantity / variance. Variance over the tolerance flips the invoice lifecycle to disputed with reasonCode QUA (quantity) or PRI (price). Approve or override via POST /v1/documents/{id}/lifecycle.
Webhook events
Every order document fires the same envelope as invoices, qualified by data.type:
document.received { data: { type: "purchase-order", ... } }
document.delivered { data: { type: "sales-order", ... } }
lifecycle.updated.confirmed { data: { documentType: "PURCHASE_ORDER", currentStatus: "confirmed" } }
lifecycle.updated.fulfilled { data: { documentType: "SALES_ORDER", currentStatus: "fulfilled" } }
If you only care about orders (not invoices), filter at subscription time:
curl -X POST https://back.p2p-flowie.com/exchange/v1/webhooks \
-H "Authorization: Bearer $FLOWIE_API_KEY" \
-d '{
"url": "https://yourapp.example.com/hooks/orders",
"events": ["document.received", "lifecycle.updated.confirmed", "lifecycle.updated.fulfilled"],
"filter": { "documentType": ["PURCHASE_ORDER", "SALES_ORDER", "QUOTE"] }
}'
Test in sandbox
The sandbox simulators (0208:SIM_HAPPY, SIM_DISPUTE, SIM_PARTIAL) drive the full chain — sending a PO to SIM_HAPPY auto-emits the matching sales-order from the simulated counterparty 5–10 seconds later, then the invoice 30 seconds after. Use POST /v1/sandbox/clock/advance to skip the wait. See sandbox shortcuts for the full list of synthesized behaviours.
Building a white-label platform
If you're an ERP, an accounting SaaS, or a public-sector aggregator, you'll run Flowie under your own brand. The model is "Stripe Connect for Peppol":
- You hold one platform key (
flw_plat_live_…orflw_wl_live_…). - For each tenant customer, you onboard a managed company.
- You can either keep acting on their behalf (
X-Flowie-Company) or issue a tenant-scoped key they use directly.
Onboarding in one shot
curl -X POST …/v1/platform/companies \
-H "Authorization: Bearer flw_plat_live_xyz" \
-d '{
"vatNumber":"FR86797978996",
"receiveDocuments":true,
"webhook":{"url":"https://erp.acme.fr/hooks/flowie","events":["*"]},
"apiKey":{"name":"tenant-acme","scopes":["send","documents.read","lifecycle"]}
}'
The response gives you the tenant's company object, a freshly minted API key (once-shown), and the configured webhook. Save the key in your tenant's secret store.
Scoping requests to a tenant
Two ways. Pick based on your threat model:
| Pattern | When | Pros / Cons |
|---|---|---|
Platform key + X-Flowie-Company |
You hold one key in your own vault, act on each tenant. | Fewer secrets to manage · but one key compromise = all tenants. |
| Per-tenant key | Tenants directly hit the API from their stack. | Blast radius limited to one tenant · but you must manage rotation. |
Branding & custom domain
PATCH /v1/platform/settings lets you set a logo, primary color, and a customDomain (peppol.yourbrand.com). TLS is provisioned automatically.
Compliance — 47 countries across Europe, MENA, and Asia-Pacific
Flowie covers 47 jurisdictions on four continents — every EU member plus Norway, Iceland, Liechtenstein, the UK, and Switzerland in Europe; Saudi Arabia, the UAE, Israel, Egypt, and Türkiye in the Middle East; India, Singapore, Malaysia, Thailand, and Vietnam in South / SE Asia; Japan, South Korea, and China in East Asia; Australia and New Zealand in the Pacific. Each country has its own dedicated page with mandate timeline, format profile, required fields, error codes, primary government sources, and sandbox shortcuts:
📋 Compliance overview — coverage map across all 47 countries (Europe, MENA, APAC) →
Coverage model: Flowie operates a registered Peppol Access Point directly where we hold national accreditation, and integrates via a vetted local partner registered with the in-country regulator (KSeF, SDI intermediario, ZATCA service-provider, ASP, etc.) for jurisdictions that require an in-country provider. Either way, you call the same POST /v1/documents/send.
Quick highlights — the regimes you're most likely to encounter
🇫🇷 France — PPF + PA / PDP
Mandatory for domestic B2B from September 2026 (receive) and September 2027 (send). Flowie is a registered Plateforme Agréée (PA) — number 0040. The DGFiP renamed PDP → PA in 2025; both labels refer to the same accreditation. Lifecycle transitions (approved, rejected, paid) are auto-reported to PPF within 2 minutes. Public-sector recipients require a buyerReference (Service Exécutant) — without it, PPF rejects with code 00058. Full deep-dive →
🇮🇹 Italy — SDI (Sistema di Interscambio)
Mandatory since 2019 for B2B, B2C, and B2G. Flowie routes through its own SDI adapter; you never talk to SDI directly. A SDI rejection surfaces as a document.failed webhook with the native SDI error code. Full deep-dive →
🇧🇪 Belgium — Pure Peppol since 2026-01-01
Belgium decommissioned HERMES on 2025-12-31; the B2B mandate (Loi du 6 février 2024) is delivered exclusively over Peppol BIS Billing 3.0 with the BE-CIUS profile — exactly the network Flowie already routes on. Full deep-dive →
🇩🇪 Germany — Wachstumschancengesetz (B2B phasing 2025–2028)
Receive obligation universal since 1 January 2025; send obligation phases by company size — large from 2027, all from 2028. XRechnung (XML, federal-favoured) and ZUGFeRD/Factur-X (PDF/A-3 hybrid, B2B-favoured). Full deep-dive →
🇪🇸 Spain — Veri*Factu + Crea y Crece + FACe
Veri*Factu corporate live since July 2025; Crea y Crece B2B mandate phasing 2026–2028. FACe handles B2G. Three obligations layered, all handled from the same JSON. Full deep-dive →
🇵🇱 Poland — KSeF mandatory clearance
Large taxpayers from 1 February 2026; all VAT taxpayers from 1 April 2026. Clearance regime — invoices not legally valid until KSeF returns a number. FA(2) format mandatory. Full deep-dive →
🇷🇴 Romania — RO e-Factura
Universal B2B clearance since July 2024 — the most aggressive timeline in the EU. ANAF returns a signed XML before legal delivery. Full deep-dive →
🇸🇦 Saudi Arabia — ZATCA Fatoora
Real-time clearance through the Fatoora portal. Phase 1 (Generation) universal since December 2021; Phase 2 (Integration) ramps by wave through 30 June 2026 (Wave 24 captures every taxpayer with revenue > SAR 375,000). UBL 2.1 with KSA-specific extensions (TLV QR code, cryptographic stamp, hash chain). Full deep-dive →
🇦🇪 UAE — Peppol 5-corner with FTA
First MENA country to adopt the Peppol 5-corner model — sender AP, receiver AP, plus a real-time copy to the FTA's Data Reporting Platform. Phase 1 (revenue > AED 50m + government) live 1 July 2026; full rollout by July 2027. PINT AE format. Full deep-dive →
🇮🇱 Israel — ITA allocation-number clearance
SHAAM clearance returns an allocation number; without it, the buyer cannot deduct input VAT. Threshold tightens fast: NIS 10,000 from January 2026, NIS 5,000 from June 2026 — effectively all VAT B2B. Full deep-dive →
🇮🇳 India — GST IRP & IRN
Every B2B invoice from a taxpayer above ₹5 cr turnover must be cleared by an IRP (Invoice Registration Portal); response carries an IRN + signed QR code. Taxpayers ≥ ₹10 cr have a 30-day reporting deadline from issue. Multiple IRPs in operation; Flowie load-balances. Full deep-dive →
🇸🇬 Singapore — Peppol InvoiceNow + GST 5-corner
Newly incorporated GST registrants must comply from 1 April 2026; existing businesses absorbed in waves through April 2031. PINT-SG format. IMDA = Peppol Authority; IRAS receives the 5th-corner copy. Full deep-dive →
🇲🇾 Malaysia — LHDN MyInvois
Real-time clearance via MyInvois — UUID + QR code returned for embedding. Final wave 1 January 2026 covers RM 1m–5m taxpayers; SMEs below RM 1m are exempt (cabinet raised the floor in December 2025). Full deep-dive →
🇦🇺 Australia + 🇳🇿 New Zealand — Peppol PINT A-NZ
Joint trans-Tasman CIUS. Australia's ATO is the Peppol Authority (federal NCEs Peppol-default by Dec 2026, no B2B mandate). New Zealand's MBIE makes large suppliers (revenue > NZ$33m) Peppol-mandatory from 1 January 2027. Mandated NZ agencies pay 95% of Peppol invoices in 5 business days. AU → NZ →
🇯🇵 Japan — JP PINT & Qualified Invoice
Qualified Invoice System mandatory since October 2023 (T-prefixed registration numbers). Peppol JP PINT recommended but voluntary. The lever Japan uses is tax economics: input-tax credit on non-qualified invoices drops to 50% in Oct 2026, 0% in Oct 2029. Full deep-dive →
🇨🇳 China — Fully Digital e-fapiao + Golden Tax IV
Fully digital e-fapiao universal since 2024–2025; new VAT Law supporting regulations effective 1 January 2026. Every fapiao is issued through the STA platform — there is no off-platform legal invoice. Full deep-dive →
Real-time reporting regimes
Greece (myDATA), Hungary (NAV Online Számla), Spain (Veri*Factu), Korea (NTS HomeTax), and Türkiye (e-Arşiv) all require near-real-time invoice reporting. Flowie ships the reporting envelope on every send.
The remaining 30+ countries — Austria, Bulgaria, Croatia, Cyprus, Czechia, Denmark, Estonia, Finland, Greece, Hungary, Iceland, Ireland, Latvia, Liechtenstein, Lithuania, Luxembourg, Malta, Netherlands, Norway, Portugal, Slovakia, Slovenia, Sweden, Switzerland, UK, Egypt, Vietnam, Thailand, Türkiye — are documented in full in the coverage map.
Pure-Peppol countries
The Netherlands, Sweden, Norway, Austria, Ireland, Cyprus, Malta, Luxembourg, Latvia, Belgium and others run no central hub — Peppol AP-to-AP delivery is the entire mandate. From a caller perspective, just POST /v1/documents/send; nothing extra to configure. See the overview map for which countries fall into this bucket.
paymentDate is later than issueDate by > 90 days, SDI flags it as late-payment. If your currency differs from the original invoice, PPF rejects the report. Validate before calling /lifecycle.
Sandbox testing
Everything behaves identically to production — except no real Peppol delivery happens. Base URL: https://back.flowie.ink, keys start with flw_test_.
Test VAT numbers
| VAT | Behavior |
|---|---|
BE0000000001 | Always enriches successfully. |
BE0000000099 | Returns VAT_INACTIVE. |
BE0000000404 | Returns VAT_NOT_FOUND. |
Test Peppol IDs
| Peppol ID | Behavior |
|---|---|
0208:TEST_OK | Delivers successfully after ~1s. |
0208:TEST_AP_FAIL | Fires document.failed after ~2s (simulated AP rejection). |
0208:TEST_TIMEOUT | Simulates a transport timeout; retries then fails. |
Triggering webhook replays
Any event delivered to a sandbox webhook has a Resend button in the dashboard. The replayed request is byte-identical to the original — perfect for testing signature verification.
Going-live checklist
| ✓ | Item | Why it matters |
|---|---|---|
| ☐ | Switch base URL to https://back.p2p-flowie.com | You'd be surprised. |
| ☐ | Swap test key for live key | flw_test_… → flw_live_…. |
| ☐ | Register live webhooks with fresh secrets | Don't reuse sandbox secrets in production. |
| ☐ | Run a canary invoice to your own Peppol ID | End-to-end smoke test on real infrastructure. |
| ☐ | Set up monitoring on document.failed + compliance.reported.failed | You want to hear about delivery issues before your customer does. |
| ☐ | Implement Retry-After backoff | Graceful behavior under rate-limits. |
| ☐ | Persist Idempotency-Key per outgoing row | Safe retries across deploys. |
| ☐ | Store requestId in your application logs | First thing support asks for. |
| ☐ | Subscribe to status.flowie.ink | Catch upstream (SMP, PPF, SDI) incidents. |
| ☐ | Document your error → UI message mapping | Surface user-facing errors cleanly. |
| ☐ | Plan for v1 deprecation (12-month horizon) | Watch the changelog. |
Migrating from v2
If you were on the legacy /api/… surface, here's the mapping for the 5 biggest changes in v3:
| v2 | v3 | Note |
|---|---|---|
POST /api/send | POST /v1/documents/send | Body shape unchanged; add type: "invoice". |
GET /api/invoices | GET /v1/documents?type=invoice | Unified list across document types. |
POST /api/invoices/{id}/paid | POST /v1/documents/{id}/lifecycle with status:"paid" | State machine replaces ad-hoc endpoints. |
GET /api/peppol/search | GET /v1/directory/search | Identical params. |
POST /api/webhooks | POST /v1/webhooks | Event names normalized; see catalog. |
v2 stays online until 2027-04-01. After that, requests to /api/… return 410 Gone.
