Test billing, then go live

Run any bill on demand, play the whole billing timeline once, then move it all to live.

Live mode bills each subscription on its due date. In test mode nothing bills on its own: you run each bill, with live's statuses and webhooks. A year of renewals, failures and cancellations fits in an afternoon.

Run a test bill

Bill a test subscription now

subscriptions-test-bill.sh

# Test mode only: runs the next billing now, with live's statuses and webhooks.# 409 payment_not_due before nextBillingAt.curl -X POST "https://api.qbitflow.app/v2/subscriptions/$SUBSCRIPTION_UUID/test-bill" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a TestBill
// Test mode only: runs the next billing now, with live's statuses and webhooks. A ConflictError// payment_not_due before nextBillingAt; a BadRequestError test_mode_only for a live one.const bill = await client.subscriptions.testBill(subscriptionUuid);// billUuid is sub-hist@…: the id its subscription.billed carries once paid.console.log(bill.billUuid, bill.stage, bill.outcome ?? 'running', bill.failureCode ?? '');
# Test mode only: run the next bill now instead of on its due date.bill = client.subscriptions.test_bill(subscription_uuid)print(bill.bill_uuid, bill.stage, bill.outcome, bill.failure_code)  # sub-hist@…
// Test mode only: runs the next billing now, with live's statuses and webhooks.bill, err := client.Subscriptions.TestBill(ctx, subscriptionUUID)if err != nil {	log.Fatal(err) // 409 payment_not_due before NextBillingAt; 400 test_mode_only for a live one}fmt.Println(bill.BillUUID, bill.Stage, bill.Outcome, bill.FailureCode) // sub-hist@…
// Test mode only: runs the next billing now instead of on its due date.$bill = $client->subscriptions->testBill($subscriptionUuid);echo "Bill {$bill->billUuid}: {$bill->stage} " . ($bill->outcome ?? '-') . ' ' . ($bill->failureCode ?? '') . "\n"; // sub-hist@…

It runs the due bill now and answers how it went, at most about 2 minutes later:

stageMeans
donesettled: see outcome
waitingthe attempt failed: nextAttemptAt says when it's retried (failureCode why); or a trial's grace period runs
pendingwaiting for the customer to sign a new maximum
runningstill settling: read it again later

Once done, outcome is paid (with its txHash), cancelled (by its subscriber, its trial over, its merchant closed, inactive on-chain, or its last attempt failed), skipped (below the minimum amount: the period moves on without a charge), paused (its customer paused it: no charge) or notDue. On a failing bill it runs the next attempt now. Not due yet: 409 payment_not_due. The dashboard's Run next bill now, on the subscription's page, does the same.

Check it

Subscribe to a plan billed every 5 minutes, wait for nextBillingAt, run the bill: outcome: "paid", and subscription.billed brings the new bill.periodEnd. Run it again at once: 409 payment_not_due.

Walk the whole timeline once

  1. Step 1: Subscribe and renew

    Subscribe with test tokens: subscription.created, then subscription.billed. Wait a period, run the bill: another subscription.billed.

  2. Step 2: Fail a bill

    Empty the paying wallet (or subscribe with a small allowance) and run the bill: subscription.billingFailed, pastDue, dunning on a read. A single-period subscription shows actionRequired: "topUpAllowance".

  3. Step 3: Pause and resume

    Pause it, then resume it, from the customer's page (managementPageLink): two subscription.statusChanged.

  4. Step 4: Cancel

    From the customer's page: stopped. After its nextBillingAt, run its bill: cancelled.

Pay with test tokens

Get QBFT, QBitFlow's test token, from the QBFT faucet, which also lists each test network's coin. The dashboard's Dashboard/Tools sends QBFT too (to each wallet once every 6 hours per chain) and can run a test payment into your wallet. Then pay a test checkout the way your customers will (how to pay).

What test mode does differently

WhatTest modeLive
PricesAt most $5: a product's, an inline price, and a checkout's amount with its feesNo cap
Paid withFaucet tokens, on test networksReal tokens, on the mainnets
Subscription frequencyAt least 5 minutesAt least 1 hour, and a price above $0.25
Subscription billsOnly when you run one (the test bill)On their due date, by QBitFlow
Redirect URLshttp:// or https://https:// only
Customer emailsNever sentSent
Webhook retries3 retries, 5 minutes, 30 minutes and 2 hours after the failed attempt1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours after the failed attempt, then every 12 hours, for 3 days
A failing endpointNever turned off, no emailIts team emailed after 24 hours, turned off after 3 days

Go live

Live is a separate set of spaces: nothing carries over. Switch the dashboard to Live, then redo what your integration needs, row by row:

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 swap your server's QBITFLOW_API_KEY and QBITFLOW_WEBHOOK_SECRET for the live ones. Keep the two secrets apart: each endpoint signs with its own.

Live redirects are https:// only: a live checkout's successUrl and cancelUrl, and your live checkout.defaults setting, which payment links and checkouts without URLs use. http:// answers 400 validation_failed: check the setting in live before your first payment.

Check it

Make one small live payment with real tokens. Its event (payment.completed, or subscription.created) reaches your live endpoint in Dashboard/Settings/Events (Live), and your server acts on it.

Live subscriptions bill within about 10 minutes of their nextBillingAt.