---
title: "Integration Guides"
description: "Step-by-step playbooks for sending, receiving, going live, and building white-label products on Flowie Exchange."
canonical: "https://docs.get-flowie.com/guides/"
source: "https://docs.get-flowie.com/guides/index.html"
---

# Integration Guides

Guides

# Integration playbooks

Short, opinionated, end-to-end recipes for the six tasks most teams do in their first month.

## Sending invoices over Peppol

Register the sender, verify the recipient, `POST /v1/documents/send`, watch the delivery webhook. **[Read the full guide → Send an invoice over Peppol](<send-invoice.html>)**

## Receiving invoices

Incoming documents land as `document.received` webhooks: subscribe once, verify the HMAC, fetch the structured view, advance the lifecycle — or poll `GET /v1/documents` if you cannot expose an endpoint. **[Read the full guide → Receive invoices](<receive-invoices.html>)**

## Changing platform — portability 🇫🇷 France only

A French taxpayer may change _Plateforme Agréée_ at any time and **keeps its SIREN/SIRET addressing** , so nothing downstream has to be re-addressed. Import the company from its SIRET — one call, or a whole client book with `POST /v1/companies/import/batch` — then let Flowie build and parse the normalised inter-PA message. The clocks are the hard part: **24 h to acknowledge** , **5 _jours ouvrés_ to decide**, and past that delay _le silence vaut accord_. This flow is PPF-specific: outside France there is no PA to leave. **[Read the full guide → Portability (change of PA)](<portability.html>)**

Leaving a provider **outside** France is a different job: a registry edit, an authorisation to re-grant at the tax authority, and an archive to get back. What that takes per country — and a request form that needs no API key — is on its own page. **[Read the full guide → Changing platform in Europe](<portability-europe.html>)**

## 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 and reads a Factur-X or CII invoice into a structured document; everything else gets stored on the documents service with `deliveryStatus="stored"`.



### The pattern
[code] 
    ┌────────────────┐    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    └──────────────────┘
[/code]

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, a Factur-X PDF/A-3 or a CII invoice is read into a structured document; other 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`.  
URL query params + raw body | ERP webhooks where you want a **fixed URL** and the source system POSTs its native event JSON verbatim — no wrapping, no base64, no Power Automate transformation. The wrapper constants travel as query params. | `POST /v1/documents/send?type=event&from=vat:…` \+ `Content-Type` header + body = the ERP payload byte-for-byte. See [Shape E](<#ingest-shape-rawbody>).  
  
### 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  
invoice / credit-note / debit-note| `auto`| `file` = Factur-X or CII| `pending` (or `awaiting_registration`) + the CII `number`| `%PDF-` with an embedded CII, or a `CrossIndustryInvoice` root  
same six| `auto`| `file` = other PDF| `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.
[code] 
    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 }
        }
      }'
[/code]

#### 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.
[code] 
    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>"
      }'
[/code]

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.**
[code] 
    # 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",
    #   ...
    # }
[/code]

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`.
[code] 
    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"
        }
      }'
[/code]

#### Shape E — URL query params + raw body (recommended for ERP webhooks)

The most permissive option for ERP integrations that emit native event JSON and want a fixed webhook URL with zero body wrapping. The constants (`type`, `from`, optional `contentType`, `filename`) travel as URL query parameters; the request body is the native ERP payload, byte-for-byte. The server reads `request.body()`, wraps internally, and routes through the same pipeline as Shapes A–D. **This is what you want when D365 / SAP / NetSuite Business Events should POST their payload verbatim without a Power Automate / iPaaS transformation step.**
[code] 
    curl -X POST "https://back.p2p-flowie.com/exchange/v1/documents/send?type=event&from=vat:FR53309136540" \
      -H "Authorization: Bearer $FLOWIE_API_KEY" \
      -H "Idempotency-Key: D365-SalesOrderConfirmed-{eventId}" \
      -H "Content-Type: application/json" \
      -d '{
        "BusinessEventId":  "SalesOrderConfirmed",
        "SalesOrderId":     "SO-2026-0042",
        "CustomerAccount":  "RETAIL-PV-75008",
        "TotalAmount":      6480.00,
        "Currency":         "EUR"
      }'
[/code]

Supported query parameters:

  * `type` (required to trigger raw-body mode) — same enum as the JSON body field (`invoice`, `credit-note`, …, `event`). For non-Peppol audit/observability events, use `type=event`.
  * `from` (required when type ≠ event) — sender identifier (`vat:…`, `0009:…`, `peppol:…`, or `comp_…`). For `type=event`, defaults to `org:{actingOrgId}` if omitted.
  * `contentType` (optional) — overrides the request `Content-Type` header for the stored file. Useful when the payload's true media type doesn't match the wire `Content-Type`.
  * `filename` (optional) — explicit stored filename. Defaults to `{Idempotency-Key}.bin` or a random UUID-based name.



Detection: the server activates raw-body mode **iff** the URL contains `?type=…`. When no query params are present, the existing JSON body schema applies (Shapes A–D) — zero regression. Same `SendDocumentResponse` shape comes back regardless of which mode you used.

D365-specific recipe: configure the Business Event HTTPS endpoint with the URL above, set `{{EventPayload}}` as the request body, leave OAuth2 auth at the header level. Nothing else to map — no Power Automate flow, no body template, no base64.

#### 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](<#order-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`](<../reference/index.html#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.
[code] 
    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 ..." }
        ]
      }'
[/code]

### 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:
[code] 
    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"
      }
    }
[/code]

