Concepts

API keys, On-Behalf-Of and security

One key, one space, one mode. Keep it on your server, retry creates safely, and respect 429s.

Anatomy · API key

  1. sk: a secret key
  2. live|test: its mode: live or test
  3. <uuid>: the key's id, its uuid
  4. <secret>: 64 characters, shown once when it's created

What a key can do

  • Act in its space. An organization key in the shared space, a member's key in theirs (spaces). Its live or test label matches its space: a test key sees test data only.
  • At most as an admin. Never as the owner: owner-only routes aren't open to it.
  • Never the dashboard's flows. Routes that need a wallet's signature or a person (keys, webhook secret rotation, sending, approving or rejecting a refund, releasing held funds) answer 404 to a key: keys never mint keys.
  • Never "Confirm it's you". In the dashboard, a sensitive change (credentials, team, members, wallets, keys, webhook secrets) needs a recent sign-in, or answers 403 reauth_required. An API key never meets it.
  • GET /me says what a key is: its role and its space.
Create a client

client-init.sh

# Every request sends your API key. The start-up check: what is this key, and which mode is it in?curl https://api.qbitflow.app/v2/me \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: { "credential": "apiKey", "role": "admin", "space": { "test": true, … }, … }
// One client per API key, shared by the whole program. A blank key, one not starting with// sk_, or one with a space or a control character, throws a ValidationError (nothing is sent).const client = new QBitFlow(process.env.QBITFLOW_API_KEY ?? ''); // The recommended start-up check: what is this key, and which mode is it in? An// AuthenticationError for an unknown or revoked key.const me = await client.me();console.log(`${me.role} key, test mode: ${me.space?.test}`);
# One client per API key, shared by the whole program. ValidationError: not an sk_… key.client = qbitflow.QBitFlow(os.environ["QBITFLOW_API_KEY"]) # The recommended start-up check: what is this key, and which mode is it in?me = client.me()  # AuthenticationError for an unknown or revoked keyif me.space is not None:    print(f"{me.space.organization_name}, role {me.role}, test mode {me.space.test}")
// One client per API key, shared by the whole program: it is safe for concurrent use.client, err := qbitflow.NewClient(os.Getenv("QBITFLOW_API_KEY"))if err != nil {	log.Fatal(err) // a *qbitflow.ValidationError: no key, or not an sk_… key} // The recommended start-up check: what is this key, and which mode is it in?me, err := client.Me(ctx)if err != nil {	log.Fatal(err) // a *qbitflow.AuthenticationError for an unknown or revoked key}if me.Space != nil {	fmt.Printf("%s, role %s, test mode %v\n", me.Space.OrganizationName, me.Role, me.Space.Test)}
// One client per API key, shared by the whole application.$client = new \QBitFlow\QBitFlow(apiKey: (string) getenv('QBITFLOW_API_KEY')); // The recommended start-up check: which space and mode is this key in?$me = $client->me(); // an AuthenticationException for an unknown or revoked keyprintf("%s, role %s, %s mode\n", $me->space?->organizationName ?? '?', $me->role ?? '?', $me->space?->test ? 'test' : 'live');

Send it in X-API-Key. Keys are created and revoked in the dashboard only, and shown once: QBitFlow keeps only their hash. To roll one, create a new key, move to it, then revoke the old one. A key may expire (expiresAt); lastUsedAt spots a forgotten one. A revoked or expired key answers 401 at once, and so does a key in an earlier format (v1's, or its uuid before its mode): create a new one.

Avoid

Ship it to a browser or commit it

It acts as an admin of your organization.

Do

Keep it in a server's environment

QBITFLOW_API_KEY, read at start-up. Revoke it at once if it leaks.

On-Behalf-Of

An organization key sends On-Behalf-Of: <userUuid> to act in a member's space, in its own mode, with the member's role (user): admin routes answer 403. The rules are in spaces and test mode.

Idempotency keys

A create can time out after QBitFlow did it. Send an Idempotency-Key with every create (checkout sessions, products, customers, webhook endpoints, invitations), the same on every retry: within 24 hours, the same request gets the first answer again (Idempotent-Replayed: true), never a second create. The rules and their codes: idempotency.

Rate limits and errors

  • 429: the request did nothing. Wait Retry-After (also details.retryAfterSeconds), then retry, a create with the same Idempotency-Key (rate limits).
  • Every error has one body: a readable error, a stable code to branch on, optional details, and the requestId for support (also X-Request-Id). A resource your key may not see answers 404, like one that doesn't exist (errors).