How-to

From test mode to live

Live is a separate space. Redo each line below with the dashboard on Live, then swap the key.

0 of 7 done

  • a live key (sk_live_…), created in the dashboard switched to live

    a test key never sees or creates live data

  • the receiving wallets on the mainnets, per chain (dashboard: POST /wallets, POST /token-wallets), and the currencies they accept, before any checkout (a space whose wallets accept nothing opens none: 409 merchant_not_ready; check with GET /wallets/accepted-currencies)

    test wallets are for test networks; the organization fee also needs the organization's own wallet on each chain (GET /organization/wallets, routes/members.md)

  • created again in live (new uuids)

    references may be the same as in test (unique per space and mode)

  • created again in live, each with its own new secret

    test endpoints only receive test events; store the live secret separately

  • invited again in live (POST /invitations with a live key); their fee and trust are per mode

    a member of test only → 404 in live

  • set in live (PUT /settings/:key with a live credential); the dashboard can apply one change to both modes

    values are per space and mode

  • nothing to do

    owners and admins work in both modes

Then switch your integration

Avoid

Branch your code on the mode

Two code paths drift, and the test one stops proving anything.

Do

Swap the key, not the code

Read the live key (sk_live_…) from the environment; the same code runs in both modes.

Check which key you hold with the client's first call:

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');
  • Store the live webhook secret beside the test one, never instead: each endpoint has its own.
  • https redirects. A live checkout's successUrl and cancelUrl (yours, or checkout.defaults') must be https://.
  • Re-create what you referenced by id. Live products and customers get new uuids; references you chose may stay the same.

What behaves differently in live

Differences to expect in live: the subscription frequency minimum is 1 hour (5 minutes in test); subscriptions are billed on their due date by QBitFlow (test subscriptions only when you trigger their bill, POST /subscriptions/:uuid/test-bill, Subscriptions and bills); customer emails are sent (never in test mode); faucets (POST /faucets/:chain) and test payments (POST /payments/test) answer 400 test_mode_only.

  • Prices: test mode caps them at $5, live has no cap. Test payments are made with faucet tokens, whose supply is limited, so every test price is at most $5: a product's (create and update), a checkout's inline priceUsd (payments and subscriptions), and a test product's at checkout. Above → 400 validation_failed on the field (priceUsd, or productUuid / productReference), details.max: "5" (Products, Checkout sessions). Live prices are only bounded by what the chosen token can hold (Payments and transactions).
  • Redirects: live checkouts redirect over https only. A live checkout's successUrl and cancelUrl (yours, or the checkout.defaults setting's) must be https:// → else 400 validation_failed on the field. Test mode takes http:// (a local shop): check your live URLs and the live checkout.defaults before going live.

Your first live payment

Take one small real payment end to end before you announce anything: your wallet receives it, payment.completed arrives, the order is fulfilled once. Refund it from the dashboard if you like, and watch refund.approved arrive.