**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`.

**3\. Direct HTTPS target — native EventPayload (recommended for non-Peppol events)**. For Business Events that aren't invoices (`SalesOrderConfirmed`, `PurchaseOrderApprovalDone`, `VendorPaymentJournalPosted`, `WorkflowCompletedV3`, plus customer-specific events like `BetVolumeReported` or `BettingAgentRegistered`), point D365 directly at the [Shape E URL](<#ingest-shape-rawbody>) with the wrapper constants in the query string. The Business Event's native `EventPayload` goes verbatim as the request body — no Power Automate flow, no template, no base64.
[code] 
    POST https://back.p2p-flowie.com/exchange/v1/documents/send?type=event&from=vat:FR12345678901
    Authorization: Bearer flw_live_…
    Idempotency-Key: D365-{BusinessEventId}-{EventId}
    Content-Type: application/json
    
    {{EventPayload}}     // ← native D365 event JSON, no transformation
[/code]

### 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`.
[code] 
    @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)
[/code]

### 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](<../reference/index.html#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}`](<../reference/index.html#request-inspector>) 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](<../sandbox/index.html#sandbox-shortcuts>) for the full list.

Runnable demos

Three copy-pasteable end-to-end examples live in [`examples/`](<https://github.com/FlowieAI/exchange-api/tree/main/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`](<https://github.com/FlowieAI/exchange-api/blob/main/examples/erp_inbound.py>) | 4 | Generic ERP inbound — one scenario per payload shape, mapped to D365 / SAP / NetSuite / QuickBooks.  
[`erp_inbound_pmu.py`](<https://github.com/FlowieAI/exchange-api/blob/main/examples/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`](<https://github.com/FlowieAI/exchange-api/blob/main/examples/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`](<https://github.com/FlowieAI/exchange-api/blob/main/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:
[code] 
    issued → under_review → approved → partially_paid? → paid
[/code]

Side branches:
[code] 
    any-non-terminal → rejected (with reasonCode)
    any-non-terminal → disputed (with reasonCode)
[/code]

Keep your side in sync by acting on `lifecycle.updated`:
[code] 
    @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)
[/code]

## 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:
[code] 
    quote (received)
      ↓ accepted →  purchase-order (sent, orderReference="QUO-1234")
                     ↓ confirmed →  sales-order (received, orderReference="PO-5678")
                                    ↓ fulfilled →  invoice (received, orderReference="PO-5678")
                                                    ↓ approved → paid
[/code]

Typical O2C (order-to-cash) for a seller:
[code] 
    quote (sent)
      ↓ accepted →  purchase-order (received, quoteReference="QUO-1234")
                     ↓ confirmed →  sales-order (sent, orderReference="PO-5678")
                                    ↓ fulfilled →  invoice (sent, orderReference="PO-5678")
[/code]

### Send a quote, then a PO
[code] 
    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 }]
        }
      }'
[/code]

Once the buyer accepts and emits the PO, send it referencing the quote:
[code] 
    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 }]
        }
      }'
[/code]

### 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:
[code] 
    curl https://back.p2p-flowie.com/exchange/v1/documents/{invoiceId}/structured \
      -H "Authorization: Bearer $FLOWIE_API_KEY"
[/code]

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`](<../reference/index.html#update-lifecycle>).

### Webhook events

Every order document fires the same envelope as invoices, qualified by `data.type`:
[code] 
    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" } }
[/code]

If you only care about orders (not invoices), filter at subscription time:
[code] 
    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"] }
      }'
[/code]

### 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`](<../sandbox/index.html#test-clock>) to skip the wait. See [sandbox shortcuts](<../sandbox/index.html#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_…` or `flw_wl_live_…`).
  * For each tenant customer, you [onboard](<../reference/index.html#platform-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
[code] 
    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"]}
      }'
[/code]

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`](<../reference/index.html#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) →**](<../compliance/index.html>)

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 `0064`. _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 →](<../compliance/fr/index.html>)

#### 🇮🇹 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 →](<../compliance/it/index.html>)

#### 🇧🇪 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 →](<../compliance/be.html>)

#### 🇩🇪 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 →](<../compliance/de.html>)

#### 🇪🇸 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 →](<../compliance/es.html>)

#### 🇵🇱 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 →](<../compliance/pl.html>)

#### 🇷🇴 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 →](<../compliance/ro.html>)

#### 🇸🇦 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 →](<../compliance/sa.html>)

#### 🇦🇪 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 →](<../compliance/ae.html>)

#### 🇮🇱 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 →](<../compliance/il.html>)

#### 🇮🇳 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 →](<../compliance/in.html>)

#### 🇸🇬 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 →](<../compliance/sg.html>)

#### 🇲🇾 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 →](<../compliance/my.html>)

#### 🇦🇺 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 →](<../compliance/au.html>) [NZ →](<../compliance/nz.html>)

#### 🇯🇵 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 →](<../compliance/jp.html>)

#### 🇨🇳 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 →](<../compliance/cn.html>)

### Real-time reporting regimes

Greece ([myDATA](<../compliance/gr.html>)), Hungary ([NAV Online Számla](<../compliance/hu.html>)), Spain ([Veri*Factu](<../compliance/es.html>)), Korea ([NTS HomeTax](<../compliance/kr.html>)), and Türkiye ([e-Arşiv](<../compliance/tr.html>)) 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](<../compliance/index.html>).

### 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](<../compliance/index.html>) 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/exchange`, 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/exchange`| 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](<https://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](<../changelog.html>).  
  
## 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](<../reference/webhooks.html#events>).  
  
v2 stays online until **2027-04-01**. After that, requests to `/api/…` return `410 Gone`.
