FlowieExchange
API Reference · v3.0.0

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.

Base URL

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-Company to 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.

🔒 Keys are shown once
The full key string is returned exactly once, at creation. After that, only the key prefix is visible. Rotate a compromised key immediately — revoke it at DELETE /v1/api-keys/{id}.

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

GET/v1/me

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.
Best practice
Generate the idempotency key before the first attempt — typically from your database row ID, not a random UUID on retry. That way, a crash between generation and HTTP call can still be recovered.
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, max 100.

  • cursorstringoptional

    Pass the cursor value 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:

PlanRequests / minDocuments / month
Free6050
Starter120500
Pro3005,000
Platform60050,000
White-label1,200Unlimited

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.

Exponential backoff
On 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.

StatusMeaning
400The request is malformed or fails validation.
401Missing, expired, or invalid credentials.
403Credentials are valid but lack the required scope or company access.
404The resource doesn't exist (or isn't visible to you).
409Conflict — typically an idempotency or state transition issue.
422Semantically invalid (e.g. VAT not in directory, unreachable recipient).
429Rate-limited. Honor Retry-After.
500Internal error. Report requestId to support.
502 / 503Upstream service unavailable. Circuit breaker may be open.

Request inspector

GET/v1/requests/{request_id}

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 Sunset response header 6+ months before removal.
Forward-compatible parsers
Ignore unknown fields. Treat enum values as opaque strings. That way your integration survives any additive change automatically.
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": false in the payload.
  • There are no quotas; rate limits remain.

Bootstrap a sandbox key

POST/v1/sandbox/bootstrap

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.

