FlowieExchange
Guides

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.

  1. 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"}'
  2. Verify the recipient

    Always verify before sending to avoid bounced deliveries.

    curl -X POST …/v1/directory/verify \
      -d '{"peppolId":"0208:9876543210","documentType":"INVOICE"}'
  3. 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
  4. Listen for document.delivered

    Your webhook will receive a signed POST as soon as the recipient's AP confirms.

Pre-flight checks before switching a customer live
Run the payload through 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:

  1. Subscribe once

    curl -X POST …/v1/webhooks -d '{
      "url":"https://example.com/hooks/peppol",
      "events":["document.received","document.updated","lifecycle.updated"]
    }'
  2. On delivery, verify the HMAC

    See signing & verification. Constant-time compare, raw body.

  3. Fetch the structured view

    curl …/v1/documents/{id}/structured

    Push that into your ERP, AP automation, or data warehouse.

  4. Move the lifecycle along

    Call POST /v1/documents/{id}/lifecycle as 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.

Why one endpoint instead of a separate "inbound" route?
  • 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

ChooseWhenBody
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.

typeformatBody fieldResponse deliveryStatusSniff?
invoice · credit-note · debit-note · purchase-order · sales-order · quote jsondocumentpending (Peppol-routed)
same sixubl-xmlxmlpending (validated then routed)
same sixautofile = UBL XMLpending (sniff → ubl-xml path)UBL/CII detected
same sixautofile = PDF / Factur-Xstored + fileId + storedFormat:"pdf"%PDF- magic
same sixautofile = PNG / JPEGstored + storedFormat:"png"|"jpeg"image magic bytes
same sixautofile = ZIPstored + storedFormat:"zip"PK\x03\x04 magic
same sixautofile = JSON / unknownstored + storedFormat:"json"|"binary"fall-through
same sixrawfile = anythingstored + storedFormat reflects bytesnone — 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).

typeTypical senderLifecycle entryCross-references
invoice SellerissuedorderReference → PO
credit-note SellerissuedoriginalInvoiceId → invoice
debit-note SellerissuedoriginalInvoiceId → invoice
purchase-order Buyer issuedorderReference → quote
sales-order SellerissuedorderReference → PO
quote Sellerissued

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

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:

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.

Runnable demos

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:

FileScenariosWhat 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:

typeDirectionLifecyclePeppol BIS profile
quoteSeller → Buyerissued → accepted | rejected
purchase-orderBuyer → Sellerissued → confirmed → fulfilledurn:fdc:peppol.eu:poacc:trns:order:3
sales-orderSeller → Buyerissued → confirmed → fulfilledurn:fdc:peppol.eu:poacc:trns:order_response:3
invoiceSeller → Buyerissued → under_review → approved → paidurn:cen.eu:en16931:2017 (BIS 3.0)
credit-noteSeller → Buyersame as invoiceBIS 3.0 Credit Note
debit-noteSeller → Buyersame as invoiceBIS 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":

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:

PatternWhenPros / 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.

Reporting is automatic — but your data must be clean
If 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

VATBehavior
BE0000000001Always enriches successfully.
BE0000000099Returns VAT_INACTIVE.
BE0000000404Returns VAT_NOT_FOUND.

Test Peppol IDs

Peppol IDBehavior
0208:TEST_OKDelivers successfully after ~1s.
0208:TEST_AP_FAILFires document.failed after ~2s (simulated AP rejection).
0208:TEST_TIMEOUTSimulates 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

ItemWhy it matters
Switch base URL to https://back.p2p-flowie.comYou'd be surprised.
Swap test key for live keyflw_test_…flw_live_….
Register live webhooks with fresh secretsDon't reuse sandbox secrets in production.
Run a canary invoice to your own Peppol IDEnd-to-end smoke test on real infrastructure.
Set up monitoring on document.failed + compliance.reported.failedYou want to hear about delivery issues before your customer does.
Implement Retry-After backoffGraceful behavior under rate-limits.
Persist Idempotency-Key per outgoing rowSafe retries across deploys.
Store requestId in your application logsFirst thing support asks for.
Subscribe to status.flowie.inkCatch upstream (SMP, PPF, SDI) incidents.
Document your error → UI message mappingSurface 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:

v2v3Note
POST /api/sendPOST /v1/documents/sendBody shape unchanged; add type: "invoice".
GET /api/invoicesGET /v1/documents?type=invoiceUnified list across document types.
POST /api/invoices/{id}/paidPOST /v1/documents/{id}/lifecycle with status:"paid"State machine replaces ad-hoc endpoints.
GET /api/peppol/searchGET /v1/directory/searchIdentical params.
POST /api/webhooksPOST /v1/webhooksEvent names normalized; see catalog.

v2 stays online until 2027-04-01. After that, requests to /api/… return 410 Gone.