Build your own e-invoicing product on top of Flowie
This is the playbook accounting SaaS, ERPs, and public-sector aggregators follow when they integrate Flowie under their own brand. By the end you'll have a working tenant onboarding flow, scoped credentials, branded UX, signed webhooks, and a path to support thousands of customers.
X-Flowie-Company, or hand them a scoped tenant key for direct integration.
Mental model
| Concept | What it means |
|---|---|
| Platform organization | Your Flowie account. Holds platform keys, branding, webhook fan-out config. |
| Managed company | One Peppol-registered legal entity belonging to a tenant. One per tenant per VAT. |
| Platform key | flw_plat_live_… or flw_wl_live_… — your master credential. Never expose to tenants. |
| Tenant key | Per-managed-company personal key (flw_live_…). Optional — only issue if the tenant integrates Flowie directly. |
| X-Flowie-Company | Header you set with a platform key to act on a specific tenant. |
Prerequisites
- A Flowie organization with Platform or White-label entitlement (request via sales).
- Test API credentials. The dashboard's Settings → API keys → Platform key screen issues them.
- An HTTPS endpoint that can receive webhooks (your dev tunnel is fine for now).
-
Get a platform key
curl -X POST https://back.flowie.ink/exchange/v1/api-keys \ -H "Authorization: Bearer $FLOWIE_DASHBOARD_JWT" \ -d '{"name":"my-platform","scopes":["platform","*"],"keyType":"platform"}'You'll get back something like:
{ "id": "key_01HXY", "key": "flw_plat_test_AbC123…", "keyPrefix": "flw_plat_test_AbC", "scopes": ["*"], "createdAt": "2026-04-25T10:00:00Z" }Persist the
keystring in your secret manager. You won't see it again. -
Onboard your first tenant
One call does everything atomically: registers the company, publishes it to Peppol SMP, opens a tenant-scoped webhook, and (optionally) mints a tenant key.
curl -X POST https://back.flowie.ink/exchange/v1/platform/companies \ -H "Authorization: Bearer $PLATFORM_KEY" \ -H "Idempotency-Key: tenant-acme-init" \ -H "Content-Type: application/json" \ -d '{ "vatNumber": "FR86797978996", "name": "ACME France SARL", "metadata": { "tenantId": "t_acme", "tier": "premium" }, "webhook": { "url": "https://yourplatform.com/hooks/flowie?tenant=t_acme", "events": ["document.received","document.delivered","document.failed", "lifecycle.updated","compliance.reported.failed"] }, "apiKey": { "name": "tenant-acme", "scopes": ["send","receive","documents.read","documents.write","lifecycle"] } }'Response:
{ "company": { "id": "comp_01HY7…", "peppolId": "0009:FR86797978996", "vatNumber": "FR86797978996", "name": "ACME France SARL", "country": "FR", "status": "active", "smpRegistered": false, "metadata": { "tenantId": "t_acme", "tier": "premium" }, "createdAt": "2026-04-25T10:00:00Z" }, "apiKey": { "id": "key_01HY7…", "key": "flw_test_tacme_xyz123…", "keyPrefix": "flw_test_tacme", "name": "tenant-acme" }, "webhook": { "id": "wh_01HY7…", "url": "https://yourplatform.com/hooks/flowie?tenant=t_acme", "status": "active" } }Idempotent by designReusingIdempotency-Keywithin 24h returns the same response. If your retry is from a different deploy and the original key has expired, you'll get a409 COMPANY_EXISTSwith the existingcompanyId— treat it as success.SMP registration is async. Listen for the
company.smp_registeredevent on the platform-level webhook (or pollGET /companies/{id}) to know when the tenant can send/receive. -
Choose a key strategy
Pattern When Trade-off Platform-only ( X-Flowie-Company)Your stack does everything; tenants never touch the API. One secret to manage · platform key compromise = all tenants. Per-tenant key Tenants integrate directly (e.g. via your SDK) or you want hard isolation. Blast radius limited to one tenant · you must manage rotation & storage. Hybrid Most platforms. Use the platform key from your backend; issue tenant keys only on request. Best of both, slightly more code. Acting on behalf of a tenant from your backend looks like this:
curl -X POST https://back.flowie.ink/exchange/v1/documents/send \ -H "Authorization: Bearer $PLATFORM_KEY" \ -H "X-Flowie-Company: comp_01HY7…" \ -H "Idempotency-Key: t_acme-inv-001" \ -H "Content-Type: application/json" \ -d @invoice.jsonWithout the header, the call would error
403 COMPANY_REQUIRED— platform keys must always specify whom they're acting for. -
Wire up webhooks
You have two options. Pick the one that matches how you want to fan out events:
- Platform-level webhook. One endpoint receives events from all tenants. Each event includes
data.company.idand the tenant'smetadataso you can route. Easier to operate. - Per-tenant webhook (created during onboarding above). One endpoint per tenant. Heavier, but gives you per-tenant retry isolation.
Either way, the receiver pattern is the same — verify HMAC, ack fast, queue work:
@app.post("/hooks/flowie") async def flowie_hook(req: Request, tenant: str | None = None): raw = await req.body() verify_hmac(req.headers["X-Flowie-Signature"], raw, secret=lookup_webhook_secret(tenant)) event = json.loads(raw) queue.enqueue("process_flowie_event", tenant=tenant, event=event) return Response(status_code=204)Full verification recipe in the webhook cookbook; payload fixtures in /fixtures.
- Platform-level webhook. One endpoint receives events from all tenants. Each event includes
-
Brand the experience (white-label)
If you have a white-label entitlement, you can replace Flowie's branding everywhere your tenants see it:
curl -X PATCH https://back.flowie.ink/exchange/v1/platform/settings \ -H "Authorization: Bearer $PLATFORM_KEY" \ -d '{ "branding": { "displayName": "ACME e-Invoice", "logoUrl": "https://acme.com/logo.svg", "primaryColor":"#0F62FE", "supportEmail":"support@acme.com" }, "customDomain": "peppol.acme.com", "defaults": { "preferredFormat": "ubl-xml", "autoCompliance": { "FR": true, "IT": true, "BE": true } } }'The
customDomainfield provisions TLS automatically (Let's Encrypt). DNS records to point at us are returned in the response. -
Send on behalf of a tenant
Same call as a single-tenant integration, plus the
X-Flowie-Companyheader. Pull the company id from your tenant table by tenantId:def send_invoice(tenant_id: str, invoice: dict) -> dict: company_id = db.get("flowie_company_id", tenant_id=tenant_id) return platform_api.post( "/documents/send", headers={ "X-Flowie-Company": company_id, "Idempotency-Key": f"{tenant_id}-{invoice['id']}", }, json={ "type": "invoice", "from": company_id, "to": invoice["recipientPeppolId"], "document": invoice["body"], }, ).json()
Scale to 100+ tenants
The onboarding API is designed for batch use. Common patterns:
- Backfill from your existing customer table. Iterate, call
POST /platform/companieswith an idempotency key per customer. Failures are isolated; safe to retry. - Just-in-time onboarding. Onboard the first time a tenant tries to send. Hide the latency behind a "preparing your workspace" loading state — typically < 5 seconds.
- Bulk send. Use
POST /v1/documents/send/batchfor nightly jobs. Up to 100 documents per call, all atomic per item.
Concrete script for backfill:
for tenant in db.tenants(active=True):
try:
api.post("/platform/companies",
headers={"Idempotency-Key": f"backfill-{tenant.id}"},
json={
"vatNumber": tenant.vat,
"name": tenant.name,
"metadata": {"tenantId": tenant.id},
},
timeout=30,
)
except httpx.HTTPStatusError as e:
log.error("onboard_failed", tenant=tenant.id,
status=e.response.status_code, body=e.response.text)
continue
Billing & chargebacks
Flowie bills the platform organization monthly, by document volume. Use GET /v1/platform/usage to break down per tenant for chargebacks:
curl "https://back.p2p-flowie.com/exchange/v1/platform/usage?period=month&groupBy=company" \
-H "Authorization: Bearer $PLATFORM_KEY"
{
"period": { "start":"2026-04-01", "end":"2026-04-30" },
"total": { "documentsSent": 18420, "documentsReceived": 22100 },
"byCompany": [
{ "companyId":"comp_…", "tenantId":"t_acme",
"sent": 4203, "received": 5012, "complianceReports": 3801 },
…
]
}
Observability
| Metric | How to read it |
|---|---|
| Tenant health | Per-company document.failed rate over rolling 24h. |
| Compliance health | compliance.reported.failed count by country. |
| Webhook delivery | Webhook record's failureCount field; monitor for > 0. |
| Quota burn | GET /v1/stats?period=month per tenant; alert at 80%. |
| Upstream health | GET /health/readiness on Flowie's side; see circuit-breaker state. |
Offboarding a tenant
Three steps, in order:
- Revoke the tenant key:
DELETE /v1/platform/api-keys/{key_id}. - Disable the per-tenant webhook (don't delete — keep the audit trail):
PATCH /v1/webhooks/{id}with{"status":"disabled"}. - Deregister the company:
DELETE /v1/companies/{id}. Historical documents remain queryable for the legally-mandated retention period (10y in IT, 6y in FR).
Production go-live checklist
| ✓ | Item | Why |
|---|---|---|
| ☐ | Platform key stored only in secret manager (Vault / AWS SM / GSM) | Compromise = blast radius across all tenants. |
| ☐ | Tenant keys (if used) stored encrypted at rest, scoped by tenant | Reduces blast radius if one is leaked. |
| ☐ | All POST calls send an Idempotency-Key derived from your DB row id | Safe retries across deploys. |
| ☐ | Webhook handler verifies HMAC on the raw body, before parsing | Forgery resistance. |
| ☐ | Webhook handler dedupes on X-Flowie-Event-Id | At-least-once delivery. |
| ☐ | Webhook handler queues work, doesn't process inline | Stay under the 5s ack window. |
| ☐ | Per-tenant alerting on document.failed and compliance.reported.failed | Fail fast, fix fast. |
| ☐ | Monthly chargeback job hits /platform/usage | Don't eat your tenants' cost. |
| ☐ | Custom domain DNS verified; TLS auto-renewing | Brand integrity. |
| ☐ | Sandbox-mode integration tests in CI before any prod deploy | Catch regressions. |
