FlowieExchange
AI Agents

Agent onboarding — sign up & sign in autonomously

Two paths exist depending on whether the agent is exploring the API on its own or asking a real human to grant production access:

PathHuman in the loop?Issued keyBest for
Sandbox bootstrapNoflw_test_…Prototyping, demos, agent CI, MCP playground.
OAuth consent flow (PKCE)Yes — one-time consentflw_test_… (sandbox today; production rolling out)Agents that need to act on a specific user's data with explicit scope grants.
v1 status
The OAuth flow currently issues sandbox flw_test_… keys (7-day expiry). Production flw_live_… issuance is gated on a dashboard-side consent UI; we'll announce in the changelog when it ships. Until then: use OAuth for the consent ceremony but expect a sandbox- scoped key on the other end.

Sandbox bootstrap — zero-friction path

The agent calls a public, rate-limited endpoint and gets a fresh test key plus a starter sandbox company. No auth, no consent, no human:

curl -X POST https://back.flowie.ink/exchange/v1/sandbox/bootstrap \
  -H "Content-Type: application/json" \
  -d '{"label":"my-agent"}'

Response:

{
  "organizationId": "org_sbx_…",
  "apiKey":         "flw_test_…",
  "keyPrefix":      "flw_test_abc1",
  "keyType":        "personal",
  "company": {
    "id":         "comp_sbx_…",
    "peppolId":   "0208:0000000001",
    "vatNumber":  "BE0000000001",
    "name":       "Sandbox Test BVBA",
    "country":    "BE"
  },
  "expiresAt": "2026-05-12T…"
}

Constraints:

For more advanced sandbox shapes — platform / white-label keys, simulated errors, time-travel — see the Sandbox guide.

OAuth consent flow — agent acts on behalf of a user

When an agent needs to operate on a real user's account, the user must explicitly approve the scope list before the agent gets a key. Flowie implements a deliberately minimal slice of OAuth 2.1 for this:

The four-step dance

┌──────┐                              ┌──────────────────┐
│agent │                              │ Flowie Exchange  │
└───┬──┘                              └─────────┬────────┘
    │                                           │
    │  1. POST /v1/oauth/authorize             │
    │     {client_name, scopes,                │
    │      code_challenge=SHA256(verifier)}    │
    ├──────────────────────────────────────────►│
    │   ◄──── 200 {consent_url}                │
    │                                           │
    │  2. Show consent_url to user             │
    │                                           │
    │           User clicks link, lands on consent
    │           page, reviews scopes, clicks Approve
    │                                           │
    │  3. ◄── auth_code shown on screen        │
    │     (or redirected to your URI)          │
    │                                           │
    │  4. POST /v1/oauth/token                 │
    │     {grant_type, code, code_verifier}    │
    ├──────────────────────────────────────────►│
    │   ◄──── 200 {access_token: flw_test_…}  │
    │                                           │

Scope catalogue

Fetch the live catalogue at GET /v1/oauth/scopes — public, no auth. The minimum bar:

ScopeWhat it grants
sendIssue invoices, credit notes, orders over Peppol.
receiveConfigure inbound delivery + webhooks + SMP registration.
documents.readList, search, download XML / PDF / structured views.
documents.searchFiltered search across the corpus.
documents.writeMark read / archive / tag / add notes.
companies.readRead sender / partner companies + Peppol registrations.
companies.writeUpdate companies, register on the SMP.
directorySearch the Peppol directory, verify reachability.
partnersManage trading partners and routing settings.
paymentsRecord payments, manage terms, ISO 20022 / SEPA export.
lifecycleApprove / reject / mark as paid — drives PPF/SDI compliance reporting.
complianceRead compliance dashboard + report records.
statsPer-period sent / received / delivered / failed counters.
Ask for less, not more
Agents that ask for send alone get approved more often than agents that demand the full scope list up-front. If you need extra access later, trigger a new consent flow with the additional scopes — the user knows what they're agreeing to.

PKCE walkthrough

RFC 7636. The agent generates two values once per authorization:

  1. code_verifier — a random 43-128 character string, base64url-safe. This is the agent's secret. Never sends it until step 4.
  2. code_challenge = BASE64URL(SHA256(code_verifier)) with no padding.

The challenge goes up in the POST /v1/oauth/authorize request. The verifier goes up in the POST /v1/oauth/token request. Server compares — if they don't match, the exchange fails.

Claude Desktop recipe

An MCP-connected Claude Desktop agent that sets itself up. Prompt the user with the consent URL, accept the OOB code back, swap for an API key, then add it to the MCP config:

User: "Set up a Flowie sandbox account for me."

Agent (internal, hidden):
  1. POST /v1/sandbox/bootstrap → flw_test_… key + sandbox company
  2. Update ~/Library/.../claude_desktop_config.json:
     {
       "mcpServers": {
         "flowie-exchange": {
           "url": "https://back.flowie.ink/exchange/mcp",
           "transport": "streamable-http",
           "headers": {"Authorization": "Bearer flw_test_…"}
         }
       }
     }
  3. Tell user to restart Claude Desktop.

