Take a payment

One checkout per order. Send your customer to its link, follow it to the end, then pay it yourself.

A checkout session is the hosted page your customer pays on (checkout). Its id (pay@…) becomes the payment's id, for life: store it with your order.

  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 a checkout

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

It answers 201 with:

  • link: the checkout page. Redirect your customer there.
  • uuid: pay@…, the session's id and the payment's.
  • expiresAt: at most 24 hours away, or the expiresInMinutes you give (10 to 1,440).

reference is your order id: it comes back on the payment and its webhook. successUrl and cancelUrl are where your customer goes next; {{UUID}} in them becomes the session's id. Fulfil on payment.completed (next step), never on the redirect: anyone can open your success URL.

Or sell a catalog product

Name it by productReference (or productUuid) instead of the inline fields. Both together answer 400; a product that isn't yours, 404 not_found.

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";

Both examples use the order reference order-1042, and an open checkout holds its reference: the second answers 409 unique_violation. Change it, or expire the first checkout.

Add taxes, shipping or the processing fee

Up to 10 lines of your own (a tax, shipping), plus QBitFlow's processing fee if your customer pays it. They see every line before paying.

Add fees to a checkout

checkout-create-payment-fees.sh

# The customer pays the price plus your lines and, with processingFee, QBitFlow's fee (grossed up,# so you keep the price and your lines). Test mode caps the total at $5, fees included; the# network fee comes on top.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": "3.99",    "reference": "order-1044",    "successUrl": "https://shop.example.com/orders/success?uuid={{UUID}}",    "cancelUrl": "https://shop.example.com/orders/cancel",    "fees": {      "items": [{ "label": "Shipping", "description": "Standard, 3 to 5 days", "amountUsd": "0.75" }],      "processingFee": true    }  }'# 201: a CreatedCheckoutSession ({ "link": "https://…/checkout?uuid=pay@…", "uuid": "pay@…", "expiresAt": "…" })
// The customer pays the price plus your lines and, with processingFee, QBitFlow's fee (grossed// up, so you keep the price and your lines). Test mode caps the total at $5, fees included; the// network fee comes on top.const session = await client.checkoutSessions.createPayment({	productName: 'T-shirt',	description: 'Blue, size M',	priceUsd: '3.99', // USD	reference: 'order-1044',	successUrl: `https://shop.example.com/orders/success?uuid=${Placeholders.UUID}`,	cancelUrl: 'https://shop.example.com/orders/cancel',	fees: {		items: [{ label: 'Shipping', description: 'Standard, 3 to 5 days', amountUsd: '0.75' }],		processingFee: true, // omitted: your checkout.customerPaysProcessingFee setting decides	},});console.log(`Redirect the customer to ${session.link}`);
session = client.checkout_sessions.create_payment(    product_name="T-shirt",    description="Blue, size M",    price_usd="3.99",  # USD: you keep the price and your lines    reference="order-1044",    success_url=f"https://shop.example.com/orders/success?uuid={qbitflow.PLACEHOLDER_UUID}",    cancel_url="https://shop.example.com/orders/cancel",    fees=qbitflow.CheckoutFees(        # Your own lines (up to 10), shown to the customer and paid with the price.        items=[            qbitflow.FeeItem(                label="Shipping", description="Standard, 3 to 5 days", amount_usd="0.75"            ),        ],        # The customer also pays QBitFlow's processing fee, a last line computed by QBitFlow.        processing_fee=True,    ),)# The customer pays the price plus every line (then the network fee on top).print("Send the customer to", session.link)
// The customer pays the price plus every line: $3.99 + $0.75 + the processing fee. You// keep the price and your lines. Test mode caps the total, fees included, at $5.session, err := client.CheckoutSessions.CreatePayment(ctx, &qbitflow.CreatePaymentSessionParams{	ProductName: "T-shirt",	Description: "Blue, size M",	PriceUSD:    "3.99",	Reference:   "order-1044",	SuccessURL:  "https://shop.example.com/orders/success?uuid=" + qbitflow.PlaceholderUUID,	CancelURL:   "https://shop.example.com/orders/cancel",	Fees: &qbitflow.CheckoutFees{		Items: []qbitflow.FeeItem{			{Label: "Shipping", Description: "Standard, 3 to 5 days", AmountUSD: "0.75"},		},		// QBitFlow's processing fee, grossed up so you keep the price and your lines.		// Left nil, your checkout.customerPaysProcessingFee setting decides.		ProcessingFee: new(true),	},})if err != nil {	log.Fatal(err)}fmt.Println("Redirect the customer to", session.Link)
$session = $client->checkoutSessions->createPayment(new \QBitFlow\Params\CreatePaymentSessionParams(	productName: 'T-shirt',	description: 'Blue, size M',	priceUsd: '3.99',	reference: 'order-1044',	successUrl: 'https://shop.example.com/orders/success?uuid=' . \QBitFlow\Placeholders::UUID,	cancelUrl: 'https://shop.example.com/orders/cancel',	fees: new \QBitFlow\Params\CheckoutFees(		items: [			new \QBitFlow\Params\FeeItem(label: 'Shipping', amountUsd: '0.75', description: 'Standard, 3 to 5 days'),		],		processingFee: true, // the customer pays QBitFlow's fee: you keep 3.99 + 0.75	),));echo "Pay at {$session->link}\n"; // the customer pays the price, the shipping and the processing fee
  • A line: a label, an optional description and an amountUsd above 0, a decimal string (each field's limits).
  • processingFee: true adds QBitFlow's fee, grossed up so you keep the price and your lines in full. Left out, your checkout.customerPaysProcessingFee setting decides.
  • The customer pays the price plus every line (the session's amountUsd, within test mode's cap), and the network fee on top.

Follow its status

Your customer picks a currency your wallets accept and confirms from their wallet: one transaction, their wallet to yours (who pays the network fee).

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",};

created, then waitingConfirmation, then completed or expired, as drawn above. A failed attempt goes back to created with lastAttempt, and the customer can try again: never cancel an order on it, give up on expired.

Waiting for the end suits a script or a test. In production, let the webhook tell you the moment it's paid:

Wait for a checkout to finish

wait-for-completion.sh

# Waiting for a checkout means reading its status again (every few seconds) until it is completed# or expired. Webhooks are the way to fulfil orders.curl "https://api.qbitflow.app/v2/checkout-sessions/$SESSION_UUID" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a CheckoutSessionStatus
// Polls get until the session is completed or expired. When the timeout comes first,// it returns the last status seen: check it.const final = await client.checkoutSessions.waitForCompletion(sessionUuid, {	timeout: 10 * 60_000, // ms	interval: 5_000, // ms, at least 1000});if (final.status === CheckoutSessionStatusValue.Completed) {	console.log(`paid, tx ${final.txHash}`);} else {	console.log(`not paid: ${final.status}`); // expired, or still open at the timeout}
# For scripts and back-office jobs: fulfil orders on the payment.completed webhook. It polls# checkout_sessions.get; on timeout it returns the last status seen (check .status).final = client.checkout_sessions.wait_for_completion(session_uuid, timeout=600, interval=3)if final.status == qbitflow.CheckoutSessionStatusValue.COMPLETED:    print("paid")else:    print("not paid:", final.status)  # expired, or still open when the timeout elapsed
// For scripts, tests and back-office jobs: orders are fulfilled on the webhook.status, err := client.CheckoutSessions.WaitForCompletion(ctx, sessionUUID, &qbitflow.WaitOptions{	Timeout:  2 * time.Minute, // default 10 minutes	Interval: 5 * time.Second, // default 3 s})if err != nil {	log.Fatal(err) // Get's errors, or the context's}if status.Status != qbitflow.CheckoutSessionStatusCompleted {	fmt.Println("not paid (yet):", status.Status) // expired, or still open at the timeout}
// For scripts and back-office jobs: fulfil orders on the payment.completed webhook.$status = $client->checkoutSessions->waitForCompletion($sessionUuid, timeout: 600, interval: 3);if ($status->status === \QBitFlow\Enums\CheckoutSessionStatusValue::COMPLETED) {	echo "Paid, tx {$status->txHash}\n";} else {	echo "Not paid: {$status->status}\n"; // expired, or still pending when the timeout elapsed}

End a checkout sooner

Order cancelled? Expire its checkout: it can no longer 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

Pay it yourself

Pay the link with test tokens from the QBFT faucet (the last step covers testing in full).

Check it

The status reads completed, with a txHash, and the payment is in Dashboard/Payments. 409 merchant_not_ready on create: add a wallet.