Concepts

Subscriptions: the spending cap and the allowance

Your customer signs a spending cap once. One bill a period, on its due date, never past the cap.

cancelled · nothing more can be pulled

What the customer authorizes, once

At checkout they pick numPeriods, which sizes the allowance they sign (allowance.amount), and a maximum per bill (allowance.maxPerPeriod):

  • The cap: one bill (the price and its lines) times the periods they choose (at least minPeriods), with a buffer and the network fees on top. Nothing can be pulled past it.
  • The rhythm: one pull a period: the first at checkout, the next ones on their due dates. Nothing is prepaid.
  • The price is locked, with the lines its checkout added (a tax, the processing fee), converted to the token at each bill. A new product price applies to new subscribers: to move one, cancel at the end of its period and send a new checkout.

The terms come from the product; a checkout can override each: frequency (at least 1 hour in live, 5 minutes in test, at most 1 year), trialPeriod and minPeriods (up to 1,000).

From the catalog · sample terms

Todayfree: a signed message
7-day trialthey sign the cap
First bill · $4.99as they confirm
Every monthon each due date
Create a subscription checkout with a trial

checkout-create-subscription.sh

# A monthly subscription with a 7-day trial. The subscription exists, with the session's id, once# the customer signed (here: started the trial): act on the subscription.created webhook.curl https://api.qbitflow.app/v2/checkout-sessions/subscription \  -H "X-API-Key: $QBITFLOW_API_KEY" \  -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{    "productName": "T-shirt",    "description": "Blue, size M",    "priceUsd": "4.99",    "frequency": { "value": 1, "unit": "months" },    "trialPeriod": { "value": 7, "unit": "days" },    "reference": "order-1043",    "successUrl": "https://shop.example.com/orders/success?uuid={{UUID}}",    "cancelUrl": "https://shop.example.com/orders/cancel"  }'# 201: a CreatedCheckoutSession ({ "link": "https://…/checkout?uuid=sub@…", "uuid": "sub@…", "expiresAt": "…" })
// The subscription exists, with the session's id, once the customer signed (here: started the// trial). Act on the subscription.created webhook.const session = await client.checkoutSessions.createSubscription({	productName: 'T-shirt',	description: 'Blue, size M',	priceUsd: '4.99', // USD per period, a decimal string	frequency: { value: 1, unit: DurationUnit.Months },	trialPeriod: { value: 7, unit: DurationUnit.Days },	reference: 'order-1043',	successUrl: `https://shop.example.com/orders/success?uuid=${Placeholders.UUID}`,	cancelUrl: 'https://shop.example.com/orders/cancel',});console.log(`Redirect the customer to ${session.link}`);
session = client.checkout_sessions.create_subscription(    product_name="T-shirt",    description="Blue, size M",    price_usd="4.99",  # USD per period, a decimal string    frequency=qbitflow.Duration(value=1, unit=qbitflow.DurationUnit.MONTHS),    trial_period=qbitflow.Duration(value=7, unit=qbitflow.DurationUnit.DAYS),    reference="order-1043",    success_url=f"https://shop.example.com/orders/success?uuid={qbitflow.PLACEHOLDER_UUID}",    cancel_url="https://shop.example.com/orders/cancel",)print("Send the customer to", session.link)  # sub@…: the subscription's id once signed
// The Subscription exists, with the session's id (sub@…), once the customer signed: act on// the subscription.created webhook.session, err := client.CheckoutSessions.CreateSubscription(ctx, &qbitflow.CreateSubscriptionSessionParams{	ProductName: "T-shirt",	Description: "Blue, size M",	PriceUSD:    "4.99", // USD per period, a decimal string	Frequency:   &qbitflow.Duration{Value: 1, Unit: qbitflow.DurationUnitMonths},	TrialPeriod: &qbitflow.Duration{Value: 7, Unit: qbitflow.DurationUnitDays},	Reference:   "order-1043",	SuccessURL:  "https://shop.example.com/orders/success?uuid=" + qbitflow.PlaceholderUUID,	CancelURL:   "https://shop.example.com/orders/cancel",})if err != nil {	log.Fatal(err)}fmt.Println("Redirect the customer to", session.Link)
$session = $client->checkoutSessions->createSubscription(new \QBitFlow\Params\CreateSubscriptionSessionParams(	productName: 'T-shirt',	description: 'Blue, size M',	priceUsd: '4.99', // USD per period, a decimal string	reference: 'order-1043',	frequency: \QBitFlow\Models\Duration::months(1),	trialPeriod: \QBitFlow\Models\Duration::days(7), // the first bill comes after the trial	successUrl: 'https://shop.example.com/orders/success?uuid=' . \QBitFlow\Placeholders::UUID,	cancelUrl: 'https://shop.example.com/orders/cancel',));echo "Subscribe at {$session->link}\n";

One id, end to end

The checkout's id is the subscription's: sub@…, for life. Store it when you create the session.

  • It doesn't exist while it's a checkout. GET /subscriptions/:uuid answers 404 until the checkout completes (paid on-chain or, for a trial, its message signed).
  • Then subscription.created arrives, followed by its first subscription.billed, in that order. A trial gets no first bill: nothing is paid until it's confirmed.

What's left: the allowance

From the contract · subscription.created

110 USDC left · 30 max a bill

1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011

11 bills at the $10.00 priceThe contract's count (remainingPeriods, 1 USDC as $1), before each bill's network fee, which the allowance also pays: with fees, it may cover fewer. Every bill at its 30 USDC maximum: at most 3.

Each bill spends its amount and its network fee from what remains (allowance: amount and amountMinUnits, remainingPeriods, maxPerPeriod; absent during a trial). How it's sized, at signing and at each top-up:

  • Each period adds one bill plus a 10% buffer for the token's price, and the network fees refunded in the token come on top (the creation's own, and one per bill). A top-up's own fee comes out of the new allowance.
  • The whole is never less than one maximum per bill plus the same buffer. A $5 bill with a 6.5 USDC maximum: 6 periods add 6 × 5.5 USDC and their network fees; 1 period alone is raised to 7.15 USDC.
  • The maximum per bill (maxPerPeriod) is a separate, wider cap, so a bill still goes through when the token's price moves. The allowance covers the periods asked while a bill doesn't rise more than the buffer in the token (a stablecoin: always). remainingPeriods is the live count.

When the customer must act, actionRequired says what:

actionRequiredWhenWhat happens
topUpAllowanceunder 1.05 periods leftnothing yet: still active. They add periods
raiseMaximumthe token's price moved: a bill is above the maximumthe bill waits up to 7 days, then retries start
confirmTriala trial ended unconfirmedthey confirm within the 7 days grace, or it's cancelled

An exhausted allowance fails the bill (allowanceExhausted): the subscription is pastDue until paid (billing). Your customer fixes everything on their management page (managementPageLink, on every subscription read and event).

Avoid

Cut access on actionRequired

It never changes the status by itself.

Do

Prompt your customer

On subscription.actionRequiredChanged, sent at every change.

Free trials

A trial starts with a signed message that moves no funds and authorizes nothing: subscription.created arrives with status: "trial". To continue, the customer signs the subscription itself, the same sub@… becoming the paid one. One trial per wallet and product.

More on recurring crypto payments.