Flowie Exchange API
The Flowie Exchange API is a single REST API for sending, receiving, and managing electronic invoices over the Peppol network. It covers 47 countries across Europe, MENA, and Asia-Pacific, handles regulatory compliance reporting automatically, and scales from a freelancer sending one invoice per month to a white-label platform managing thousands of tenant companies.
https://back.p2p-flowie.com in production · https://back.flowie.ink in sandbox. All endpoints below are prefixed with /v1.
Quick index
Authentication
Every request must carry a bearer token. Flowie Exchange supports two kinds of credentials; pick whichever matches your caller.
Flowie JWT
If the caller is a Flowie dashboard user, pass the Auth0-issued JWT you already use elsewhere. The organization is resolved from the _permissions claim.
Switching organizations
JWTs typically grant access to multiple organizations (the user's _permissions claim is a dict of org_id → permissions). By default the API picks the first one in that dict. To act as a specific organization, pass the X-Flowie-Organization-Id header on every request:
curl https://back.p2p-flowie.com/exchange/v1/documents \
-H "Authorization: Bearer eyJhbGc..." \
-H "X-Flowie-Organization-Id: 685a5670efafaa26ebf0128e"
The header is validated against the JWT's _permissions: passing an org the token doesn't grant returns 403. Organization-Id (the legacy name used by the AFNOR routes) is also accepted as an alias.
To list every org a caller can switch to, hit GET /v1/me. The Flowie docs auth widget uses this endpoint to render the org-picker dropdown next to your email.
API keys are bound to a single org at creation time and ignore this header.
Exchange API keys
For programmatic access, issue an Exchange API key from the dashboard or via the API. Keys are prefixed so you can tell them apart at a glance:
-
flw_live_…Personal key
Scoped to a single company. Use for server-to-server calls from your own stack.
-
flw_plat_live_…Platform key
Scoped to an organization that manages other companies. Combine with
X-Flowie-Companyto act on behalf of a tenant. -
flw_wl_live_…White-label key
Same as a platform key, plus the ability to customize branding, quotas, and settings per tenant.
-
flw_test_…Sandbox key
Any of the above with
_test_in the prefix hits sandbox. No real Peppol delivery.
Scopes
Keys carry a list of scopes. Use * only for full-access keys you control end-to-end; prefer the narrowest set your workload needs.
send receive
documents.read documents.search documents.write
companies.read companies.write
directory partners payments
lifecycle compliance stats platform
*
curl https://back.p2p-flowie.com/exchange/v1/companies \
-H "Authorization: Bearer flw_live_abc123"
from httpx import Client
api = Client(
base_url="https://back.p2p-flowie.com/exchange/v1",
headers={"Authorization": "Bearer flw_live_abc123"},
)
const headers = { Authorization: "Bearer flw_live_abc123" };
const res = await fetch(
"https://back.p2p-flowie.com/exchange/v1/companies",
{ headers }
);
req, _ := http.NewRequest("GET",
"https://back.p2p-flowie.com/exchange/v1/companies", nil)
req.Header.Set("Authorization", "Bearer flw_live_abc123")
Acting on a managed company (platform keys)
Authorization: Bearer flw_plat_live_xyz789
X-Flowie-Company: comp_abc123def456
Get caller identity + accessible orgs
Returns who the caller is, which organizations they can act as, and the active org for the current request. Works with both JWT and API-key auth. Used by the docs auth widget to render the organization-switcher dropdown.
Returns
{
"authMethod": "jwt",
"userId": "user_…",
"email": "alice@example.com",
"keyType": "jwt",
"organizationId": "org_685a5670efafaa26ebf0128e",
"organizationIds": ["org_685a…", "org_72b1…"],
"organizations": [
{ "id": "org_685a…", "name": "PMU", "country": "FR", "vatNumber": "FR12345678901" },
{ "id": "org_72b1…", "name": "Subsidiary", "country": "FR", "vatNumber": "FR98765432109" }
],
"scopes": ["*"],
"isTestMode": false
}
curl https://back.p2p-flowie.com/exchange/v1/me \
-H "Authorization: Bearer eyJhbGc..."
Idempotency
Network calls are imperfect. Any POST in this API accepts an Idempotency-Key header; if a request with that key has already completed in the last 24 hours, we return the original response byte-for-byte instead of acting again.
- Keys are strings, up to 255 characters. UUID v4 works great.
- Cache TTL is 24 hours. After that, a repeated key is treated as new.
- If you retry before the first response has finished processing, you'll get a
409 idempotency_in_progress. Retry in a moment. - Mutating a request under the same key is never allowed. We compare the full body hash — mismatched retries return
422 idempotency_body_mismatch.
curl -X POST …/v1/documents/send \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d @invoice.json
from uuid import uuid4
key = str(uuid4()) # persist this alongside the row!
resp = api.post("/documents/send",
headers={"Idempotency-Key": key}, json=payload)
const key = crypto.randomUUID();
await flowie("/documents/send", {
method: "POST",
headers: { "Idempotency-Key": key },
body: JSON.stringify(payload),
});
Pagination
All list endpoints are cursor-paginated. Don't hard-code offsets — the cursor is an opaque server-issued token and will change format without notice.
-
limitintegeroptional
Page size. Default
20, max100. -
cursorstringoptional
Pass the
cursorvalue returned by the previous page. Omit to start at the first page.
Every list response has the same envelope:
{
"data": [ /* records */ ],
"hasMore": true,
"cursor": "eyJpZCI6ImRvY19YLi4uIn0"
}
Iterate all pages
cursor = None
while True:
params = {"limit": 100}
if cursor: params["cursor"] = cursor
page = api.get("/documents", params=params).json()
for doc in page["data"]:
process(doc)
if not page["hasMore"]: break
cursor = page["cursor"]
let cursor;
do {
const q = new URLSearchParams({ limit: "100" });
if (cursor) q.set("cursor", cursor);
const page = await flowie(`/documents?${q}`);
page.data.forEach(process);
cursor = page.hasMore ? page.cursor : null;
} while (cursor);
Rate limits & quotas
Rate limits are enforced with a 60-second sliding window per key. Quotas are enforced monthly per organization. Both depend on your plan:
| Plan | Requests / min | Documents / month |
|---|---|---|
| Free | 60 | 50 |
| Starter | 120 | 500 |
| Pro | 300 | 5,000 |
| Platform | 600 | 50,000 |
| White-label | 1,200 | Unlimited |
Every response includes the current state:
-
X-RateLimit-Limitinteger
Requests allowed in the current 60-second window.
-
X-RateLimit-Remaininginteger
Requests left before you're throttled.
-
X-RateLimit-Resetunix timestamp
When the window rolls over.
-
Retry-Afterseconds
Present only on
429. How long to wait before retrying.
429 or 503, wait Retry-After seconds (or 2ⁿ × 250ms jittered) and try again. Don't retry 4xx client errors — they'll always fail.
429 response
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714046400
{
"error": {
"type": "rate_limit_error",
"code": "RATE_LIMITED",
"message": "You have exceeded 300 req/min. Retry in 37s.",
"requestId": "req_01HXYZ…"
}
}
Errors
Every error response uses the same envelope (RFC 7807 + Flowie extensions). See the error catalog for a full list of codes and how to fix them.
| Status | Meaning |
|---|---|
400 | The request is malformed or fails validation. |
401 | Missing, expired, or invalid credentials. |
403 | Credentials are valid but lack the required scope or company access. |
404 | The resource doesn't exist (or isn't visible to you). |
409 | Conflict — typically an idempotency or state transition issue. |
422 | Semantically invalid (e.g. VAT not in directory, unreachable recipient). |
429 | Rate-limited. Honor Retry-After. |
500 | Internal error. Report requestId to support. |
502 / 503 | Upstream service unavailable. Circuit breaker may be open. |
Request inspector
Every response carries an X-Request-Id header (and a requestId field on errors). Pass it back to this endpoint to retrieve the full request trace: timing, intermediate upstream calls, validation diff, and final status. Mirrors what you see at requests.html.
Error shape
{
"error": {
"type": "validation_error",
"code": "INVALID_REQUEST",
"message": "Request validation failed",
"details": [
{ "field": "document.lines[0].vatRate",
"rule": "range",
"message":"Must be between 0 and 100" }
],
"requestId": "req_01HXYZ2K3M4N5P6Q7R",
"docUrl": "https://docs.flowie.ink/errors#INVALID_REQUEST"
}
}
Versioning
The API version is baked into the URL (/v1/…). We follow semantic versioning with these commitments:
- Breaking changes ship under a new path (
/v2/…). Old paths stay alive for at least 12 months. - Additive changes — new fields, new enum values, new endpoints — land in
/v1/without notice. - Deprecations are announced in the changelog and flagged with the
Sunsetresponse header 6+ months before removal.
Sunset header example
Sunset: Wed, 01 Oct 2026 00:00:00 GMT
Deprecation: true
Link: <https://docs.flowie.ink/changelog#send-v1>; rel="deprecation"
Sandbox mode
Use a flw_test_… key with the staging base URL. Sandbox behaves identically to live with these differences:
- Documents are not delivered to real Peppol access points — they're routed to an internal echo endpoint.
- Compliance reporting goes to a mock PPF/SDI that always accepts.
- Webhooks fire the same events with
"livemode": falsein the payload. - There are no quotas; rate limits remain.
Bootstrap a sandbox key
Public, unauthenticated. Mints a fresh flw_test_* API key bound to a brand-new throwaway organization plus a Belgian sandbox company (BE0000000001, peppolId 0208:0000000001). Returns the key only once. Rate-limited per IP; meant for the docs Playground and CI smoke tests.
Request body
- labelstringoptional
Free-form tag for the issued key — appears in the dashboard. Default
quickstart. Max 64 chars. - emailstringoptional
Optional contact email (we may follow up with usage tips).
- keyTypeenumoptional
personalplatformwhite_labelDefaults to
personal(token prefixflw_test_). Passplatformto mint a multi-tenant key (flw_plat_test_) that satisfies the platform-key gate on/v1/platform/*ops, orwhite_labelfor the branding-enabled variant (flw_wl_test_). See Sandbox · Key types.
Reset sandbox state
Wipes events, idempotency cache, and pending scheduled events for the calling organization. Test-mode key only.
Request body
- confirmenumrequired
Type the literal string
yesto acknowledge the wipe. - scopeenumoptional
alldocumentseventsidempotencyWhat to wipe. Defaults to
all.
Advance virtual clock
Move the company-scoped virtual clock forward — used to test 60-day overdue flows, retry escalations, etc. Wakes any scheduled events whose virtual fire-time is now in the past.
Request body
- companyIdstringrequired
Company whose virtual clock should be advanced.
- bystringrequired
How far to jump. Accepts compact units:
1h,3d,2w,1m,1y.
Reset virtual clock
Snap the virtual clock back to wall-clock time for a company.
Request body
- companyIdstringrequired
Force rate-limit
Make every subsequent request from this organization return 429. Use to validate your client's retry/backoff path against a real Retry-After.
Request body
- durationSecondsintegeroptional
How long the forced
429should last. Default60, range 1–3600 (max 1 hour).
Flush idempotency cache
Drop the 24h idempotency cache for the calling key — useful when you want to re-issue a request that previously succeeded under the same Idempotency-Key. No request body.
Base URL
https://back.flowie.ink/exchange/v1
Companies
A company represents a legal entity that can send or receive documents on Peppol. Create one per VAT number you operate under. Flowie auto-enriches the legal name, address, and Peppol identifier, then registers the company with the Peppol SMP so other access points can route messages to it.
- idstring
Unique identifier,
comp_…. - name / legalNamestring
Display name and registered legal name.
- vatNumberstring
Normalized
^[A-Z]{2}[A-Z0-9]+$. - countryISO 3166-1 α-2
Derived from the VAT prefix.
- peppolIdstring
Scheme-prefixed Peppol participant identifier, e.g.
0208:0123456789. - additionalIdentifiersobject[]
Extra identifiers (GLN, DUNS, SIRET…).
- addressAddress
Postal address. See Address.
- capabilitiesobject
Which document types the company can send/receive.
- statusstring
active,inactive, orsuspended. - smpRegisteredboolean
True once the SMP record is live.
- smpRegisteredAttimestamp
When SMP registration completed.
- complianceobject
Per-country compliance status (PPF for FR, SDI for IT). Belgium has no regulator-side report; the field is empty for BE companies.
- settingsobject
Sending preferences, default currency, auto-reporting toggles.
- statsobject
Summary counters (documents sent, received).
- metadataobject
Your free-form key-value store.
- createdAt / updatedAttimestamp
ISO 8601 UTC.
Create a company
Registers a new company. Only vatNumber is strictly required — everything else is auto-enriched from the national registry (INSEE, KBO, Camera di Commercio, …) and the Peppol directory.
Request body
-
vatNumberstringrequired
Country prefix + number, e.g.
BE0123456789. Pattern^[A-Z]{2}[A-Z0-9]+$. -
namestringoptional
Display name. Defaults to the enriched legal name.
-
addressAddressoptional
Overrides the auto-enriched address.
-
additionalIdentifiersobject[]optional
Extra routing identifiers.
{ "scheme": "0088", "value": "1234567890128" }for GLN, etc. -
capabilitiesobjectoptional
{"send": ["invoice","credit-note"], "receive": ["invoice"]}. Default: full set. -
settingsobjectoptional
Default currency, auto-compliance toggles, preferred contact.
-
complianceobjectoptional
Per-country compliance configuration overrides (e-reporting enrolment, PPF/SDI routing hints).
-
metadataobjectoptional
Free-form key-value (max 40 keys, 500 chars each).
Returns
The company object with status 201. SMP registration happens asynchronously — listen for company.smp_registered via webhook.
vatNumber already owned by your organization returns 409 duplicate with the existing companyId. Use that as your idempotent upsert.
Request
curl -X POST https://back.p2p-flowie.com/exchange/v1/companies \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: upsert-acme-be" \
-d '{
"vatNumber": "BE0123456789",
"capabilities": {
"send": ["invoice","credit-note"],
"receive": ["invoice"]
},
"metadata": { "tenantId": "t_acme" }
}'
company = api.post("/companies",
headers={"Idempotency-Key": "upsert-acme-be"},
json={
"vatNumber": "BE0123456789",
"capabilities": {
"send": ["invoice","credit-note"],
"receive": ["invoice"],
},
"metadata": {"tenantId": "t_acme"},
},
).json()
const company = await flowie("/companies", {
method: "POST",
headers: { "Idempotency-Key": "upsert-acme-be" },
body: JSON.stringify({
vatNumber: "BE0123456789",
capabilities: { send: ["invoice","credit-note"], receive: ["invoice"] },
metadata: { tenantId: "t_acme" },
}),
});
payload, _ := json.Marshal(map[string]any{
"vatNumber": "BE0123456789",
"capabilities": map[string]any{
"send": []string{"invoice","credit-note"},
"receive": []string{"invoice"},
},
"metadata": map[string]string{"tenantId": "t_acme"},
})
req, _ := http.NewRequest("POST",
"https://back.p2p-flowie.com/exchange/v1/companies",
bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Idempotency-Key", "upsert-acme-be")
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)
Response
{
"id": "comp_01HXYZ123ABC",
"name": "ACME Business Solutions BVBA",
"legalName": "ACME Business Solutions BVBA",
"vatNumber": "BE0123456789",
"country": "BE",
"peppolId": "0208:0123456789",
"additionalIdentifiers": [],
"address": {
"street": "Rue de la Loi 16",
"city": "Bruxelles",
"postalCode": "1000",
"country": "BE"
},
"capabilities": {
"send": ["invoice","credit-note"],
"receive": ["invoice"]
},
"status": "active",
"smpRegistered": false,
"smpRegisteredAt": null,
"compliance": {},
"settings": { "defaultCurrency": "EUR" },
"stats": { "sent": 0, "received": 0 },
"metadata": { "tenantId": "t_acme" },
"createdAt": "2026-04-25T10:00:00Z",
"updatedAt": "2026-04-25T10:00:00Z"
}
{
"error": {
"type": "conflict",
"code": "COMPANY_EXISTS",
"message": "A company with this VAT already exists in your organization.",
"details": [{ "field": "vatNumber", "value": "BE0123456789",
"existingId": "comp_01HXYZ…" }],
"requestId":"req_01HXYZ…"
}
}
{
"error": {
"type": "invalid_request_error",
"code": "VAT_NOT_FOUND",
"message": "VAT BE0000000000 is not in the national registry.",
"requestId":"req_01HXYZ…"
}
}
List companies
Returns all companies you own or manage, most-recently created first.
Query parameters
- countryISO 3166-1 α-2optional
Filter by country.
- statusstringoptional
active,inactive, orsuspended. - searchstringoptional
Full-text over name, legal name, VAT, and Peppol ID.
- include_addressbooleanoptional
Resolve each row's
legalAddressIdinto a fulladdressobject (adds round-trips). Defaulttrue— setfalsefor a faster, lighter list. - limit / cursorpaginationoptional
See Pagination.
curl "https://back.p2p-flowie.com/exchange/v1/companies?country=BE&status=active&limit=50" \
-H "Authorization: Bearer $KEY"
page = api.get("/companies",
params={"country": "BE", "status": "active", "limit": 50}).json()
{
"data": [
{ "id": "comp_01HXYZ…", "name": "ACME BVBA",
"vatNumber": "BE0123456789", "country": "BE",
"peppolId": "0208:0123456789", "status": "active" }
],
"hasMore": false,
"cursor": null
}
Resolve by VAT / SIREN
Looks up any company, anywhere, by legal identifier — returns the same shape as the company object but synthesized from national registries and the Peppol directory. Use it to pre-fill forms, verify recipients, or check Peppol reachability.
Query parameters
- countryCodeISO 3166-1 α-2required
- vatNumberstringone of
- registrationNumberstringone of
SIREN, KBO, CF, … depending on
countryCode.
curl "https://back.p2p-flowie.com/exchange/v1/companies/resolve?countryCode=FR®istrationNumber=797978996" \
-H "Authorization: Bearer $KEY"
Search companies
Autocomplete over your managed companies. Optimized for < 80 ms response time. Use for dropdowns in UIs.
- qstringrequired
Query fragment (min 2 chars).
- countryCodeISO 3166-1 α-2optional
- limitintegeroptional
Default
10, max50.
[
{ "id": "comp_…", "name": "ACME BVBA", "vatNumber": "BE0123456789",
"country": "BE", "peppolId": "0208:0123456789" }
]
Retrieve a company
Returns the company object. The path parameter accepts three forms:
comp_01HXYZ…— the canonical idvat:BE0123456789— VAT-scoped lookuppeppol:0208:0123456789— Peppol-ID lookup
curl https://back.p2p-flowie.com/exchange/v1/companies/vat:BE0123456789 \
-H "Authorization: Bearer $KEY"
Update a company
Partial update. System-managed attributes (peppolId, status, timestamps, stats) are read-only. Merging rules:
- Top-level keys are replaced wholesale.
metadatais shallow-merged. Set a key tonullto delete it.- Changing
capabilities.sendorcapabilities.receivemay trigger an SMP re-registration (you'll see acompany.smp_registeredevent).
Request body
All fields optional — send only what you want to change.
- namestringoptional
Display name.
- addressAddressoptional
- capabilitiesobjectoptional
{"send": [...], "receive": [...]}. May trigger SMP re-registration. - settingsobjectoptional
- complianceobjectoptional
- metadataobjectoptional
Shallow-merged. Set a key to
nullto delete it.
Deregister a company
Permanently removes the SMP record and marks the company inactive. Historical documents remain queryable. Returns 204 No Content.
Join requests
If a Flowie user wants to connect to an already-registered company, they hit POST /companies/{id}/join. The company's organization admins see pending requests via:
and accept or reject with:
Issue a join request as the calling user.
curl -X PATCH \
https://back.p2p-flowie.com/exchange/v1/companies/comp_abc \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"settings": { "defaultCurrency": "EUR" },
"metadata": { "tier": "premium", "oldKey": null }
}'
curl -X DELETE \
https://back.p2p-flowie.com/exchange/v1/companies/comp_abc \
-H "Authorization: Bearer $KEY"
# 204 No Content
Documents
The document resource represents an invoice, credit note, debit note, or purchase order. Flowie accepts a structured JSON body (we'll render valid UBL 2.1) or a raw UBL/CII XML payload. Either way, we validate, sign, deliver over Peppol, and track lifecycle status through to payment.
- idstring
doc_… - typeenum
invoicecredit-notedebit-notepurchase-ordersales-orderquoteevent - directionenum
incomingoutgoing - numberstring
Your external document number.
- issueDate / dueDatedate (YYYY-MM-DD)
- currencyISO 4217
- grossAmount / netAmount / vatAmountdecimal
- sender / receiverParty
- statusenum
draftsentdeliveredrejected - deliveryStatusenum
pendingdeliveredfailedrejected - lifecycleStatusenum
Business-level state. See Lifecycle.
- documentobject
The full structured body (lines, tax, payment, …).
- xmlstring
Rendered UBL (populated on delivery).
- metadataobject
- receivedAt / sentAttimestamp
Send a document
Delivers a document over Peppol to the to participant. Always set Idempotency-Key — duplicate sends to SDI or PPF can create regulatory headaches.
Doubles as Flowie's inbound integration point. Wire any ERP / accounting system / iPaaS webhook directly here — see Inbound: ERP webhooks for the full matrix of payload shapes (structured JSON · UBL XML · PDF / Factur-X / image / proprietary file).
Body
-
typeenumrequired
invoicecredit-notedebit-notepurchase-ordersales-orderquoteevent -
formatenumoptional
jsonubl-xmlcii-xmlautorawjson(default) — we render UBL.ubl-xml/cii-xml— provide your own XML inxml.auto— supply afile; the server sniffs the bytes and routes to the right pipeline.raw— supply afile; the server stores it as-is on the document file API and returnsdeliveryStatus="stored"(no Peppol routing). -
fromstringrequired
Your sender company. Accepts a bare Peppol id (
0208:0123456789) or the prefixed formspeppol:…,vat:…,comp_…/org:…. Whatever you pass is normalised to the sender's canonical Peppol id before delivery — the response always echoes the bare0208:…form. -
tostringrequired when type ≠ event
Recipient. A Peppol participant id (
0208:0123456789orpeppol:…) — used as-is — or any other identifier we can resolve to one:vat:…,siren:…/siret:…,duns:…,gln:…,lei:…,eori:…,email:…,domain:…,name:…,org:…/id:…(or the bare unprefixed form of any of these). Non-Peppol identifiers are resolved against org-v2 + the PPF Annuaire (FR) + the Peppol Directory, and provisioned if never seen, so they route to a real participant. Optional (omit) whentype=event— events are pure observability/audit records and have no recipient. -
documentDocumentBodyrequired when format=json
See schema below.
- numberstringrequired
- issueDatedaterequired
- dueDatedateoptional
- currencyISO 4217optional
Default
EUR. - buyerReferencestringoptional
Required by many public-sector buyers (e.g. Service Executant / Code Service in FR).
- orderReferencestringoptional
PO number.
- notestringoptional
- seller / buyerPartyoptional
Overrides the auto-derived seller/buyer.
- paymentPaymentInfooptional
means,iban,bic,reference,discountTerms[]. - deliveryobjectoptional
- linesInvoiceLine[]required
Each line:
description,quantity,unit,unitPrice,vatRate,vatCategory,itemCode,period. - allowances / chargesarrayoptional
Document-level discounts or surcharges.
- attachmentsarrayoptional
- totalsobjectoptional
Pre-computed totals. If omitted, we derive them from lines + allowances.
-
xmlstringrequired when format=ubl-xml
Raw UBL 2.1 or CII XML. We validate against the Peppol BIS 3.0 schematron before delivery.
-
fileFileAttachmentrequired when format=auto or raw
Arbitrary file payload (PDF, image, ZIP, proprietary format).
{ content: <base64>, contentType?, filename? }. Max 5 MiB. Withformat=autowe sniff magic bytes — if the file is UBL XML it routes through the regular pipeline (deliveryStatus="pending"); otherwise the bytes are persisted on the document file API and the response carriesdeliveryStatus="stored",fileId, andstoredFormat.
Query parameters — raw-body mode
Instead of a JSON body, you can POST the native ERP payload verbatim (UBL/CII XML, PDF, image, proprietary file) as the raw request body and carry the wrapper constants in the URL. Triggered whenever type is present as a query param. Handy for wiring an ERP / iPaaS webhook straight at this endpoint.
- typeenumrequired
Same enum as the body
type. Its presence is what switches the endpoint into raw-body mode. - fromstringoptional
Sender company. Defaults to the key's organization (
org:<id>) when omitted. - contentTypestringoptional
MIME type of the raw body. Falls back to the
Content-Typeheader, then a magic-byte sniff. - filenamestringoptional
Filename persisted on the file record. Defaults to
<Idempotency-Key>.<ext>or an auto-generatedevent-*.<ext>.
Returns
201 Created with the document object. For structured payloads deliveryStatus starts as pending; listen for document.delivered or document.failed. For raw uploads deliveryStatus="stored" and the response includes fileId + storedFormat.
Request — full invoice
curl -X POST https://back.p2p-flowie.com/exchange/v1/documents/send \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: inv-2026-0417" \
-d '{
"type": "invoice",
"from": "comp_01HXYZ…",
"to": "0208:9876543210",
"document": {
"number": "INV-2026-0417",
"issueDate": "2026-04-25",
"dueDate": "2026-05-25",
"currency": "EUR",
"buyerReference": "SERV-FIN-042",
"orderReference": "PO-91234",
"seller": { "name": "ACME BVBA" },
"buyer": { "name": "Globex SRL", "vatNumber": "IT01234567890" },
"payment": {
"means": "credit_transfer",
"iban": "BE68539007547034",
"bic": "BPOTBEB1",
"reference": "INV-2026-0417"
},
"lines": [
{
"description": "Consulting services — April 2026",
"quantity": 10, "unit": "hours",
"unitPrice": 150.00,
"vatRate": 21,
"vatCategory": "S"
},
{
"description": "Travel expenses",
"quantity": 1, "unit": "lump",
"unitPrice": 450.00,
"vatRate": 21,
"vatCategory": "S"
}
]
}
}'
doc = api.post("/documents/send",
headers={"Idempotency-Key": "inv-2026-0417"},
json={
"type": "invoice",
"from": company["id"],
"to": "0208:9876543210",
"document": {
"number": "INV-2026-0417",
"issueDate": "2026-04-25",
"dueDate": "2026-05-25",
"currency": "EUR",
"buyerReference": "SERV-FIN-042",
"payment": {"means": "credit_transfer",
"iban": "BE68539007547034",
"bic": "BPOTBEB1",
"reference": "INV-2026-0417"},
"lines": [
{"description": "Consulting — April",
"quantity": 10, "unit": "hours",
"unitPrice": 150.00, "vatRate": 21},
{"description": "Travel",
"quantity": 1, "unitPrice": 450.00, "vatRate": 21},
],
},
}).json()
const doc = await flowie("/documents/send", {
method: "POST",
headers: { "Idempotency-Key": "inv-2026-0417" },
body: JSON.stringify({
type: "invoice",
from: company.id,
to: "0208:9876543210",
document: {
number: "INV-2026-0417",
issueDate: "2026-04-25",
dueDate: "2026-05-25",
currency: "EUR",
payment: { means: "credit_transfer",
iban: "BE68539007547034",
bic: "BPOTBEB1",
reference: "INV-2026-0417" },
lines: [
{ description: "Consulting — April",
quantity: 10, unit: "hours",
unitPrice: 150.00, vatRate: 21 },
{ description: "Travel",
quantity: 1, unitPrice: 450.00, vatRate: 21 },
],
},
}),
});
Response
{
"id": "doc_01HY7AB9C2DE3FG",
"type": "invoice",
"direction": "outgoing",
"number": "INV-2026-0417",
"issueDate": "2026-04-25",
"dueDate": "2026-05-25",
"currency": "EUR",
"grossAmount": 2359.50,
"netAmount": 1950.00,
"vatAmount": 409.50,
"sender": { "peppolId": "0208:0123456789", "name": "ACME BVBA" },
"receiver": { "peppolId": "0208:9876543210", "name": "Globex SRL" },
"status": "sent",
"deliveryStatus": "pending",
"lifecycleStatus":"issued",
"sentAt": "2026-04-25T10:05:00Z",
"createdAt": "2026-04-25T10:05:00Z"
}
{
"error": {
"type": "delivery_error",
"code": "RECIPIENT_NOT_FOUND",
"message": "0208:9876543210 is not registered on Peppol for document type 'invoice'.",
"requestId":"req_…"
}
}
Batch send
Submit up to 100 documents in one request. Results come back in the same order as the input; failures don't poison successful sends.
Request body
- documentsSendItem[]required
Array of send items. Each item takes the same fields as Send a document (
type,format,from,to,document,xml,file) plus an optional per-itemidempotencyKey.
{
"documents": [
{ "type": "invoice", "from": "comp_…", "to": "0208:…", "document": {…} },
{ "type": "invoice", "from": "comp_…", "to": "0208:…", "document": {…} }
]
}
{
"results": [
{ "ok": true, "id": "doc_01…", "status": "sent" },
{ "ok": false, "error": { "code": "INVALID_REQUEST", "message": "…" } }
],
"sent": 1,
"failed": 1
}
Validate without sending
Run full Peppol BIS schematron + recipient reachability checks without delivering anything. Handy as a CI step before switching a customer live.
Request body
Same shape as Send a document, minus the raw file upload mode.
- typeenumrequired
invoicecredit-notedebit-notepurchase-ordersales-orderquoteevent - formatenumoptional
jsonubl-xmlcii-xml - fromstringrequired
Sender company —
comp_…,vat:…, orpeppol:…. - tostringrequired
Recipient Peppol participant identifier (drives the reachability check).
- documentDocumentBodyrequired when format=json
Same structured body as Send. See schema.
- xmlstringrequired when format=ubl-xml / cii-xml
{
"valid": false,
"errors": [
{ "rule": "BR-16", "message": "An Invoice shall have at least one line.",
"path": "/Invoice/InvoiceLine" }
],
"warnings": [],
"recipientReachable": true
}
List documents
Paginated list across both directions.
Query parameters
- directionenumoptional
incomingoutgoing - typeenumoptional
- statusstringoptional
Exact
lifecycleStatus— org-specific and may be localized (e.g.draft,sent). The delivery valuesdelivered/failedare routed todeliveryStatus. - deliveryStatusenumoptional
pendingdeliveredfailedrejectedPeppol network delivery state — use this (not
status) to find delivered documents. - from / todateoptional
Filter by
issueDaterange. - amountMin / amountMaxnumberoptional
Gross amount bounds.
- companyIdstringoptional
- searchstringoptional
Full-text over number, party names, references, note.
- limit / cursorpaginationoptional
Advanced search
Same filters as the list endpoint, but accepts a JSON body with compound expressions: AND, OR, NOT trees. Use when a query exceeds URL length limits or you need nested predicates.
Request body
- querystringoptional
Free-text query, same semantics as the list
searchparam. - filtersobjectoptional
Compound predicate tree (
AND/OR/NOT) over the same fields as the list filters (direction, type, status, deliveryStatus, issueDate range, amount bounds, companyId). - sortobjectoptional
Sort spec, e.g.
{"field": "issueDate", "order": "desc"}. - limitintegeroptional
Page size. Default
20. - cursorstringoptional
Opaque pagination cursor from the previous page.
Retrieve a document
Download XML
Returns the signed UBL XML with Content-Type: application/xml.
Download PDF
Returns a human-readable PDF rendering.
Structured view
Flat, scalar-only representation — perfect for pushing to a data warehouse or spreadsheet.
Document actions
Non-lifecycle operations: mark-read, mark-unread, archive, unarchive, tag, untag, assign, unassign, add-note, link.
- actionenumrequired
mark-readmark-unreadarchiveunarchivetaguntagassignunassignadd-notelink - tagstringconditional
Required by the tag / untag actions.
- userIdstringconditional
Required by the assign / unassign actions.
- notestringconditional
Required by the add-note action.
- relatedDocumentIdstringconditional
The document to link to — required by the link action.
curl "…/v1/documents?direction=outgoing&status=sent&from=2026-04-01&amountMin=500" \
-H "Authorization: Bearer $KEY"
curl -X POST …/v1/documents/search \
-H "Authorization: Bearer $KEY" \
-d '{
"any": [
{ "all": [{"status":"sent"},{"deliveryStatus":"failed"}]},
{ "all": [{"lifecycleStatus":"disputed"}]}
],
"from": "2026-01-01"
}'
curl -X POST …/v1/documents/doc_abc/actions \
-H "Authorization: Bearer $KEY" \
-d '{"action":"tag","tag":"priority/high"}'
Structured response
{
"id": "doc_01…",
"type": "invoice",
"direction": "incoming",
"number": "INV-2026-0417",
"issueDate": "2026-04-25",
"dueDate": "2026-05-25",
"currency": "EUR",
"grossAmount": 2359.50,
"netAmount": 1950.00,
"vatAmount": 409.50,
"status": "delivered",
"lifecycleStatus": "approved",
"deliveryStatus": "delivered",
"senderPeppolId": "0208:0123456789",
"senderName": "ACME BVBA",
"senderVatNumber": "BE0123456789",
"receiverPeppolId": "0208:9876543210",
"receiverName": "Globex SRL",
"receiverVatNumber": "IT01234567890",
"buyerReference": "SERV-FIN-042",
"orderReference": "PO-91234",
"paymentIban": "BE68539007547034",
"paymentReference": "INV-2026-0417",
"receivedAt": "2026-04-25T10:05:08Z",
"sentAt": "2026-04-25T10:05:00Z",
"createdAt": "2026-04-25T10:05:00Z",
"updatedAt": "2026-04-25T10:05:08Z"
}
Lifecycle
Once a document is delivered, it moves through a business-level state machine: issued → under_review → approved → partially_paid → paid, with side branches for rejected and disputed. Flowie persists the history, enforces allowed transitions, and reports each relevant change to the national compliance platform automatically.
Retrieve lifecycle history
Full event log, current status, allowed transitions, and per-country compliance state.
Update lifecycle status
Body
-
statusenumrequired
under_reviewapprovedrejectedpartially_paidpaiddisputed - reason / reasonCodestringoptional
Required for
rejectedanddisputed. Use the official Peppol reason codes. - notestringoptional
- paymentDatedateconditional
Required for
paid/partially_paid. - paymentAmount / paymentCurrency / remainingAmountnumber / ISO 4217conditional
- paymentReferencestringoptional
Batch lifecycle update
Up to 500 updates in one call. Atomic per document; failures are reported per item.
Request body
- updatesobject[]required
Array of updates. Each entry is a lifecycle update body (
status,reason,note,paymentDate, …) plus the targetdocumentId.
issued → paid without a prior approved) returns 409 invalid_transition and a hint listing legal next states. Fetch the history to see what's allowed now.
curl -X POST …/v1/documents/doc_abc/lifecycle \
-H "Authorization: Bearer $KEY" \
-d '{
"status": "paid",
"paymentDate": "2026-04-25",
"paymentAmount": 2359.50,
"paymentCurrency": "EUR",
"paymentReference": "PAY-2026-0001"
}'
{
"documentId": "doc_abc",
"previousStatus": "approved",
"currentStatus": "paid",
"updatedAt": "2026-04-25T10:35:00Z",
"compliance": {
"reportedTo": ["PPF","SDI"],
"status": "reported",
"nextCheckAt":"2026-04-25T10:40:00Z"
},
"allowedTransitions": ["disputed"]
}
Directory
Peppol's public directory lets you find any registered participant across every access point in Europe. Use these endpoints to verify reachability before sending.
Search directory
Find any participant registered on the Peppol network. You must supply at least one search criterion — q or vatNumber — and a free-text q must be scoped by country (a bare SIREN/SIRET or a vatNumber already carries its country, so it's exempt). Matching on q is fuzzy (substring). By default results are collapsed to one row per legal entity — the directory lists each company once per identifier scheme.
- qstringconditional
Free-text company name, e.g.
epsa. A bare 9- or 14-digit value is treated as a French SIREN/SIRET and routed to an exact lookup. One ofqorvatNumberis required. - vatNumberstringconditional
Exact VAT number, e.g.
BE0633501357orFR26921376265. One ofqorvatNumberis required. - countryISO 3166-1 α-2conditional
Required when searching by a free-text
q, e.g.BE. Optional (a filter) otherwise. - city / postalCodestringoptional
Further geographic filters.
- naceCodesstring[]optional
Filter by NACE business-activity code(s).
- documentTypesstring[]optional
Only return participants that can receive these types.
- includeSubEntitiesbooleanoptional
Default
false(one row per legal entity). Settrueto return every Peppol identifier-scheme / establishment row — needed when you want the exact routable participant ID. - detailenumoptional
basicfullDefault
basic(directory fields only).fullenriches each row with access-point / SMP detail — slower, one lookup per result. - limitintegeroptional
Max distinct participants to return. Default
20.
Lookup Peppol ID
Verify recipient
The recommended pre-flight check before every send. Tells you whether the recipient exists, can accept the document type, and returns the access point metadata.
Request body
- peppolIdstringrequired
Recipient Peppol participant identifier, e.g.
0208:9876543210. - documentTypestringrequired
Document type to check reachability for, e.g.
INVOICE.
curl "…/v1/directory/search?q=epsa&country=BE&limit=20" \
-H "Authorization: Bearer $KEY"
{
"data": [
{
"peppolId": "0208:0655917760",
"name": "EPSA MARKETPLACE Belgium SRL",
"country": "BE",
"city": null,
"postalCode": null,
"vatNumber": null,
"documentTypes": ["invoice", "credit-note"],
"accessPoint": null
}
],
"hasMore": true,
"cursor": null
}
curl -X POST …/v1/directory/verify \
-H "Authorization: Bearer $KEY" \
-d '{
"peppolId": "0208:9876543210",
"documentType":"INVOICE"
}'
{
"peppolId": "0208:9876543210",
"exists": true,
"canReceive": true,
"recipientName": "Globex SRL",
"documentType": "INVOICE",
"accessPoint": "peppol.ehealth.fgov.be"
}
Partners
A partner is a counterparty you regularly transact with — a customer, a supplier, or both. Partners store defaults (preferred currency, payment terms, contacts, routing ID) so you don't have to supply them on every send.
Create a partner
At least one of peppolId or vatNumber is required.
- peppolIdstringconditional
Pattern
^\d{4}:.+$. - vatNumberstringconditional
- roleenumoptional
supplierbuyerboth - contactName / contactEmailstringoptional
- defaultsobjectoptional
currency,paymentTermsDays,note,orderReference… - tags / metadataarray / objectoptional
List partners
Query parameters
- roleenumoptional
supplierbuyerboth - searchstringoptional
Full-text over name, VAT, and Peppol ID.
- countryISO 3166-1 α-2optional
- tagsstringoptional
Comma-separated tag filter.
- hasActivitybooleanoptional
Only partners with at least one sent/received document.
- peppolStatusstringoptional
- sortBy / orderstringoptional
Field to sort by and direction (
asc/desc). - limit / cursorpaginationoptional
Retrieve partner
Path accepts part_…, vat:…, or peppol:….
Update partner
Request body
All fields optional — same shape as create.
- peppolIdstringoptional
- vatNumberstringoptional
- roleenumoptional
supplierbuyerboth - contactName / contactEmailstringoptional
- defaultsobjectoptional
- tags / metadataarray / objectoptional
Delete partner
curl -X POST …/v1/partners \
-H "Authorization: Bearer $KEY" \
-d '{
"peppolId": "0208:9876543210",
"role": "buyer",
"contactName":"Laura Rossi",
"contactEmail":"laura@globex.it",
"defaults": { "currency": "EUR", "paymentTermsDays": 30 },
"tags": ["strategic","italy"]
}'
{
"id": "part_01HXY…",
"peppolId": "0208:9876543210",
"name": "Globex SRL",
"vatNumber": "IT01234567890",
"country": "IT",
"role": "buyer",
"contactName": "Laura Rossi",
"contactEmail":"laura@globex.it",
"peppolStatus":"active",
"defaults": { "currency": "EUR", "paymentTermsDays": 30 },
"tags": ["strategic","italy"],
"enrichment": { "naceCode": "70.22" },
"stats": { "documentsSent": 12, "documentsReceived": 0 },
"metadata": {},
"createdAt": "2026-04-25T10:00:00Z",
"updatedAt": "2026-04-25T10:00:00Z"
}
Webhooks
Webhooks deliver events to your HTTPS endpoint. Every delivery is signed (X-Flowie-Signature), retried with exponential backoff, and recorded for replay. See the Webhook cookbook for signing, retries, and idempotency patterns.
Create a webhook
- urlhttps URLrequired
- eventsstring[]required
document.receiveddocument.updateddocument.sentdocument.delivereddocument.failedlifecycle.updatedcompany.smp_registered* - secretstringoptional
Auto-generated if omitted. Used for HMAC-SHA256 signing.
- companyIdstringoptional
Scope events to a specific managed company.
List webhooks
Query parameters
- companyIdstringoptional
Only return webhooks scoped to this managed company.
Update webhook
Request body
- urlhttps URLoptional
- eventsstring[]optional
- rotateSecretbooleanoptional
Set
trueto mint a new signing secret (returned once in the response).
Delete webhook
curl -X POST …/v1/webhooks \
-H "Authorization: Bearer $KEY" \
-d '{
"url": "https://example.com/hooks/peppol",
"events": ["document.received","document.delivered","document.failed"],
"secret": "whsec_rotate_me"
}'
{
"id": "wh_01…",
"url": "https://example.com/hooks/peppol",
"events": ["document.received","document.delivered","document.failed"],
"status": "active",
"companyId": null,
"failureCount": 0,
"lastDeliveredAt": null,
"createdAt": "2026-04-25T10:00:00Z"
}
Events
Every webhook delivery has a durable twin in the Events API. If your endpoint was down, or you want a replay, poll /v1/events and acknowledge what you've processed.
List events
Query parameters
- typestringoptional
Filter by event type, e.g.
document.received. - companyIdstringoptional
Scope to a managed company.
- limitintegeroptional
Page size. Default
20.
Acknowledge one event
Returns 204 No Content. Acked events are hidden from subsequent list calls.
Batch acknowledge
Request body
- eventIdsstring[]required
Event IDs to acknowledge, e.g.
["evt_…", "evt_…"].
Replay an event
Re-emits a delivered event onto every matching webhook subscription as if it had just happened. Useful for recovering from a downstream outage on your side without rewinding our delivery state. Returns {"replayed": <n>} with the count of webhook deliveries scheduled.
{
"data": [
{
"id": "evt_01HY…",
"type": "document.received",
"createdAt": "2026-04-25T10:05:08Z",
"data": {
"documentId": "doc_01…",
"direction": "incoming",
"type": "invoice",
"number": "INV-2026-0417"
}
}
],
"hasMore": false
}
Compliance
France PPF and Italy SDI require that lifecycle state changes (accepted / rejected / paid) be reported to a national platform. Flowie does this for you. These endpoints surface the current state and the underlying report records. Belgium runs pure Peppol since 2026-01-01 (HERMES decommissioned 2025-12-31) — no regulator-side report fires for BE.
Compliance status
Query parameters
- companyIdstringoptional
Limit to a single managed company.
- countryISO 3166-1 α-2optional
Compliance reports
Every report record has documentId, reportedTo, platformResponse, and an error if the authority rejected.
Query parameters
- companyIdstringoptional
- countryISO 3166-1 α-2optional
- statusstringoptional
Filter by reporting status.
- from / todateoptional
Report-date range.
- limit / cursorpaginationoptional
Stats
Usage, quota, and rate-limit status for the current period.
Query parameters
- periodenumoptional
dayweekmonthyear - companyIdstringoptional
{
"period": { "start":"2026-04-01", "end":"2026-04-30" },
"quota": { "limit": 5000, "used": 412, "remaining": 4588 },
"rateLimit": { "perMinute": 300 },
"documents": {
"sent": 180,
"received": 232,
"delivered": 178,
"failed": 2
},
"byType": { "invoice": 390, "credit-note": 22 },
"byCountry": { "FR": 150, "BE": 120, "IT": 142 },
"partners": { "total": 47, "active": 31 }
}
Platform
These endpoints are for organizations running Flowie under their own brand — accounting SaaS, ERPs, public-sector aggregators. Most require a flw_plat_live_… or flw_wl_live_… key.
Onboard a managed company
Registers a tenant, optionally creates a scoped API key and webhook, and registers on SMP — all in one call.
- vatNumberstringrequired
- namestringoptional
- addressAddressoptional
- metadataobjectoptional
- receiveDocumentsbooleanoptional
Default
true. - autoVerifybooleanoptional
- webhookobjectoptional
Same shape as webhook create; created atomically.
- apiKeyobjectoptional
{ "name": "tenant-…", "scopes": ["send","documents.read"] }.
List managed companies
Create API key for tenant
- namestringrequired
- companyIdstringoptional
Scopes the key to that tenant.
- scopesstring[]optional
- expiresAttimestampoptional
- rateLimitobjectoptional
List platform API keys
Revoke a key
Usage breakdown
Returns total counters and a per-group array.
Query parameters
- periodstringoptional
Reporting window, e.g.
month. - groupByenumoptional
companycountrytype
Update platform settings
Request body
- brandingobjectoptional
Logo, colors, sender display name for white-label delivery.
- defaultsobjectoptional
Default tenant settings applied at onboard time.
- customDomainstringoptional
Custom domain for webhook/callback URLs.
Cross-tenant event stream
Returns the unified event stream across every tenant managed by this platform key. Same shape as /v1/events with an extra companyId on each row so you can fan out per-tenant. Filters: type, companyId, limit, cursor. Platform / white-label keys only.
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":"erp-tenant-t001",
"scopes":["send","documents.read","lifecycle"] }
}'
{
"company": { "id":"comp_01HY…", "peppolId":"0009:FR86797978996", … },
"apiKey": { "id":"key_01…", "key":"flw_live_t001_abc…", "keyPrefix":"flw_live_t001" },
"webhook": { "id":"wh_01…", "status":"active" }
}
API keys
Create API key
Authenticate with a Flowie JWT (Auth0) — the same token your dashboard uses. The new key is bound to the caller's Flowie organization (resolved from the JWT's _permissions claim) and inherits its tier. Multi-org users should pass X-Flowie-Organization-Id to target a specific org. An existing flw_live_* key may also call this endpoint to mint additional keys for the same org.
- namestringrequired
- companyIdstringoptional
- scopesstring[]optional
See scopes list.
- expiresAttimestampoptional
- rateLimitintegeroptional
Response includes key exactly once. Store it in your secret manager immediately.
List API keys
Revoke API key
Immediate. Any request-in-flight bearing the revoked key finishes, but new requests 401.
{
"id": "key_01HY…",
"key": "flw_live_abc123def456ghi…", // shown once
"keyPrefix": "flw_live_abc123",
"name": "Mobile App",
"scopes": ["send","documents.read"],
"companyId": null,
"createdAt": "2026-04-25T10:00:00Z",
"expiresAt": "2027-04-25T00:00:00Z"
}
Categorization
Tag documents, partners, or other objects. Tags live in groups (e.g. business-unit, project, cost-center). We also expose an AI suggest endpoint — feed it a document, get a ranked list of tags.
List tag groups
List tags in a group
Tags on an object
Assign tag
Body: {"tagId": "tag_…", "objectType": "document"}.
Remove tag
AI tag recommendation
Body: {"objectId":"doc_…", "objectType":"document", "context": {…}} → ranked list of recommended tags with confidence scores.
[
{ "tagId": "tag_cc_rd", "name": "R&D", "groupId": "cost-center", "confidence": 0.92 },
{ "tagId": "tag_proj_x1", "name": "Project X1", "groupId": "project", "confidence": 0.71 }
]
Payments
Document payment info
Record a payment
Body: {"amount": …, "date": "YYYY-MM-DD", "reference": "…"}. Automatically advances the lifecycle to partially_paid or paid.
Export ISO 20022 / SEPA
Generates a pain.001 SEPA credit-transfer file for a set of documents, ready for upload to your bank.
{
"documentId": "doc_abc",
"amountDue": 2359.50,
"amountPaid": 0.00,
"currency": "EUR",
"dueDate": "2026-05-25",
"status": "unpaid",
"iban": "BE68539007547034",
"reference": "INV-2026-0417"
}
AFNOR XP Z12-013
Flowie Exchange is a French PDP (Plateforme de Dématérialisation Partenaire) and ships a fully compliant implementation of the AFNOR XP Z12-013 facade. ERPs, OD (Opérateur de Dématérialisation), other PDPs, and the public PPF infrastructure all talk to us in the standardized format below — so swapping us in or out of an existing AFNOR-compliant integration is a base-URL change.
The standard defines three contractually-required interfaces that every PDP must publish. Flowie's facade implements all three at the URLs below; identifiers and verbs match the AFNOR Annexe A normative grammar verbatim.
-
Part 1 — Service de fluxflow-serviceSubmission, retrieval and search of structured flows (invoices, credit notes, status updates) between PDPs and between PDPs and the PPF concentrator. Mounted at
/afnor/flow-service/v1. -
Part 2 — Service d'annuairedirectory-serviceLookup and reverse-lookup of recipients keyed by SIREN, SIRET, and routing codes — published once per day by the PPF and queried at runtime by every PDP. Mounted at
/afnor/directory-service/v1. -
Part 3 — Webhooksflow-service/webhooksSubscription model so receiving PDPs and ODs are notified the moment a flow targeting them is processed.
Reference documents:
Architecture & vocabulary
The PPF (Portail Public de Facturation) sits as a passive concentrator and annuaire. Every B2B invoice in France must flow through at least one PDP. PDPs route to each other directly when both sides are on different platforms; flows transit the PPF only for fallback, reporting (e-Reporting), and lifecycle aggregation.
- PA (Plateforme Acheteur) — the buyer's PDP receives the flow.
- PV (Plateforme Vendeur) — the seller's PDP submits the flow.
- OD (Opérateur de Dématérialisation) — non-certified upstream of a PDP; can submit but not receive.
- OPDF — Operation Process Description Format; how flow lifecycle is described on the wire.
- MR-DG — Mandat de Représentation côté Destinataire / côté Generic; routing-code level mandate.
Every operation below is authenticated with a Flowie token (Bearer) or the AFNOR-compliant ?token= query parameter — both forms are accepted.
Submit a flow
Multipart: flowInfo (JSON) + file (binary). Returns 202 Accepted with a flowId.
- flowInfo.namestringrequired
- flowInfo.flowSyntaxenumrequired
CIIUBLFactur-XCDARFRR - flowInfo.trackingIdstring (≤36)optional
- flowInfo.processingRuleenumoptional
B2BB2CB2G - flowInfo.flowProfileenumoptional
BasicCIUSExtended-CTC-FR - flowInfo.sha256hexoptional
Search flows
Request body
SearchFlowParams. Filters are AND-combined; array values are OR-combined.
- limitintegeroptional
Page size, 1–100. Default
25. - whereSearchFlowFiltersoptional
Filter object. Fields:
updatedAfter,updatedBefore,processingRule[],flowType[],flowDirection[],trackingId,ackStatus.
Retrieve a flow
Query parameters
- docTypeenumoptional
MetadataOriginalConvertedReadableView
AFNOR webhooks
Same operations as Webhooks but under the AFNOR-shaped schema:
Create body
- callbackobjectrequired
url(required), plus optionalheaders[],authentication,signature. - metadataobjectrequired
Subscription filters:
flowType,flowDirection(required),processingRule,ackStatus(optional).
Update body — technical params only
- headersobject[]optional
- authenticationobjectoptional
- signatureobjectoptional
AFNOR directory (SIREN / SIRET / routing codes)
Every */search response uses the AFNOR envelope: search, totalNumberOfResults, results.
Request body
- filtersobjectoptional
Field → value map of search predicates.
- sortingobject[]optional
- fieldsstring[]optional
Restrict the returned columns.
- limitintegeroptional
1–100. Default
50. - ignoreintegeroptional
Offset — rows to skip.
Query parameters
- fieldsstring[]optional
Comma-separated columns to return.
Request body
- filtersobjectoptional
- sortingobject[]optional
- fieldsstring[]optional
- includestring[]optional
Expand related rows.
- limitintegeroptional
1–100. Default
50. - ignoreintegeroptional
Query parameters
- fieldsstring[]optional
- includestring[]optional
Request body
- filtersobjectoptional
- includestring[]optional
- limitintegeroptional
1–100. Default
50.
Query parameters
- fieldsstring[]optional
- includestring[]optional
Directory-line search
Stub endpoint for directory-line queries — the AFNOR aggregate row that joins SIREN + SIRET + routing-code data into a single result row, used for OD ↔ PDP onboarding flows. Response is currently empty (returns the AFNOR search envelope with totalNumberOfResults: 0) until the PDP-PDP federation handshake is wired up.
Request body
- filtersobjectoptional
- sortingobject[]optional
- fieldsstring[]optional
- limitintegeroptional
1–100. Default
50.
Healthchecks
Public, unauthenticated. Returns { "status": "ok", "version": "1.0", "service": "flow-service|directory-service" }. Required by the AFNOR PDP certification suite.
curl -X POST …/afnor/flow-service/v1/flows \
-H "Authorization: Bearer $KEY" \
-F 'flowInfo={"name":"INV-2026-0417","flowSyntax":"UBL","processingRule":"B2B","flowProfile":"Extended-CTC-FR","trackingId":"t-42"};type=application/json' \
-F 'file=@invoice.xml'
HTTP/1.1 202 Accepted
{
"flowId": "flw_01HY…",
"submittedAt": "2026-04-25T10:00:00Z",
"name": "INV-2026-0417",
"flowSyntax": "UBL",
"trackingId": "t-42",
"processingRule": "B2B",
"flowProfile": "Extended-CTC-FR",
"sha256": "e3b0c442…"
}
PunchOut cart callback
The cXML PunchOut return endpoint. SAP Ariba, Coupa, Ivalua and friends POST the user's cart back here when they check out. We OCR the cXML into a Flowie request, then respond with an HTML page that redirects the user to the originating chat thread.
Authentication: no bearer — we validate the SharedSecret in the cXML header against a per-partner allow-list, plus BuyerCookie for org scoping.
Accepted bodies
Content-Type: application/x-www-form-urlencodedwith acxml-urlencodedorcxml-base64field.Content-Type: application/xmlwith the raw cXML PunchOutOrderMessage.
Response
An HTML <meta refresh> redirect — typically to {APP_URL}/{org_slug}/ai/chat/{thread_id} if the v2 BuyerCookie contains a thread hint, or {APP_URL}/{org_slug}/requests otherwise.
<cXML payloadID="..." timestamp="...">
<Header>
<Sender><Credential domain="NetworkID">...</Credential>
<SharedSecret>***</SharedSecret></Sender>
</Header>
<Message><PunchOutOrderMessage>
<BuyerCookie>org_01HY…:thread_abc:v2</BuyerCookie>
...
</PunchOutOrderMessage></Message>
</cXML>
Health
Public, unauthenticated. Great for load balancers and synthetic monitors.
Liveness
Returns {"status":"ok"} as long as the process can serve requests.
Readiness
Includes circuit-breaker state for every upstream.
Contracts
Actively probes upstreams (SMP, national directories). Slower; don't call from a hot path.
{
"status": "ok",
"circuits": {
"peppol-smp": { "state": "closed", "failures": 0 },
"ppf-annuaire": { "state": "closed", "failures": 0 },
"document-service":{ "state": "closed", "failures": 0 }
}
}
Appendices
Address object
- streetstring
- citystring
- postalCodestring (≤20)
- regionstring
- countryISO 3166-1 α-2required
Party object
{ "name":"…", "vatNumber":"…", "address":Address, "contact": {"name":"…", "email":"…", "phone":"…"} }
PaymentInfo object
- meansenum
credit_transferdirect_debitcardcashcheque - ibanIBAN
- bicSWIFT BIC
- referencestring
- discountTermsarray
Rejection reason codes (Peppol BIS 3)
| Code | Meaning |
|---|---|
RE | Receipt rejected by the buyer |
NOA | No adjustment was possible |
QUA | Quantity discrepancy |
PRI | Price disagreement |
REF | Buyer reference incorrect |
TAX | Tax calculation mismatch |
DUP | Duplicate invoice |
OTH | Other — always pair with reason |