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.
- createdYou create it201 with its link, uuid and expiresAt
- waitingConfirmationThe customer paysA failed attempt goes back to created, with lastAttempt
- completed ✓Confirmed on-chainpayment.completed or subscription.created
- expiredOr, unpaid at expiresAtcheckout.expired. You can expire it sooner
Create one
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 checkoutPOST /checkout-sessions/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:
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
customerUuidorcustomerReference(your own id) and the checkout asks them nothing. Otherwise yourcheckout.customerDetailssetting 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
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
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 waitingConfirmationstatus, 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/created,
waitingConfirmation (a transaction was sent), then completed or expired.
- A failed attempt isn't a status. The session goes back to
createdwithlastAttempt, and the customer can retry from the same link. Never cancel an order onlastAttempt: give up onexpired(checkout.expired). - Lifetime. QBitFlow's default, at most 24 hours, or
expiresInMinutes(10 to 1,440, never above the default). A payment sent just beforeexpiresAtis waited for, up to 3 hours. - References. A
referenceis unique among your payments (or subscriptions), and an open session holds it: a second session with it answers409 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.
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 itPayment links
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 session | Payment link | |
|---|---|---|
| Created by | your server, per order | nobody: every product has one |
| Sells | a catalog or an inline product | its product |
| Reference, customer, redirects | per session | your checkout.defaults |
| Checkout fees | the session's fees | your checkout.customerPaysProcessingFee setting |
| Best for | an integration that knows the order | a no-code button, an email, a QR code |