Cancel and refund
End a subscription now or at the end of its paid period, and refund a bill.
A subscription ends at once (cancelled) or at the end of the period paid for (stopped,
then cancelled). Either way it's cancelled on-chain, so nothing can bill it again, and
subscription.statusChanged tells you each change.
- trial → active: confirmed
- trial → trialExpired: trial ends
- trialExpired → active: confirmed
- active → paused: paused
- paused → active: resumed
- active → pastDue: bill fails
- pastDue → active: bill paid
- active → stopped: cancel
- paused → stopped: cancel
- pastDue → stopped: you stop it
- stopped → cancelled (final): period ends
- pastDue → cancelled (final): last retry fails
- trialExpired → cancelled (final): grace ends
- any status → cancelled (final): your cancel, closed, inactive
Cancel a subscription
subscriptions-cancel.sh
# immediate=false stops it now (never billed again) and cancels it at the end of the period paid# for. 409 when it is already stopped or inactive.curl -X POST "https://api.qbitflow.app/v2/subscriptions/$SUBSCRIPTION_UUID/cancel?immediate=false" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a Cancelled, "status": "stopped". 202 while the on-chain cancellation still confirms# (subscription.statusChanged tells the end).// immediate: false stops it now (never billed again) and cancels it at the end of the period// paid for. A ConflictError when it is already stopped or inactive.const { subscription, pending } = await client. subscriptions. cancel( subscriptionUuid, { immediate: false,});if (pending) { // HTTP 202: the on-chain cancellation is still confirming. It goes on regardless: // subscription.statusChanged tells the end. console.log('cancellation confirming');} else { console.log(`now ${subscription.status}`); // stopped}result = client. subscriptions. cancel( subscription_uuid, immediate=False) # at period endif result.pending: # HTTP 202: still confirming on-chain; subscription.statusChanged tells the end. print("cancellation confirming")else: print("now", result.subscription.status) # stopped, cancelled at the period's endsubscriptions/main.go
// Immediate false: never billed again, cancelled when the paid period ends.res, err := client.Subscriptions.Cancel(ctx, subscriptionUUID, &qbitflow. CancelSubscriptionParams{ Immediate: new(false),})if err != nil { log.Fatal(err) // 409 subscription_already_stopped_or_inactive}if res.Pending { // HTTP 202: the on-chain cancellation is still confirming; subscription.statusChanged // tells the end. fmt.Println("cancellation confirming")} else { fmt.Println("now", res.Subscription.Status) // stopped}$result = $client->subscriptions->cancel( $subscriptionUuid, new \QBitFlow\Params\CancelSubscriptionParams( immediate: false));if ($result->pending) { // HTTP 202: the on-chain cancellation is still confirming; subscription.statusChanged tells the end. echo "Cancellation confirming\n";} else { echo "Now {$result->subscription->status}\n"; // stopped: cancelled at the end of the paid period}immediate: true(the default): cancelled at once, whatever its status.immediate: false:stoppednow, cancelled at itsnextBillingAt; the customer keeps the period paid for. ApastDueone has none left: its due bill cancels it right away. Alreadystopped:409(sendimmediate: trueto end it now).- A trial is always cancelled at once (
immediate: falseanswers400). - It answers the subscription:
200once done,202while the on-chain cancellation confirms (the status changes once it's confirmed;subscription.statusChangedtells you).
Others end it too. The customer, from their page: stopped (cancellationReason: "customer"),
never billed again, cancelled at the end of its paid period. QBitFlow: after the last failed bill,
an unconfirmed trial 7 days after its end
(when the trial ends), a merchant leaving
(a seller removed, an organization closed), or a subscription no longer active on-chain.
Watch out
A bill can land just after a cancellation
A charge already on its way may still confirm: subscription.billed with
subscription.status: "cancelled". It stays cancelled and no access is due; refund it if your
policy says so.
Check it
Cancel a test subscription without a trial with immediate: false: once 200, it reads stopped,
subscription.statusChanged (active → stopped) shows in Dashboard/Settings/Events,
and access runs until accessUntil (its currentPeriodEnd: a stopped subscription isn't renewed).
Tag a subscription
Your metadata merges into the subscription's, and it's the only field you edit: its price, terms and status stay as signed.
subscriptions-update.sh
# Merges metadata into the subscription: a key given with an empty value is deleted, the others# are kept. It is the only field a merchant edits.curl -X PATCH "https://api.qbitflow.app/v2/subscriptions/$SUBSCRIPTION_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "metadata": { "plan": "pro", "legacyKey": "" } }'# 200: a Subscription// Metadata is merged into the subscription's: '' deletes a key, keys not named are kept. It// is the only thing a merchant edits on a subscription; webhooks carry it.const updated = await client. subscriptions. update( subscriptionUuid, { metadata: { plan: 'pro', coupon: '' },});console. log( updated. metadata. plan);# Metadata is merged into the subscription's: "" deletes a key, keys not named are kept.# It is the only thing a merchant edits on a subscription; webhooks carry it.updated = client. subscriptions. update( subscription_uuid, metadata={"plan": "pro", "coupon": ""})print( updated. metadata. get( "plan"))subscriptions/main.go
// Metadata is merged into the subscription's: "" deletes a key, keys not named are kept.// It is the only thing a merchant edits on a subscription; webhooks carry it.updated, err := client.Subscriptions.Update(ctx, subscriptionUUID, &qbitflow. UpdateSubscriptionParams{ Metadata: map[string]string{"plan": "pro", "coupon": ""},})if err != nil { log.Fatal(err)}fmt. Println( updated. Metadata["plan"])// Metadata is merged into the subscription's: '' deletes a key, keys not named are kept. It is the// only thing a merchant edits on a subscription; webhooks carry it.$updated = $client->subscriptions->update( $subscriptionUuid, new \QBitFlow\Params\UpdateSubscriptionParams( metadata: ['plan' => 'pro', 'coupon' => ''],));echo ($updated->metadata['plan'] ?? '-') . "\n";Refund a bill
Each paid period is a bill with its own id, sub-hist@…, refunded like a payment, from the
dashboard: Dashboard/Payments/a bill/Refund. You sign with the
wallet that was paid, holding the share
(as for a payment). Once the transfer
confirms, the refund is approved and refund.approved arrives. A refund doesn't cancel the
subscription: cancel it too if that's what you mean.
refunds-list.sh
# The refunds waiting for an answer.curl "https://api.qbitflow.app/v2/refunds?status=pending" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of Refund ({ items, hasMore, nextCursor? })// The active ones: pending, waiting for an answer (no status: every status).const page = await client.refunds.list({ status: [RefundStatus.Pending] });for (const refund of page.items) { const { uuid, txUuid, initiatedBy, reason, amount, currency } = refund; console.log(`${uuid} of ${txUuid} (${initiatedBy}): ${amount} ${currency.symbol}, ${reason}`);}# The ones waiting for an answer (status None: every status).page = client. refunds. list( status=[qbitflow. RefundStatus. PENDING])for refund in page.items: print(refund.uuid, refund.tx_uuid, refund.initiated_by, refund.reason, refund.amount)// The active ones: pending, waiting for an answer (Status empty: every status).page, err := client.Refunds.List(ctx, &qbitflow.RefundListParams{ Status: []qbitflow. RefundStatus{qbitflow. RefundStatusPending},})if err != nil { log.Fatal(err)}for _, r := range page.Items { fmt.Println(r.UUID, r.TxUUID, r.InitiatedBy, r.Reason, r.Amount, r.Currency.Symbol)}// The active ones: pending, waiting for an answer (no status: every status).$page = $client->refunds->list(new \QBitFlow\Params\RefundListParams( status: [\QBitFlow\Enums\RefundStatus::PENDING],));foreach ($page->items as $refund) { echo "{$refund->uuid} {$refund->txUuid} ({$refund->initiatedBy}): {$refund->amount} {$refund->currency->symbol}, " . ($refund->reason ?? '-') . "\n";}refunds-get.sh
# One refund, by its id (refund@…): its status, its share of the payment and its amount.curl "https://api.qbitflow.app/v2/refunds/$REFUND_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a Refundconst refund = await client.refunds.get(refundUuid); // refund@…// pending (a customer's request), approved or rejected.const { uuid, status, refundPercent, amount, currency } = refund;console.log(`${uuid}: ${status}, ${refundPercent}% = ${amount} ${currency.symbol}`);refund = client.refunds.get(refund_uuid) # refund@…# pending (a customer's request), approved or rejected.print(refund.uuid, refund.status, f"{refund.refund_percent}%", refund.amount)refund, err := client.Refunds.Get(ctx, refundUUID) // refund@…if err != nil { log.Fatal(err)}// pending (a customer's request), approved or rejected.fmt.Println(refund.UUID, refund.Status, refund.RefundPercent+"%", refund.Amount, refund.Currency.Symbol)$refund = $client->refunds->get( $refundUuid); // refund@…// pending (a customer's request), approved or rejected.echo "{$refund->uuid} {$refund->status} {$refund->refundPercent}% {$refund->amount} {$refund->currency->symbol}\n";subscription.billed· the body your endpoint receives
{ "createdAt": "2026-10-01T12:00:00Z", "data": { "bill": { "amount": "10", "amountMinUnits": "10000000", "amountUsd": "10", "chain": "BASE", "confirmedAt": "2026-10-01T11:59:58Z", "createdAt": "2026-10-01T12:00:00Z", "currency": {…}, "customerReference": "crm-5521", "customerUuid": "01a05cd7-2a00-7d00-8000-0000000000d1", "explorerUrl": "https://basescan.org/tx/0x3333333333333333333333333333333333333333333333333333333333333333", "from": "0xabcdef1234567890abcdef1234567890abcdef12", "paid": "10.0042", "paidMinUnits": "10004200", "paidUsd": "10.0042", "periodEnd": "2026-10-31T12:00:00Z", "periodStart": "2026-10-01T12:00:00Z", "productDescription": "Monthly access to the Pro plan", "productName": "Pro plan", "productUuid": "019cadfd-8900-7c00-8000-0000000000c1", "settlement": {…}, "subscriptionUuid": "sub@01a0f755-c320-7000-8000-000000000002", "test": false, "to": "0x1234567890abcdef1234567890abcdef12345678", "txHash": "0x3333333333333333333333333333333333333333333333333333333333333333", "uuid": "sub-hist@01a0f755-f200-7000-8000-000000000005" }, "subscription": {…} }, "id": "evt_bca1239f-e7d7-517c-b004-08aaeee1e76f", "test": false, "type": "subscription.billed", "version": "v2"}data.bill.periodEnd: Access runs until then.data.bill.subscriptionUuid: The subscription'ssub@…: not a refundable payment.data.bill.uuid: The bill'ssub-hist@…: refund this one, astxUuid.