---
title: "Platform Onboarding Kit"
description: "A complete walkthrough for platform builders: onboard one tenant, then a hundred. Scoped keys, branding, webhooks, billing."
canonical: "https://docs.get-flowie.com/guides/onboarding-kit"
source: "https://docs.get-flowie.com/guides/onboarding-kit.html"
---

# Platform Onboarding Kit

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

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](<mailto:sales@flowie.fr>)).
  * 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).


  1. ### Get a platform key
[code] 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"}'
[/code]

You'll get back something like:
[code] {
           "id":        "key_01HXY",
           "key":       "flw_plat_test_AbC123…",
           "keyPrefix": "flw_plat_test_AbC",
           "scopes":    ["*"],
           "createdAt": "2026-04-25T10:00:00Z"
         }
[/code]

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

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

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

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

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:
[code] @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)
[/code]

Full verification recipe in the [webhook cookbook](<../reference/webhooks.html#signing>); payload fixtures in [/fixtures](<../fixtures/index.html>).

  5. ### Brand the experience (white-label)

If you have a white-label entitlement, you can replace Flowie's branding everywhere your tenants see it:
[code] 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 }
             }
           }'
[/code]

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:
[code] 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()
[/code]




## Scale to 100+ tenants

The onboarding API is designed for batch use. Common patterns:

  * **Backfill from your existing customer table.** Iterate, call `POST /platform/companies` with 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/batch`](<../reference/index.html#send-batch>) for nightly jobs. Up to 100 documents per call, all atomic per item.



Concrete script for backfill:
[code] 
    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
[/code]

## Billing & chargebacks

Flowie bills the platform organization monthly, by document volume. Use [`GET /v1/platform/usage`](<../reference/index.html#platform-usage>) to break down per tenant for chargebacks:
[code] 
    curl "https://back.p2p-flowie.com/exchange/v1/platform/usage?period=month&groupBy=company" \
      -H "Authorization: Bearer $PLATFORM_KEY"
[/code]
[code] 
    {
      "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 },
        …
      ]
    }
[/code]

## 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:

  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

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