Agent (visible):
  "Done. I provisioned a sandbox account at organization
   org_sbx_…. After you restart Claude, you'll have access to 34 Peppol
   tools (send_document, list_documents, …). Try: 'List my last 5 invoices.'"

This is the all-autonomous path — perfect for demoing or developing. For real production access (touching a user's actual Peppol traffic), use the OAuth flow below.

Python recipe

"""Self-onboarding Flowie agent — OAuth-style consent flow with PKCE."""
import base64, hashlib, secrets, webbrowser
import httpx

BASE = "https://back.flowie.ink/exchange"

def pkce_pair():
    verifier = secrets.token_urlsafe(48).rstrip("=")[:64]
    digest = hashlib.sha256(verifier.encode()).digest()
    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
    return verifier, challenge

# 1. Register intent + grab the consent URL.
verifier, challenge = pkce_pair()
r = httpx.post(f"{BASE}/v1/oauth/authorize", json={
    "client_name": "My Local Python Agent",
    "scopes": ["send", "documents.read", "documents.search"],
    "code_challenge": challenge,
    "code_challenge_method": "S256",
})
r.raise_for_status()
auth = r.json()
print(f"Open in your browser:\n  {auth['consent_url']}\n")
webbrowser.open(auth["consent_url"])

# 2. Wait for the user to paste the OOB code back.
auth_code = input("Paste the auth code from the browser: ").strip()

# 3. Exchange code + verifier for an API key.
r = httpx.post(f"{BASE}/v1/oauth/token", json={
    "grant_type": "authorization_code",
    "code": auth_code,
    "code_verifier": verifier,
})
r.raise_for_status()
token = r.json()
print(f"Got key prefix {token['access_token'][:16]}…")
print(f"Scopes: {', '.join(token['scopes'])}")
print(f"Expires in {token['expires_in'] // 3600}h")

# 4. Use it.
api = httpx.Client(
    base_url=f"{BASE}/v1",
    headers={"Authorization": f"Bearer {token['access_token']}"},
)
print(api.get("/documents", params={"limit": 5}).json())

curl recipe

For the absolute lowest-level diagnostic. Generate verifier + challenge in any language; here we use OpenSSL:

# 1. PKCE pair
VERIFIER=$(openssl rand -base64 48 | tr -d '+/=' | head -c 64)
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '+/=' | tr 'a-z' 'a-z')

# 2. Authorize
RESP=$(curl -s -X POST https://back.flowie.ink/exchange/v1/oauth/authorize \
  -H "Content-Type: application/json" \
  -d "{\"client_name\":\"curl agent\",
       \"scopes\":[\"send\"],
       \"code_challenge\":\"$CHALLENGE\",
       \"code_challenge_method\":\"S256\"}")
CONSENT_URL=$(echo "$RESP" | jq -r .consent_url)
echo "Open: $CONSENT_URL"

# 3. After clicking Approve, paste the code:
read -p "Auth code: " CODE

# 4. Exchange
curl -s -X POST https://back.flowie.ink/exchange/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d "{\"grant_type\":\"authorization_code\",
       \"code\":\"$CODE\",
       \"code_verifier\":\"$VERIFIER\"}" | jq .

Step-up — when an agent needs more scope mid-session

The flow above is whole-cycle: agent gets a fresh key with N scopes. If the agent later needs an additional scope (e.g. it has documents.read but discovers it needs payments to mark an invoice paid), the recommended pattern is to start a fresh consent cycle with the additional scope, present the user the new consent URL, and replace the existing key. There is no append-scope-to-existing-key endpoint by design — keeping every issued key tied to exactly one explicit consent record makes audit trails clean.

FAQ

Why not just use the sandbox bootstrap for everything?

Sandbox bootstrap is anonymous. It works for prototyping, but the issued key is bound to a fresh empty sandbox org — not to the user's real Flowie account. The OAuth flow ties the key to a real user's consent, which is what you need for any agent that will touch production data.

Why PKCE? My agent runs on a server, I can keep a secret.

If your agent is server-side and confidential, you'll be migrated to the production OAuth flow when it ships (with client_secret support). For the v1 sandbox-issuing flow, every client is treated as public to keep the surface honest and the rollout simple.

What happens if the user closes the consent page before clicking Approve?

The consent request expires after 10 minutes (no auth code is ever issued). The agent gets a clean 400 on token exchange. Ask the user to retry.

Can I get a key that lasts more than 7 days?

Not via the OAuth flow yet. Production OAuth (coming separately) will mint flw_live_… keys with the same TTL semantics as keys created through the dashboard (90 days default, configurable per org). Until then, the OAuth-issued sandbox keys auto-rotate every 7 days.

How do I revoke a key the agent issued itself?

The user revokes from their dashboard; or the agent calls DELETE /v1/api-keys/{id} with its own key. Revocation is immediate.

Does the OAuth flow ever return an existing key, or always a new one?

Always a new one. Each consent flow mints a new key + new sandbox org, deliberately — preserves the one-key-per-consent-record audit invariant.