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:
| Path | Human in the loop? | Issued key | Best for |
|---|---|---|---|
| Sandbox bootstrap | No | flw_test_… | Prototyping, demos, agent CI, MCP playground. |
| OAuth consent flow (PKCE) | Yes — one-time consent | flw_test_… (sandbox today; production rolling out) | Agents that need to act on a specific user's data with explicit scope grants. |
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:
- Rate limit: 120 calls per IP per hour.
- Key TTL: 7 days.
- Test mode only: the key talks to the sandbox host
back.flowie.ink; using it against productionback.p2p-flowie.comreturns401 INVALID_TOKEN. - Documents are not delivered over real Peppol — they route to an internal echo recipient. See Sandbox shortcuts.
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:
- Public clients only. Agents can't reliably keep secrets,
so there's no
client_secret. - PKCE mandatory. The agent generates a one-time
code_verifier, hashes it with SHA-256, and sends only the hash up. The server checks the verifier against the hash on the token exchange. Protects the auth code in transit. - One-time auth codes. 5-minute TTL, single-use.
- OOB by default. Agents that can't host a redirect URI
use
urn:ietf:wg:oauth:2.0:oob— the consent page shows the auth code on screen for the user to copy back.
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:
| Scope | What it grants |
|---|---|
send | Issue invoices, credit notes, orders over Peppol. |
receive | Configure inbound delivery + webhooks + SMP registration. |
documents.read | List, search, download XML / PDF / structured views. |
documents.search | Filtered search across the corpus. |
documents.write | Mark read / archive / tag / add notes. |
companies.read | Read sender / partner companies + Peppol registrations. |
companies.write | Update companies, register on the SMP. |
directory | Search the Peppol directory, verify reachability. |
partners | Manage trading partners and routing settings. |
payments | Record payments, manage terms, ISO 20022 / SEPA export. |
lifecycle | Approve / reject / mark as paid — drives PPF/SDI compliance reporting. |
compliance | Read compliance dashboard + report records. |
stats | Per-period sent / received / delivered / failed counters. |
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:
code_verifier— a random 43-128 character string, base64url-safe. This is the agent's secret. Never sends it until step 4.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.