Body: { "label": "my-laptop", "email": "you@example.com", "keyType": "personal" | "platform" | "white_label" } — all optional. keyType defaults to personal (token prefix flw_test_); pass platform to mint a multi-tenant key (flw_plat_test_) that satisfies the platform-key gate on /v1/platform/* ops, or white_label for the branding-enabled variant (flw_wl_test_). See Sandbox · Key types.

Reset sandbox state

POST/v1/sandbox/reset

Wipes events, idempotency cache, and pending scheduled events for the calling organization. Body: {"confirm": "yes", "scope": "all" | "events" | "documents" | "idempotency"}. Test-mode key only.

Advance virtual clock

POST/v1/sandbox/clock/advance

Move the company-scoped virtual clock forward — used to test 60-day overdue flows, retry escalations, etc. Body: {"companyId": "comp_sbx_…", "by": "60d" | "12h" | "5m"}. Wakes any scheduled events whose virtual fire-time is now in the past.

Reset virtual clock

POST/v1/sandbox/clock/reset

Snap the virtual clock back to wall-clock time for a company. Body: {"companyId": "comp_sbx_…"}.

Force rate-limit

POST/v1/sandbox/rate-limit/exhaust

Make every subsequent request from this organization return 429 for durationSeconds (1 ≤ s ≤ 3600). Use to validate your client's retry/backoff path against a real Retry-After.

Flush idempotency cache

POST/v1/sandbox/idempotency/flush

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.

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.

The company object
  • 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, or suspended.

  • 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

POST/v1/companies

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.

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

Duplicates
Calling create with a 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

GET/v1/companies

Returns all companies you own or manage, most-recently created first.

Query parameters

  • countryISO 3166-1 α-2optional

    Filter by country.

  • statusstringoptional

    active, inactive, or suspended.

  • searchstringoptional

    Full-text over name, legal name, VAT, and Peppol ID.

  • 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

GET/v1/companies/resolve

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&registrationNumber=797978996" \
  -H "Authorization: Bearer $KEY"

Search companies

GET/v1/companies/search

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, max 50.

[
  { "id": "comp_…", "name": "ACME BVBA", "vatNumber": "BE0123456789",
    "country": "BE", "peppolId": "0208:0123456789" }
]

Retrieve a company

GET/v1/companies/{company_id}

Returns the company object. The path parameter accepts three forms:

  • comp_01HXYZ… — the canonical id
  • vat:BE0123456789 — VAT-scoped lookup
  • peppol:0208:0123456789 — Peppol-ID lookup
curl https://back.p2p-flowie.com/exchange/v1/companies/vat:BE0123456789 \
  -H "Authorization: Bearer $KEY"

Update a company

PATCH/v1/companies/{company_id}

Partial update. Any field in the company object that isn't a system-managed attribute (peppolId, status, timestamps, stats) can be updated. Merging rules:

  • Top-level keys are replaced wholesale.
  • metadata is shallow-merged. Set a key to null to delete it.
  • Changing capabilities.send or capabilities.receive may trigger an SMP re-registration (you'll see a company.smp_registered event).

Deregister a company

DEL/v1/companies/{company_id}

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:

GET/v1/companies/join-requests

and accept or reject with:

POST/v1/companies/{company_id}/join

Issue a join request as the calling user.

POST/v1/companies/{company_id}/join-requests/{request_id}/accept
POST/v1/companies/{company_id}/join-requests/{request_id}/reject
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.

The document object
  • 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

POST/v1/documents/send

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

    json (default) — we render UBL. ubl-xml — provide your own XML in xml. auto — supply a file; the server sniffs the bytes and routes to the right pipeline. raw — supply a file; the server stores it as-is on the document file API and returns deliveryStatus="stored" (no Peppol routing).

  • fromstringrequired

    Your sender company. Accepts comp_…, vat:…, or peppol:….

  • tostringrequired when type ≠ event

    Recipient Peppol participant identifier, e.g. 0208:0123456789. Optional (omit) when type=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. With format=auto we 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 carries deliveryStatus="stored", fileId, and storedFormat.

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

POST/v1/documents/send/batch

Submit up to 100 documents in one request. Results come back in the same order as the input; failures don't poison successful sends.

{
  "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

POST/v1/documents/validate

Run full Peppol BIS schematron + recipient reachability checks without delivering anything. Handy as a CI step before switching a customer live.

{
  "valid": false,
  "errors": [
    { "rule": "BR-16", "message": "An Invoice shall have at least one line.",
      "path": "/Invoice/InvoiceLine" }
  ],
  "warnings": [],
  "recipientReachable": true
}

List documents

GET/v1/documents

Paginated list across both directions.

Query parameters

  • directionenumoptional
    incomingoutgoing
  • typeenumoptional
  • status / deliveryStatusenumoptional
  • from / todateoptional

    Filter by issueDate range.

  • amountMin / amountMaxnumberoptional

    Gross amount bounds.

  • companyIdstringoptional
  • searchstringoptional

    Full-text over number, party names, references, note.

  • limit / cursorpaginationoptional

Advanced search

POST/v1/documents/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.

Retrieve a document

GET/v1/documents/{document_id}

Download XML

GET/v1/documents/{document_id}/xml

Returns the signed UBL XML with Content-Type: application/xml.

Download PDF

GET/v1/documents/{document_id}/pdf

Returns a human-readable PDF rendering.

Structured view

GET/v1/documents/{document_id}/structured

Flat, scalar-only representation — perfect for pushing to a data warehouse or spreadsheet.

Document actions

POST/v1/documents/{document_id}/actions

Non-lifecycle operations: mark-read, mark-unread, archive, unarchive, tag, untag, assign, unassign, add-note, link.

  • actionenumrequired
  • tag / userId / note / relatedDocumentIdconditional

    Depends on 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

GET/v1/documents/{document_id}/lifecycle

Full event log, current status, allowed transitions, and per-country compliance state.

Update lifecycle status

POST/v1/documents/{document_id}/lifecycle

Body

  • statusenumrequired
    under_reviewapprovedrejectedpartially_paidpaiddisputed
  • reason / reasonCodestringoptional

    Required for rejected and disputed. Use the official Peppol reason codes.

  • notestringoptional
  • paymentDatedateconditional

    Required for paid / partially_paid.

  • paymentAmount / paymentCurrency / remainingAmountnumber / ISO 4217conditional
  • paymentReferencestringoptional

Batch lifecycle update

POST/v1/documents/lifecycle/batch

Up to 500 updates in one call. Atomic per document; failures are reported per item.

Allowed transitions
Trying to skip states (e.g. 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

GET/v1/directory/search
  • qstringoptional
  • vatNumberstringoptional
  • country / city / postalCodestringoptional
  • naceCodesstring[]optional
  • documentTypesstring[]optional

    Only return participants that can receive these types.

Lookup Peppol ID

GET/v1/directory/{peppol_id}

Verify recipient

POST/v1/directory/verify

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.

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

POST/v1/partners

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

GET/v1/partners

Filters: role, search, country, tags, hasActivity, peppolStatus, sortBy, order, limit, cursor.

Retrieve partner

GET/v1/partners/{partner_id}

Path accepts part_…, vat:…, or peppol:….

Update partner

PATCH/v1/partners/{partner_id}

Delete partner

DEL/v1/partners/{partner_id}
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

POST/v1/webhooks
  • urlhttps URLrequired
  • eventsstring[]required
    document.receiveddocument.updated document.sentdocument.delivered document.failedlifecycle.updated company.smp_registered*
  • secretstringoptional

    Auto-generated if omitted. Used for HMAC-SHA256 signing.

  • companyIdstringoptional

    Scope events to a specific managed company.

List webhooks

GET/v1/webhooks

Update webhook

PATCH/v1/webhooks/{webhook_id}

Body: url, events, or rotateSecret: true.

Delete webhook

DEL/v1/webhooks/{webhook_id}
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

GET/v1/events

Filters: type, companyId, limit.

Acknowledge one event

POST/v1/events/{event_id}/ack

Returns 204 No Content. Acked events are hidden from subsequent list calls.

Batch acknowledge

POST/v1/events/ack

Body: {"eventIds": ["evt_…", "evt_…"]}.

Replay an event

POST/v1/events/{event_id}/replay

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

GET/v1/compliance/status

Filters: companyId, country.

Compliance reports

GET/v1/compliance/reports

Every report record has documentId, reportedTo, platformResponse, and an error if the authority rejected.

Stats

GET/v1/stats

Usage, quota, and rate-limit status for the current period. Filters: period (day, week, month, year), companyId.

{
  "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

POST/v1/platform/companies

Registers a tenant, optionally creates a scoped API key and webhook, and registers on SMP — all in one call.

  • vatNumberstringrequired
  • name / address / metadataoptional
  • receiveDocumentsbooleanoptional

    Default true.

  • autoVerifybooleanoptional
  • webhookobjectoptional

    Same shape as webhook create; created atomically.

  • apiKeyobjectoptional

    { "name": "tenant-…", "scopes": ["send","documents.read"] }.

List managed companies

GET/v1/platform/companies

Create API key for tenant

POST/v1/platform/api-keys
  • namestringrequired
  • companyIdstringoptional

    Scopes the key to that tenant.

  • scopes / expiresAt / rateLimitoptional

List platform API keys

GET/v1/platform/api-keys

Revoke a key

DEL/v1/platform/api-keys/{key_id}

Usage breakdown

GET/v1/platform/usage

Query: period, groupBy (company, country, type). Returns total counters and a per-group array.

Update platform settings

PATCH/v1/platform/settings

Body: branding, defaults, customDomain.

Cross-tenant event stream

GET/v1/platform/events

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

POST/v1/api-keys
  • namestringrequired
  • companyIdstringoptional
  • scopesstring[]optional

    See scopes list.

  • expiresAttimestampoptional
  • rateLimitintegeroptional

Response includes key exactly once. Store it in your secret manager immediately.

List API keys

GET/v1/api-keys

Revoke API key

DEL/v1/api-keys/{key_id}

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

GET/v1/categorization/groups

List tags in a group

GET/v1/categorization/groups/{group_id}/tags

Tags on an object

GET/v1/categorization/objects/{object_id}/tags

Assign tag

POST/v1/categorization/objects/{object_id}/tags

Body: {"tagId": "tag_…", "objectType": "document"}.

Remove tag

DEL/v1/categorization/objects/{object_id}/tags/{tag_id}

AI tag recommendation

POST/v1/categorization/objects/tags/auto

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

GET/v1/payments/documents/{documentId}

Record a payment

POST/v1/payments/documents/{documentId}/pay

Body: {"amount": …, "date": "YYYY-MM-DD", "reference": "…"}. Automatically advances the lifecycle to partially_paid or paid.

Export ISO 20022 / SEPA

POST/v1/payments/export/iso20022

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.

XP Z12-013 specification (AFNOR — French e-invoicing reform)

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-service
    Submission, 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-service
    Lookup 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/webhooks
    Subscription 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

POST/afnor/flow-service/v1/flows

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
POST/afnor/flow-service/v1/flows/search

Body is SearchFlowParamsupdatedAfter, updatedBefore, processingRule[], flowType[], flowDirection[], trackingId, ackStatus. Filters are AND-combined; array values are OR-combined.

Retrieve a flow

GET/afnor/flow-service/v1/flows/{flow_id}

Query: docType=Metadata|Original|Converted|ReadableView.

AFNOR webhooks

Same operations as Webhooks but under the AFNOR-shaped schema:

GET/afnor/flow-service/v1/webhooks
POST/afnor/flow-service/v1/webhooks
GET/afnor/flow-service/v1/webhooks/{webhook_uid}
PATCH/afnor/flow-service/v1/webhooks/{webhook_uid}
DEL/afnor/flow-service/v1/webhooks/{webhook_uid}

AFNOR directory (SIREN / SIRET / routing codes)

POST/afnor/directory-service/v1/siren/search
GET/afnor/directory-service/v1/siren/code-insee:{siren}
POST/afnor/directory-service/v1/siret/search
GET/afnor/directory-service/v1/siret/code-insee:{siret}
POST/afnor/directory-service/v1/routing-code/search
GET/afnor/directory-service/v1/routing-code/siret:{siret}/code:{routing_identifier}

Search body: filters, sorting, fields, include, limit (1–100, default 50), ignore. Response envelope: search, totalNumberOfResults, results.

Directory-line search

POST/afnor/directory-service/v1/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. Body schema mirrors routing-code/search; response is currently empty (returns the AFNOR search envelope with totalNumberOfResults: 0) until the PDP-PDP federation handshake is wired up.

Healthchecks

GET/afnor/flow-service/v1/healthcheck
GET/afnor/directory-service/v1/healthcheck

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

POST/document/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-urlencoded with a cxml-urlencoded or cxml-base64 field.
  • Content-Type: application/xml with 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

GET/health/liveness

Returns {"status":"ok"} as long as the process can serve requests.

Readiness

GET/health/readiness

Includes circuit-breaker state for every upstream.

Contracts

GET/health/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)

CodeMeaning
REReceipt rejected by the buyer
NOANo adjustment was possible
QUAQuantity discrepancy
PRIPrice disagreement
REFBuyer reference incorrect
TAXTax calculation mismatch
DUPDuplicate invoice
OTHOther — always pair with reason