FlowieExchange
Platform Onboarding Kit

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.

Mental model in one sentence
You hold a platform key. For each customer of yours (a "tenant"), you onboard one managed company. You then either keep acting on their behalf with X-Flowie-Company, or hand them a scoped tenant key for direct integration.

Mental model

ConceptWhat it means
Platform organizationYour Flowie account. Holds platform keys, branding, webhook fan-out config.
Managed companyOne Peppol-registered legal entity belonging to a tenant. One per tenant per VAT.
Platform keyflw_plat_live_… or flw_wl_live_… — your master credential. Never expose to tenants.
Tenant keyPer-managed-company personal key (flw_live_…). Optional — only issue if the tenant integrates Flowie directly.
X-Flowie-CompanyHeader you set with a platform key to act on a specific tenant.

Prerequisites

  1. 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 key string in your secret manager. You won't see it again.

  2. 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 design
    Reusing Idempotency-Key within 24h returns the same response. If your retry is from a different deploy and the original key has expired, you'll get a 409 COMPANY_EXISTS with the existing companyId — treat it as success.

    SMP registration is async. Listen for the company.smp_registered event on the platform-level webhook (or poll GET /companies/{id}) to know when the tenant can send/receive.

  3. Choose a key strategy

    PatternWhenTrade-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.json

    Without the header, the call would error 403 COMPANY_REQUIRED — platform keys must always specify whom they're acting for.

  4. Wire up webhooks

    You have two options. Pick the one that matches how you want to fan out events:

    1. Platform-level webhook. One endpoint receives events from all tenants. Each event includes data.company.id and the tenant's metadata so you can route. Easier to operate.
    2. 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.

  5. 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 customDomain field provisions TLS automatically (Let's Encrypt). DNS records to point at us are returned in the response.

  6. Send on behalf of a tenant

    Same call as a single-tenant integration, plus the X-Flowie-Company header. 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:

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

MetricHow to read it
Tenant healthPer-company document.failed rate over rolling 24h.
Compliance healthcompliance.reported.failed count by country.
Webhook deliveryWebhook record's failureCount field; monitor for > 0.
Quota burnGET /v1/stats?period=month per tenant; alert at 80%.
Upstream healthGET /health/readiness on Flowie's side; see circuit-breaker state.

Offboarding a tenant

Three steps, in order:

  1. Revoke the tenant key: DELETE /v1/platform/api-keys/{key_id}.
  2. Disable the per-tenant webhook (don't delete — keep the audit trail): PATCH /v1/webhooks/{id} with {"status":"disabled"}.
  3. 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

ItemWhy
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 tenantReduces blast radius if one is leaked.
All POST calls send an Idempotency-Key derived from your DB row idSafe retries across deploys.
Webhook handler verifies HMAC on the raw body, before parsingForgery resistance.
Webhook handler dedupes on X-Flowie-Event-IdAt-least-once delivery.
Webhook handler queues work, doesn't process inlineStay under the 5s ack window.
Per-tenant alerting on document.failed and compliance.reported.failedFail fast, fix fast.
Monthly chargeback job hits /platform/usageDon't eat your tenants' cost.
Custom domain DNS verified; TLS auto-renewingBrand integrity.
Sandbox-mode integration tests in CI before any prod deployCatch regressions.