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
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 signedsubscriptions/main.go
// 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 404until the checkout completes (paid on-chain or, for a trial, its message signed). - Then
subscription.createdarrives, followed by its firstsubscription.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
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).remainingPeriodsis the live count.
When the customer must act, actionRequired says what:
actionRequired | When | What happens |
|---|---|---|
topUpAllowance | under 1.05 periods left | nothing yet: still active. They add periods |
raiseMaximum | the token's price moved: a bill is above the maximum | the bill waits up to 7 days, then retries start |
confirmTrial | a trial ended unconfirmed | they 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
Do
Prompt your customer
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.