Concepts

Checkout sessions and payment links

The page your customer pays on. One per order from your server, or a product's link anyone can open.

  1. createdYou create it201 with its link, uuid and expiresAt
  2. waitingConfirmationThe customer paysA failed attempt goes back to created, with lastAttempt
  3. completed ✓Confirmed on-chainpayment.completed or subscription.created
  4. expiredOr, unpaid at expiresAtcheckout.expired. You can expire it sooner

Create one

Create a payment checkout

checkout-create-payment.sh

# A one-time checkout for an inline product. QBitFlow replaces {{UUID}} with the session's id on# redirect. The same Idempotency-Key on a retry never opens a second checkout.curl https://api.qbitflow.app/v2/checkout-sessions/payment \  -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",    "reference": "order-1042",    "successUrl": "https://shop.example.com/orders/success?uuid={{UUID}}",    "cancelUrl": "https://shop.example.com/orders/cancel"  }'# 201: a CreatedCheckoutSession ({ "link": "https://…/checkout?uuid=pay@…", "uuid": "pay@…", "expiresAt": "…" })# Redirect the customer to "link".
// An inline product: nothing needs to exist in your catalog. QBitFlow replaces {{UUID}} with the// session's id on the redirect.const session = await client.checkoutSessions.createPayment({	productName: 'T-shirt',	description: 'Blue, size M',	priceUsd: '4.99', // USD, a decimal string, never a float	reference: 'order-1042', // your order id: unique per space	successUrl: `https://shop.example.com/orders/success?uuid=${Placeholders.UUID}`,	cancelUrl: 'https://shop.example.com/orders/cancel',}); // Redirect the customer to the hosted checkout page.console.log(`Redirect the customer to ${session.link}`);
session = client.checkout_sessions.create_payment(    product_name="T-shirt",    description="Blue, size M",    price_usd="4.99",  # USD, a decimal string: never a float    reference="order-1042",  # your order id: unique per space    # QBitFlow replaces the placeholder with the session's id when it redirects the customer.    success_url=f"https://shop.example.com/orders/success?uuid={qbitflow.PLACEHOLDER_UUID}",    cancel_url="https://shop.example.com/orders/cancel",)# Redirect the customer to the hosted checkout page.print("Send the customer to", session.link)
// An inline product: no product needed in the catalog. QBitFlow replaces {{UUID}}// (qbitflow.PlaceholderUUID) with the session's id on redirect.session, err := client.CheckoutSessions.CreatePayment(ctx, &qbitflow.CreatePaymentSessionParams{	ProductName: "T-shirt",	Description: "Blue, size M",	PriceUSD:    "4.99",       // a decimal string, never a float	Reference:   "order-1042", // your order id: unique per space	SuccessURL:  "https://shop.example.com/orders/success?uuid=" + qbitflow.PlaceholderUUID,	CancelURL:   "https://shop.example.com/orders/cancel",})if err != nil {	log.Fatal(err)}// In an HTTP handler: http.Redirect(w, r, session.Link, http.StatusSeeOther)fmt.Println("Redirect the customer to", session.Link)
$session = $client->checkoutSessions->createPayment(new \QBitFlow\Params\CreatePaymentSessionParams(	productName: 'T-shirt',	description: 'Blue, size M',	priceUsd: '4.99', // a decimal string, never a float	reference: 'order-1042', // your order id: unique per space	successUrl: 'https://shop.example.com/orders/success?uuid=' . \QBitFlow\Placeholders::UUID,	cancelUrl: 'https://shop.example.com/orders/cancel',)); header('Location: ' . $session->link); // send the customer to the hosted checkout

POST /checkout-sessions/payment (or POST /checkout-sessions/subscription) answers 201:

  • link: the checkout page. Send your customer there.
  • uuid: store it with your order. It becomes the payment's (pay@…) or the subscription's (sub@…) id, for life.
  • expiresAt: when it can no longer be paid. Nothing happens on-chain until the customer pays.

Send an Idempotency-Key with every create: a retry answers the same session, never a second one (idempotency).

It sells a product of your catalog, by uuid or your own reference:

Create a checkout for a catalog product

checkout-create-payment-product.sh

# A checkout for a product of your catalog, by your own reference (or "productUuid").curl https://api.qbitflow.app/v2/checkout-sessions/payment \  -H "X-API-Key: $QBITFLOW_API_KEY" \  -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{    "productReference": "tshirt-blue-m",    "reference": "order-1042",    "successUrl": "https://shop.example.com/orders/success?uuid={{UUID}}",    "cancelUrl": "https://shop.example.com/orders/cancel"  }'# 201: a CreatedCheckoutSession ({ "link": "https://…/checkout?uuid=pay@…", "uuid": "pay@…", "expiresAt": "…" })
const session = await client.checkoutSessions.createPayment({	productReference: 'tshirt-blue-m', // or productUuid	reference: 'order-1042',	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_payment(    product_reference="tshirt-blue-m",  # or product_uuid=    reference="order-1042",    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)
session, err := client.CheckoutSessions.CreatePayment(ctx, &qbitflow.CreatePaymentSessionParams{	ProductReference: "tshirt-blue-m", // or ProductUUID	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->createPayment(new \QBitFlow\Params\CreatePaymentSessionParams(	productReference: 'tshirt-blue-m', // or productUuid: $product->uuid	reference: 'order-1042',	successUrl: 'https://shop.example.com/orders/success?uuid=' . \QBitFlow\Placeholders::UUID,	cancelUrl: 'https://shop.example.com/orders/cancel',));echo "Pay at {$session->link}\n";

Or an inline product: productName and priceUsd, a decimal string in USD (description optional; a number answers 400), nothing to create first. Never both.

  • Test mode caps a checkout's price, fees included (test mode). A live subscription's price must be above $0.25.
  • Name the customer with customerUuid or customerReference (your own id) and the checkout asks them nothing. Otherwise your checkout.customerDetails setting decides: nothing, an email, or a name and an email.
  • Checkout fees: up to 10 lines of your own (a tax, shipping) and QBitFlow's processing fee, paid with the price, line by line: once for a payment, with every bill for a subscription (checkout fees).

Redirect URLs

successUrl and cancelUrl (default: your checkout.defaults setting) are where your customer lands after paying or leaving. Live mode wants https://. QBitFlow fills {{UUID}} with the session's id and {{TRANSACTION_TYPE}} with payment or createSubscription:

From the catalog · successUrl

You sendhttps://shop.example.com/orders/success?uuid={{UUID}}
QBitFlow writes in the checkout's id
Your customer lands onhttps://shop.example.com/orders/success?uuid=pay@01a0f755-f200-7000-8000-000000000001

A member gives their own while your members.checkoutRedirects policy allows it (on by default: their shop sends customers back to its own site). With it off, a member's own key (or their dashboard) sending either answers 403 policy_disabled, details.field naming it, and their checkouts go where your checkout.defaults says. Your organization key with On-Behalf-Of always gives them.

Anyone can open your success URL, so reaching it proves nothing: fulfil on the webhook (payment.completed, subscription.created) or the session's status (webhooks).

Status and expiry

Read a checkout's status

checkout-status.sh

# Where a checkout stands: created, waitingConfirmation, completed or expired. A failed attempt# (lastAttempt) is not final: the customer can still pay from the same checkout.curl "https://api.qbitflow.app/v2/checkout-sessions/$SESSION_UUID" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: { "uuid": "pay@…", "status": "created", "lastAttempt": { "status": "failed", "code": "insufficient_funds", … } }
const status = await client.checkoutSessions.get(sessionUuid);switch (status.status) {	case CheckoutSessionStatusValue.Completed:		console.log(`paid, tx ${status.txHash}`);		break;	case CheckoutSessionStatusValue.Expired:		console.log('expired unpaid');		break;	case CheckoutSessionStatusValue.WaitingConfirmation:		console.log('sent, waiting for the network');		break;	default:		// created: waiting for the customer. A failed attempt (lastAttempt) is not final: the		// customer can still pay from the same checkout.		console.log(`open (${status.status}), last attempt: ${status.lastAttempt?.code ?? 'none'}`);}
status = client.checkout_sessions.get(session_uuid)  # pay@… or sub@…if status.status == qbitflow.CheckoutSessionStatusValue.COMPLETED:    print("paid, tx", status.tx_hash)  # the payment (or subscription) has the session's idelif status.status == qbitflow.CheckoutSessionStatusValue.EXPIRED:    print("expired unpaid")elif status.last_attempt is not None:    print("last attempt failed:", status.last_attempt.code)  # not final: they can try againelse:    print("waiting:", status.status)  # created or waitingConfirmation
status, err := client.CheckoutSessions.Get(ctx, sessionUUID)if err != nil {	log.Fatal(err)}switch status.Status {case qbitflow.CheckoutSessionStatusCompleted:	fmt.Println("paid, tx", status.TxHash)case qbitflow.CheckoutSessionStatusExpired:	fmt.Println("expired unpaid:", status.Message)case qbitflow.CheckoutSessionStatusWaitingConfirmation:	fmt.Println("sent, waiting for the network")default: // created, or a status this SDK does not know	if status.LastAttempt != nil {		// Not final: the customer may still pay from the same checkout.		fmt.Println("last attempt failed:", status.LastAttempt.Code)	}}
$status = $client->checkoutSessions->get($sessionUuid); // pay@… or sub@…echo match ($status->status) {	\QBitFlow\Enums\CheckoutSessionStatusValue::COMPLETED => "Paid, tx {$status->txHash}\n",	\QBitFlow\Enums\CheckoutSessionStatusValue::EXPIRED => "Expired unpaid: {$status->message}\n",	\QBitFlow\Enums\CheckoutSessionStatusValue::WAITING_CONFIRMATION => "Sent, waiting for the network\n",	default => $status->lastAttempt !== null // created, or a status this SDK does not know		? "Last attempt failed ({$status->lastAttempt->code}): the customer may try again\n"		: "Waiting for the customer\n",};

GET /checkout-sessions/:uuid answers created, waitingConfirmation (a transaction was sent), then completed or expired.

  • A failed attempt isn't a status. The session goes back to created with lastAttempt, and the customer can retry from the same link. Never cancel an order on lastAttempt: give up on expired (checkout.expired).
  • Lifetime. QBitFlow's default, at most 24 hours, or expiresInMinutes (10 to 1,440, never above the default). A payment sent just before expiresAt is waited for, up to 3 hours.
  • References. A reference is unique among your payments (or subscriptions), and an open session holds it: a second session with it answers 409 unique_violation.

Order cancelled? Expire the session: it can't be paid, checkout.expired is sent once and its reference is free again. One already paid (or being paid) answers 409 tx_already_sent.

Expire a checkout

checkout-expire.sh

# The customer can no longer pay it, and checkout.expired follows. Once they paid or are paying,# it answers 409 tx_already_sent.curl -X POST "https://api.qbitflow.app/v2/checkout-sessions/$SESSION_UUID/expire" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a CheckoutSessionStatus ({ "uuid": "pay@…", "status": "expired", … })
// The customer can no longer pay it, and checkout.expired follows. Once they paid or are paying,// it is a ConflictError tx_already_sent.const expired = await client.checkoutSessions.expire(sessionUuid);console.log(`checkout ${expired.status}`); // expired
# An order cancelled on your side: the checkout can no longer be paid.expired = client.checkout_sessions.expire(session_uuid)print(expired.status)  # expired; checkout.expired follows
// Once the customer paid or is paying, this is a 409 tx_already_sent.expired, err := client.CheckoutSessions.Expire(ctx, sessionUUID)if err != nil {	log.Fatal(err)}fmt.Println(expired.Status) // expired; checkout.expired follows
$expired = $client->checkoutSessions->expire($sessionUuid);echo "Now {$expired->status}\n"; // expired: the customer can no longer pay it

Every product has a paymentLink to share anywhere: a button, an email, a QR code. Opening it creates a checkout of the product with your checkout.defaults. The same visitor reopening it within 15 minutes, while it's open and nothing was sent, lands on the checkout they opened (a reload or a link preview opens no other), and a visit nobody pays sends no checkout.expired. Hiding or deleting the product turns its link off.

Checkout sessionPayment link
Created byyour server, per ordernobody: every product has one
Sellsa catalog or an inline productits product
Reference, customer, redirectsper sessionyour checkout.defaults
Checkout feesthe session's feesyour checkout.customerPaysProcessingFee setting
Best foran integration that knows the ordera no-code button, an email, a QR code