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
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@…subscriptions/main.go
// 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:
stage | Means |
|---|---|
done | settled: see outcome |
waiting | the attempt failed: nextAttemptAt says when it's retried (failureCode why); or a trial's grace period runs |
pending | waiting for the customer to sign a new maximum |
running | still 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
Step 1: Subscribe and renew
Subscribe with test tokens:
subscription.created, thensubscription.billed. Wait a period, run the bill: anothersubscription.billed.Step 2: Fail a bill
Empty the paying wallet (or subscribe with a small allowance) and run the bill:
subscription.billingFailed,pastDue,dunningon a read. A single-period subscription showsactionRequired: "topUpAllowance".Step 3: Pause and resume
Pause it, then resume it, from the customer's page (
managementPageLink): twosubscription.statusChanged.Step 4: Cancel
From the customer's page:
stopped. After itsnextBillingAt, 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
| What | Test mode | Live |
|---|---|---|
| Prices | At most $5: a product's, an inline price, and a checkout's amount with its fees | No cap |
| Paid with | Faucet tokens, on test networks | Real tokens, on the mainnets |
| Subscription frequency | At least 5 minutes | At least 1 hour, and a price above $0.25 |
| Subscription bills | Only when you run one (the test bill) | On their due date, by QBitFlow |
| Redirect URLs | http:// or https:// | https:// only |
| Customer emails | Never sent | Sent |
| Webhook retries | 3 retries, 5 minutes, 30 minutes and 2 hours after the failed attempt | 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours after the failed attempt, then every 12 hours, for 3 days |
| A failing endpoint | Never turned off, no email | Its 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 livea test key never sees or creates live data
the receiving wallets on the mainnets, per chain (dashboard:
POST /wallets,POST /), and the currencies they accept, before any checkout (a space whose wallets accept nothing opens none:token-wallets 409 merchant_; check withnot_ ready 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 /with a live key); their fee and trust are per modeinvitations a member of test only →
404in liveset in live (
PUT /with a live credential); the dashboard can apply one change to both modessettings/ :key 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.