API reference

Checkout sessions

  • 4 endpoints

Creating a session is cheap: it stores the session (in Redis) and, for a subscription's inline product, creates a hidden product; nothing happens on-chain until the customer pays.

Create payment checkout session#

POST /checkout-sessions/payment

Opens a one-time payment's checkout session and answers its link, for the customer to pay on QBitFlow's hosted page. A member opens one while their organization's members.products policy allows it, and gives successUrl or cancelUrl while its members.checkoutRedirects policy does (403 policy_disabled otherwise). A member's fees.processingFee must equal their organization's checkout.customerPaysProcessingFee (403 processing_fee_set_by_organization, details.field).

Auth
X-API-Key: any keyA member's key needs the members.products policy
On-Behalf-Of
Honored: acts in the member's space
Idempotency-Key
Accepted: send one per operation
Success
201
Request

request.sh

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

Also: Retries and idempotency keys

Body

  • referencestringOptional

    Optional reference for the transaction, set by the user when creating the transaction. This is used to identify the transaction in the system, and it can be used to link the transaction to a specific order or invoice.

    referencemax=100

  • productUuidstringOptional

    User must provide Optional: the product being purchased, one of the request's space's (unknown or another space's: 404 not_found)

  • productReferencestringOptional

    Optional: instead of the product's uuid, the reference of one of the request's space's products (unknown: 404 not_found). Without productUuid or productReference, productName and price are required (description optional, as a product's): an inline product (a payment keeps it as a snapshot, without productUuid; a subscription uses a hidden product, shared by the space's inline products of the same name, description and price)

    referencemax=100

  • productNamestringOptional

    An inline product's name (with priceUsd, without productUuid or productReference)

    namemin=2max=100

  • descriptionstringOptional

    An inline product's description, optional

    textmin=2max=500

  • priceUsdnumberOptional

    An inline product's price in USD, a string ("12.50"): above 0, at most 1,000,000, 2 decimals; at most 5 in test mode (400 validation_failed on priceUsd, details.max "5"). A live subscription's (inline or its product's) is above 0.25, the smallest bill QBitFlow charges: 400 validation_failed on priceUsd (productUuid, productReference) with details.min

    usd=1000000

  • successUrlstringOptional

    Optional: where the customer goes after paying, through QBitFlow's page for the payment ({{UUID}}: the session's prefixed id, {{TRANSACTION_TYPE}}: payment or createSubscription). Default: the checkout.defaults setting. https only in live mode (http → 400 validation_failed, the setting's too). A member's only while their organization's members.checkoutRedirects policy allows it (on by default; off → 403 policy_disabled, details.field). Fulfil on the webhook, not on this redirect

    http_urlmax=2048

  • cancelUrlstringOptional

    Optional: where the customer goes after a cancelled or failed payment (same placeholders). Default: the checkout.defaults setting. https only in live mode. A member's only while members.checkoutRedirects allows it, as successUrl

    http_urlmax=2048

  • customerUuidstringOptional

    Optional: one of the space's customers (unknown or another space's: 404 not_found). The checkout then asks the customer nothing

  • customerReferencestringOptional

    Optional: your own reference of the customer, kept on the payment or subscription (customerReference). It needs no customer at QBitFlow: the space's customer with this reference is linked (customerUuid) if there is one, and none is created otherwise. The checkout then asks the customer nothing: without a customer of QBitFlow's, nobody gets the receipt or any other customer email (create the customer with this reference and their email first, POST /customers, to have them). Without customerUuid or customerReference, the checkout asks what the checkout.customerDetails setting says

    referencemax=100

  • expiresInMinutesintegerOptional

    Optional: how long the checkout can be paid, in minutes: at least 10, at most 1440 (24 hours) and never longer than the default lifetime (a longer one → 400 validation_failed, its message giving the maximum). Omitted: the default lifetime. The response's expiresAt tells when it ends.

    min=10max=1440

  • metadatamapOptional

    Optional: your own data on the checkout (an order id, a CRM key): at most 50 keys of 1–40 characters (letters, digits and _ - . :), values of at most 500 characters ("" values are dropped). Copied onto the payment or the subscription it creates, and into its webhooks (payment.completed, subscription.*, checkout.expired). Never shown to the customer. Don't put secrets or personal data in it

    metadata

  • feesCheckoutFeesDtoOptional

    Optional: amounts added to the price (a tax, shipping, QBitFlow's processing fee), shown to the customer line by line and paid with it. The session's and the payment's amount is the price plus every line; QBitFlow's fee (and the organization's) is taken on that amount

    2 fields of CheckoutFeesDto
    • processingFeebooleanOptional

      Optional: the customer pays QBitFlow's fee on top (a processingFee line, last). It is computed on the price and your lines, at your platform fee, grossed up (the fee is also taken on the line itself) and rounded up to the cent, so you keep at least the price and your lines, as if no fee were taken. Omitted: the checkout.customerPaysProcessingFee setting decides (default false)

    • itemsarray of FeeItemDtoOptional

      Optional: up to 10 lines of your own, shown in this order before the processing fee

      max=10

      3 fields of each FeeItemDto
      • labelstringRequired

        The line's name, as the checkout shows it (one line, at most 40 characters): "VAT (20%)", "Shipping"

        namemax=40

      • descriptionstringOptional

        Optional: more about the line, at most 200 characters

        textmax=200

      • amountUsdnumberRequired

        The line's amount in USD, a string ("4.99"): above 0, at most 1,000,000, with at most 2 decimals

        usd=1000000

Response

A CreatedCheckoutSession.

  • linkstring

    The checkout page for the customer: <merchant's checkout base URL>/checkout?uuid=<uuid>

  • uuidstring

    The session's prefixed id (pay@…, sub@…), also the uuid of the payment or subscription it creates

  • expiresAtstring

    When the checkout can no longer be paid (then checkout.expired, if it wasn't paid)

Errors

Create subscription checkout session#

POST /checkout-sessions/subscription

Opens a subscription's checkout session and answers its link, for the customer to subscribe on QBitFlow's hosted page. A member opens one while their organization's members.products and members.subscriptions policies allow it, and gives successUrl or cancelUrl while its members.checkoutRedirects policy does (403 policy_disabled otherwise). A member's fees.processingFee must equal their organization's checkout.customerPaysProcessingFee (403 processing_fee_set_by_organization, details.field).

Auth
X-API-Key: any keyA member's key needs the members.products policy
On-Behalf-Of
Honored: acts in the member's space
Idempotency-Key
Accepted: send one per operation
Success
201
Request

request.sh

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

Body

  • referencestringOptional

    Optional reference for the transaction, set by the user when creating the transaction. This is used to identify the transaction in the system, and it can be used to link the transaction to a specific order or invoice.

    referencemax=100

  • productUuidstringOptional

    User must provide Optional: the product being purchased, one of the request's space's (unknown or another space's: 404 not_found)

  • productReferencestringOptional

    Optional: instead of the product's uuid, the reference of one of the request's space's products (unknown: 404 not_found). Without productUuid or productReference, productName and price are required (description optional, as a product's): an inline product (a payment keeps it as a snapshot, without productUuid; a subscription uses a hidden product, shared by the space's inline products of the same name, description and price)

    referencemax=100

  • productNamestringOptional

    An inline product's name (with priceUsd, without productUuid or productReference)

    namemin=2max=100

  • descriptionstringOptional

    An inline product's description, optional

    textmin=2max=500

  • priceUsdnumberOptional

    An inline product's price in USD, a string ("12.50"): above 0, at most 1,000,000, 2 decimals; at most 5 in test mode (400 validation_failed on priceUsd, details.max "5"). A live subscription's (inline or its product's) is above 0.25, the smallest bill QBitFlow charges: 400 validation_failed on priceUsd (productUuid, productReference) with details.min

    usd=1000000

  • successUrlstringOptional

    Optional: where the customer goes after paying, through QBitFlow's page for the payment ({{UUID}}: the session's prefixed id, {{TRANSACTION_TYPE}}: payment or createSubscription). Default: the checkout.defaults setting. https only in live mode (http → 400 validation_failed, the setting's too). A member's only while their organization's members.checkoutRedirects policy allows it (on by default; off → 403 policy_disabled, details.field). Fulfil on the webhook, not on this redirect

    http_urlmax=2048

  • cancelUrlstringOptional

    Optional: where the customer goes after a cancelled or failed payment (same placeholders). Default: the checkout.defaults setting. https only in live mode. A member's only while members.checkoutRedirects allows it, as successUrl

    http_urlmax=2048

  • customerUuidstringOptional

    Optional: one of the space's customers (unknown or another space's: 404 not_found). The checkout then asks the customer nothing

  • customerReferencestringOptional

    Optional: your own reference of the customer, kept on the payment or subscription (customerReference). It needs no customer at QBitFlow: the space's customer with this reference is linked (customerUuid) if there is one, and none is created otherwise. The checkout then asks the customer nothing: without a customer of QBitFlow's, nobody gets the receipt or any other customer email (create the customer with this reference and their email first, POST /customers, to have them). Without customerUuid or customerReference, the checkout asks what the checkout.customerDetails setting says

    referencemax=100

  • expiresInMinutesintegerOptional

    Optional: how long the checkout can be paid, in minutes: at least 10, at most 1440 (24 hours) and never longer than the default lifetime (a longer one → 400 validation_failed, its message giving the maximum). Omitted: the default lifetime. The response's expiresAt tells when it ends.

    min=10max=1440

  • metadatamapOptional

    Optional: your own data on the checkout (an order id, a CRM key): at most 50 keys of 1–40 characters (letters, digits and _ - . :), values of at most 500 characters ("" values are dropped). Copied onto the payment or the subscription it creates, and into its webhooks (payment.completed, subscription.*, checkout.expired). Never shown to the customer. Don't put secrets or personal data in it

    metadata

  • frequencyDurationOptional

    How often it is billed: at least 1 hour (5 minutes in test mode), at most 1 year

    2 fields of Duration
    • valueintegerOptional

      Number of units; 0 means no duration

    • unitstringOptional

      Required when value > 0. "months" = 30 days, "years" = 365 days

      oneof=seconds minutes hours days weeks months years

  • trialPeriodDurationOptional

    A free trial before the first bill ({"value": 0}: none, removing a product's)

    2 fields of Duration
    • valueintegerOptional

      Number of units; 0 means no duration

    • unitstringOptional

      Required when value > 0. "months" = 30 days, "years" = 365 days

      oneof=seconds minutes hours days weeks months years

  • minPeriodsintegerOptional

    The periods the customer commits to before they can cancel (0: none, removing a product's), at most 1000

    max=1000

  • feesCheckoutFeesDtoOptional

    Optional: amounts added to the price (a tax, shipping, QBitFlow's processing fee), shown to the customer line by line and locked with the price: every bill charges the price plus every line. QBitFlow's fee (and the organization's) is taken on that amount

    2 fields of CheckoutFeesDto
    • processingFeebooleanOptional

      Optional: the customer pays QBitFlow's fee on top (a processingFee line, last). It is computed on the price and your lines, at your platform fee, grossed up (the fee is also taken on the line itself) and rounded up to the cent, so you keep at least the price and your lines, as if no fee were taken. Omitted: the checkout.customerPaysProcessingFee setting decides (default false)

    • itemsarray of FeeItemDtoOptional

      Optional: up to 10 lines of your own, shown in this order before the processing fee

      max=10

      3 fields of each FeeItemDto
      • labelstringRequired

        The line's name, as the checkout shows it (one line, at most 40 characters): "VAT (20%)", "Shipping"

        namemax=40

      • descriptionstringOptional

        Optional: more about the line, at most 200 characters

        textmax=200

      • amountUsdnumberRequired

        The line's amount in USD, a string ("4.99"): above 0, at most 1,000,000, with at most 2 decimals

        usd=1000000

Response

A CreatedCheckoutSession.

  • linkstring

    The checkout page for the customer: <merchant's checkout base URL>/checkout?uuid=<uuid>

  • uuidstring

    The session's prefixed id (pay@…, sub@…), also the uuid of the payment or subscription it creates

  • expiresAtstring

    When the checkout can no longer be paid (then checkout.expired, if it wasn't paid)

Errors

Get checkout session#

GET /checkout-sessions/:uuid

Returns the status of a checkout session created by the caller (same space, so same mode), so a merchant can tell a session still waiting for its customer (created; with lastAttempt when their last attempt failed: not final, they may try again until the session expires) from one in progress, completed or expired. A session that expired unpaid answers expired while QBitFlow keeps its expiry's record (days), then 404, like an unknown id or another owner's session. A member needs the members.products policy (403 policy_disabled).

Auth
X-API-Key: any keyA member's key needs the members.products policy
On-Behalf-Of
Honored: acts in the member's space
Idempotency-Key
Ignored
Success
200
Request

request.sh

curl "https://api.qbitflow.app/v2/checkout-sessions/$SESSION_UUID" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a CheckoutSessionStatus
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",};
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}

Path parameters

  • uuidstringRequired

    The session's prefixed id (pay@…, sub@…), or its bare uuid

    txuuid

Response

A CheckoutSessionStatus.

  • uuidstring

    The session's prefixed id (pay@…, sub@…), also the id of the payment or subscription it creates

  • statusstring

    created: waiting for the customer (lastAttempt: their last attempt failed, they may try again until the session expires); waitingConfirmation: transaction sent; completed, expired: final

    One of:

    • createdWaiting for the customer: nothing sent yet, or the last attempt failed (lastAttempt)
    • waitingConfirmationA transaction was sent: waiting for the network and its record
    • completedConfirmed and recorded. Final
    • expiredThe checkout expired unpaid (at its expiresAt, or expired by its merchant). Final
  • txHashstringOptional

    Hash of the customer's transaction, once sent (not kept for completed subscriptions)

  • messagestringOptional

    Optional detail, e.g. why the session expired

  • lastAttemptAttemptOptional

    The customer's last attempt failed (its code, reason and time): never final, don't cancel the order on it; wait for completed (payment.completed, subscription.created) or expired (checkout.expired)

    5 fields of Attempt
    • statusstring

      failed

      One of:

      • failedThe attempt's transaction wasn't sent, didn't confirm or failed on the network: nothing was paid
    • codestringOptional

      The error code (errors): what to do about it (insufficient_funds, token_not_approved, tx_not_sent…)

    • messagestringOptional

      The reason, for the customer

    • txHashstringOptional

      The attempt's transaction, if it was sent

    • atstring

      When it failed

  • expiresAtstringOptional

    When the checkout can no longer be paid, while it is open (and once its merchant expired it: when); absent once completed, or expired at its time

Errors

  • 403policy_disabled

    a member doing what their organization's policy doesn't allow them, reads of what the policy governs included

Expire checkout session#

POST /checkout-sessions/:uuid/expire

Ends a checkout session the caller created now, before its expiresAt (an order cancelled, a cart changed): its intent and its send answer 404 from now on, checkout.expired is sent, and its status answers expired. Expiring an expired session answers the same again. A session whose customer paid, or sent a payment still running, can't be expired: 409 tx_already_sent. Another owner's session or an unknown id: 404. A member needs the members.products policy (403 policy_disabled).

Auth
X-API-Key: any keyA member's key needs the members.products policy
On-Behalf-Of
Honored: acts in the member's space
Idempotency-Key
Ignored
Success
200
Request

request.sh

curl -X POST "https://api.qbitflow.app/v2/checkout-sessions/$SESSION_UUID/expire" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a CheckoutSessionStatus
// 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

Path parameters

  • uuidstringRequired

    The session's prefixed id (pay@…, sub@…), or its bare uuid

    txuuid

Response

A CheckoutSessionStatus.

  • uuidstring

    The session's prefixed id (pay@…, sub@…), also the id of the payment or subscription it creates

  • statusstring

    created: waiting for the customer (lastAttempt: their last attempt failed, they may try again until the session expires); waitingConfirmation: transaction sent; completed, expired: final

    One of:

    • createdWaiting for the customer: nothing sent yet, or the last attempt failed (lastAttempt)
    • waitingConfirmationA transaction was sent: waiting for the network and its record
    • completedConfirmed and recorded. Final
    • expiredThe checkout expired unpaid (at its expiresAt, or expired by its merchant). Final
  • txHashstringOptional

    Hash of the customer's transaction, once sent (not kept for completed subscriptions)

  • messagestringOptional

    Optional detail, e.g. why the session expired

  • lastAttemptAttemptOptional

    The customer's last attempt failed (its code, reason and time): never final, don't cancel the order on it; wait for completed (payment.completed, subscription.created) or expired (checkout.expired)

    5 fields of Attempt
    • statusstring

      failed

      One of:

      • failedThe attempt's transaction wasn't sent, didn't confirm or failed on the network: nothing was paid
    • codestringOptional

      The error code (errors): what to do about it (insufficient_funds, token_not_approved, tx_not_sent…)

    • messagestringOptional

      The reason, for the customer

    • txHashstringOptional

      The attempt's transaction, if it was sent

    • atstring

      When it failed

  • expiresAtstringOptional

    When the checkout can no longer be paid, while it is open (and once its merchant expired it: when); absent once completed, or expired at its time

Errors

Edge cases

The contract's rules for these routes: Authentication & roles · Creating a session · Fees — fees (payments and subscriptions) · Session status — GET /checkout-sessions/:uuid · Expiring a session — POST /checkout-sessions/:uuid/expire.

Read all 5

Authentication & roles

  • A member creates subscription checkouts while the organization's members.subscriptions policy allows it (on by default; off → 403 policy_disabled, one-time payments only); the team always can. The policy covers the other ways to sell a subscription too: giving a product subscription terms (Products) and the link of a member's subscription product (Payment links).
  • A member opens checkouts (both create routes, from the dashboard or their own key, inline or of a product) while the organization's members.products policy allows it (on by default). Off → 403 policy_disabled, details.policy: "members.products" (checked before members.subscriptions): the member is paid only through the organization, which still opens checkouts in their space (switched into it, or On-Behalf-Of with an organization key). GET /checkout-sessions/:uuid and POST /checkout-sessions/:uuid/expire are refused the same way (403 policy_disabled: the organization reads and expires the checkouts opened for the member). The team passes, acting in the member's space too. The links of the products in their space are closed too (Payment links).
  • A checkout session is named by its prefixed id (pay@…, sub@…: also the id of the payment or subscription it creates) or its bare uuid, on every :uuid route; a malformed value → 400 validation_failed on uuid.

Creating a session

  • Idempotency-Key (idempotency): both creates take one. A create retried with the same key (a timeout, a 5xx) answers the first CreatedCheckoutSession again (the same uuid, link and expiresAt, Idempotent-Replayed: true) for 24 hours, and opens no second session (so no second checkout.expired later for an order paid on the first). SDKs send one on every create.
  • Success is HTTP 201 with a CreatedCheckoutSession { link, uuid, expiresAt } (all three always present). The uuid is pay@-prefixed for a payment session and sub@-prefixed for a subscription session. expiresAt is when the checkout can no longer be paid. If it wasn't paid by then, checkout.expired is sent (Webhook events) and its status becomes expired. A payment sent right before the expiry is waited for (up to 3 hours) and completes normally.
  • Lifetime. A session lives for QBitFlow's default lifetime (at most 24 hours; a session's expiresAt tells it), or expiresInMinutes (optional, an integer from 10 to 1440, and never longer than the default lifetime: 400 validation_failed on expiresInMinutes otherwise, its message giving the maximum). The merchant can end it earlier with POST /checkout-sessions/:uuid/expire.
  • A checkout nobody could pay is refused: 409 merchant_not_ready, before anything is written (no reference held, no product or customer created), details.reason saying what is missing:
    • noWallet: the space's wallets accept no currency (no wallet yet);
    • organizationNoWallet: the organization holds the member's funds (the trust layer: its payments go to the organization's wallets) and the organization's wallets accept no currency;
    • noTokenWallet (subscriptions): subscriptions are paid in tokens and the space's wallets accept only native coins;
    • fundsHeld (subscriptions, trials included): the organization holds the member's funds (the member isn't trusted yet, Members and held funds). A held member sells one-time payments only: see § Subscription only below.

The hosted session's availableCurrencies lists what the checkout accepts (currency objects, Currencies): what the receiving wallets accept, tokens only for a subscription. Onboarding order for a space (the organization's, or a member's): its wallets and the currencies they accept first (dashboard), then products, then checkouts. A held member's payment checkouts are paid to the organization's wallets, so the organization sets up its own first.

  • A space whose member was removed, or whose organization closed, opens no session: 410 merchant_closed. A session opened before is refused at its intent and at its send the same way (nothing is sent).
  • An expired session is gone: its intent and its send → 404, as for an unknown session, even in the instant before the store forgets it. An intent never extends a session's life.
  • A session a visit of a product's link opens (GET /pay/:productUuid, Payment links) is watched from its first intent, not from the visit: a visit nobody pays sends no checkout.expired.
  • Product (payment & subscription): the request either names an existing product (productUuid or productReference) or gives an inline product (productName + priceUsd, description optional). Neither → 400 validation_failed on each of productName and priceUsd that is missing.
    • A payment checkout of a subscription product sells it once, at its price: its subscription terms are ignored, nothing recurs. Open a subscription checkout to bill it every period.
    • priceUsd is a JSON string holding an amount in USD ("12.50", "1000"): above 0, at most 1,000,000, at most 2 decimals (the usd validator). A JSON number, a negative amount, a third decimal or an exponent → 400 validation_failed on priceUsd. Responses write it back canonical ("12.5"). (It was price, a JSON number.) How large a price can be paid also depends on the token chosen at the intent (Payments and transactions).
    • Test mode: at most $5 (test tokens come from faucets with a limited supply). An inline priceUsd above it → 400 validation_failed on priceUsd, details.max: "5"; a product above it (one priced before the cap was enforced) → the same on productUuid or productReference, whichever named it. Checked before anything is written. Live mode has no cap.
    • productUuid is the product's uuid; a value that is not a UUID → 400 validation_failed on productUuid.
    • A named product must be one of the request's space's. Another space's (another member's, another organization's), an unknown uuid or an unknown productReference → 404 not_found.
    • An inline product of a payment is a snapshot: the session, and the payment it records, carry its productName, description and price, and no productUuid (no product is created). An inline product of a subscription names a hidden (isActive=false) product of the space, which the subscription's productUuid names: the one with the same productName, description and price, created by the first such checkout (checkouts with the same inline product share it). A checkout refused on its inputs, its customer, its reference or the space's readiness creates none. One refusal comes after it: a test-mode subscription whose amount with its fees is over the cap (400 validation_failed on fees, below) has already created the hidden product. It stays hidden and unlisted (GET /products shows it only with includeHidden=true), holds no checkout, and the next checkout with the same inline product reuses it.
    • The two forms are mutually exclusive: productUuid or productReference together with any of productName / description / priceUsd → 400 validation_failed on productUuid (or productReference, whichever was given).
    • Both session types accept both forms.
  • Metadata (metadata, optional): your own data on the checkout (an order id, a CRM key), a map of strings. At most 50 keys; a key is 1–40 characters of A–Z a–z 0–9 _ - . :; a value at most 500 characters, without control or bidirectional characters. A broken limit → 400 validation_failed on metadata (too many keys, a malformed key: the key is never echoed) or metadata.<key> (its value). A key whose value is "" is dropped. The session's metadata is copied once onto what it creates: the payment (Payment.metadata, read-only, and payment.completed) or the subscription (Subscription.metadata, editable with PATCH /subscriptions/:uuid, and every subscription.* event). checkout.expired carries it. It is never shown to the customer (the hosted page, the public transaction reads). Never put secrets or personal data in it.
  • Subscription only: never for a held member. In a member's space whose funds the organization still holds (the trust layer: the member isn't trusted yet, Members and held funds), a subscription checkout (with or without a trial) is refused: 409 merchant_not_ready, details.reason: "fundsHeld", before anything is written. A subscription's receiver is fixed on-chain when the subscriber signs, for the subscription's whole life: one created while held would pay the organization's wallet forever, and the organization would have to forward every bill by hand. Once the organization trusts the member (POST /members/:userUuid/trust), their subscription checkouts open and pay the member's own wallet. A subscription product's link (GET /pay/:productUuid) of a held member opens nothing (the not-found page, Payment links). A session opened before this rule is refused the same way at its intent (POST /hosted/subscriptions/intent), nothing signed or recorded. The organization's own space, and a trusted member, sell subscriptions as usual.
  • Subscription only — durations. frequency and trialPeriod are Durations ({ "value": <uint32>, "unit": "seconds" | "minutes" | "hours" | "days" | "weeks" | "months" | "years" }, months = 30 days, years = 365 days). Every rule below answers 400 validation_failed with the field path in details.errors[].field:
    • unit must be one of the values above, and is required whenever value > 0 (frequency.unit, trialPeriod.unit).
    • A subscription product's terms are the defaults: with a productUuid (or productReference) of a subscription product, frequency, trialPeriod and minPeriods are all optional, and each one sent overrides the product's, the others kept. trialPeriod: { "value": 0 } and minPeriods: 0 remove the product's. The merged terms are validated as below (an override never gets around a rule).
    • frequency (the request's, else the product's) must be between a minimum and 1 year (frequency). The minimum depends on the mode: 1 hour live, 5 minutes in test mode (on a development server, live mode's minimum is 1 minute; test mode's stays 5 minutes). None at all (an inline product, or a one-time product, without frequency in the request), a zero value and a duration too long to count in seconds all fail on frequency. For a one-time product, the message names it: "Product <reference> has no subscription terms: pass frequency, or use a subscription product".
    • trialPeriod is optional. Omitted (without a product's), or { "value": 0 } (with or without a unit), means no trial. A trial longer than ~136 years → trialPeriod.
    • minPeriods is optional (0 to 1000).
    • A live subscription's price (inline, or its product's) must be above $0.25 → on priceUsd (or productUuid / productReference), details.min.
  • The customer. customerUuid and customerReference are optional. Give either, and the checkout asks the customer nothing (asksCustomerDetails: false, whatever checkout.customerDetails says; details sent at the intent are ignored):
    • customerUuid must be one of the space's customers (in the session's mode); otherwise → 404.
    • customerReference is your reference of the customer: QBitFlow needs no customer for it. The space's customer with that reference, if there is one, is linked (the payment's customerUuid; also one created between the checkout and its payment); otherwise none is created. Either way the payment, or the subscription and each of its bills, keeps it as customerReference, which payment.completed, subscription.created and the accounting export carry. Without a customer QBitFlow has no name and no address: the buyer gets no receipt and no other customer email (billing failures included); your system tells your customer. To have QBitFlow email them, create the customer with that reference and their email first (POST /customers): one created any time before the payment is recorded is linked to it, and gets its receipt (the emails.customerReceipts setting still applies).
    • Neither: the checkout collects what checkout.customerDetails asks (below).
  • successUrl and cancelUrl are optional (http_url, else 400); when absent, the merchant's checkout.defaults setting gives them. In live mode they must be https (the customer's browser goes there from the checkout): an http:// URL → 400 validation_failed on successUrl / cancelUrl, the setting's included (its message says so: change the setting, or give the URL). Test mode takes http (a local shop).
  • A member gives a checkout's successUrl and cancelUrl while their organization's members.checkoutRedirects policy allows it (on by default: a member's shop or plugin sends the customer back to its own site). With the policy off for the member, a member (their session or their own API key) sending either → 403 policy_disabled with details.policy: "members.checkoutRedirects" and details.field (successUrl first when both are sent), nothing created: their checkouts go where the checkout.defaults setting says (the organization's). The team gives them for a member whatever the policy (switched into their space, or an organization key with On-Behalf-Of), and a qbf.cash handle gives its own (a person, with no organization above it).
  • Redirect placeholders. In successUrl and cancelUrl (yours or the defaults'), QBitFlow replaces {{UUID}} with the session's prefixed id (pay@…, sub@…) and {{TRANSACTION_TYPE}} with its type (payment, createSubscription), e.g. https://shop.example/thanks?order=42&session={{UUID}}. The session keeps your URLs, placeholders filled: checkout.expired carries them as successUrl and cancelUrl. After a payment the customer first lands on QBitFlow's page for it (<checkout base>/manage?uuid=…), which then redirects to your successUrl; cancelUrl is used as is. A redirect proves nothing (anyone can open the URL): fulfil on the webhook (payment.completed, subscription.created) or GET /checkout-sessions/:uuid, never on the redirect alone.
  • A session the merchant didn't name a customer for (no customerUuid or customerReference) records what the checkout asks: customerDetails is the merchant's checkout.customerDetails setting (email, or full, the default: first name, last name and email; absent when the setting is none), and asksCustomerDetails tells whether the intent must send them (true when customerDetails is present and no customer was named). The intent then requires those details (Payments and transactions). The customer is recorded with the payment or subscription (a free trial: when it starts), never at an intent: an intent nobody pays adds no customer. With none, nothing the checkout sends is kept.
  • reference is optional. When set, it must not already be used by one of the space's payments (payment session) or subscriptions (subscription session), nor held by another open session of the space of the same kind → otherwise 409 (code: "unique_violation", details.field: "reference"). A payment and a subscription may share a reference: each kind has its own. A session holds its reference while it can be paid, and after that while a payment its customer sent still runs; one that expired unpaid (or that its merchant expired), or whose customer's last attempt failed and that is gone, hands it over. The recorded payments and subscriptions are checked at every creation, held or not. A payment or subscription confirmed on-chain is always recorded: should its reference be taken meanwhile (a session opened before references were held), it is recorded without one, and QBitFlow is alerted.
  • Inline product fields: productName (the name rule, 2–100), description (optional, text, 2–500).
  • reference, productReference and customerReference use the reference validator (1–100 characters: ASCII letters, digits and . _ : @ -). Anything else → 400 validation_failed on that field.

Fees — fees (payments and subscriptions)

A checkout may add amounts to its product's price: a tax, shipping, a service fee, and QBitFlow's processing fee when the merchant has the customer pay it. The customer sees them line by line and pays them with the price: once for a payment, with every bill for a subscription.

  • The body: fees.items (optional, at most 10 lines: label, the name rule on one line, 1–40 characters; description, optional, the text rule, at most 200; amountUsd, the usd validator: a JSON string holding an amount above 0, at most 1,000,000, with at most 2 decimals) and fees.processingFee (optional boolean). Anything else on a line (a JSON number for amountUsd included) → 400 validation_failed on its path (fees.items[0].amountUsd, fees.items[0].label…); 11 lines → 400 on fees.items.
  • The processing fee (processingFee: true, or omitted while the space's checkout.customerPaysProcessingFee setting is on; false turns it off for the checkout) is a last line {type: "processingFee", label: "Processing fee"} computed by QBitFlow on the price and the merchant's lines at the merchant's platform fee p (never below 0.5 %, the contracts' floor): base × p / (1 − p), rounded up to the cent. QBitFlow's fee is taken on everything charged, the processing fee included, so this gross-up is what leaves the merchant at least the price and their lines, as if no fee were taken ($100 + $20 VAT at 0.99 % → a $1.20 processing fee, $121.20 charged, $120.00 kept). The rate is the checkout's: the organization's fee when it is created (fees). The organization decides it for its members: a member's checkout (session or own key) may send fees.processingFee only when it equals the organization's checkout.customerPaysProcessingFee; a different value → 403 processing_fee_set_by_organization, details.field: "fees.processingFee". Omitting it follows the organization's value. The team, acting in a member's space too, is not checked. A marketplace's organization fee is not passed on: it is taken from that amount as on any payment.
  • The session (the hosted page's, and checkout.expired's) carries fees (the lines, in the order to show them: the merchant's, then the processing fee; [] without any) and amountUsd: the price plus every line, what the customer pays before the network fee. priceUsd stays the product's. The lines are fixed when the session is created (a later setting or fee change doesn't alter them).
  • What is charged is amountUsd: converted to the token at the intent (rounded down to its smallest unit), split by the contracts (QBitFlow's fee, the organization's, the referrer's are all taken on it), and recorded as the payment's amountUsd, with its priceUsd and fees (Payments and transactions). The network fee comes on top, quoted by the intent's breakdown.
  • Test mode: amountUsd, fees included, is capped at $5 as a price is → 400 validation_failed on fees, details.max: "5".
  • A subscription's lines are locked with its price. The subscription keeps them as fees (next to priceUsd, which stays the price), and every bill charges priceUsd plus the lines, as they were at signup: the first bill (paid at creation) and each next one, whatever the product's price or the merchant's platform fee becomes. The bill's amountUsd is that sum. The maximum per period and the allowance the subscriber signs are sized on it. A free trial names that amount in its message ("Then: … USD every …"), and its upgrade checkout keeps the trial's lines: the merchant's as they were, the processing fee computed again on the upgrade's price.
  • A product link's checkout (GET /pay/:uuid, a payment's or a subscription's) applies checkout.customerPaysProcessingFee; a qbf.cash tip never has fees.

Session status — GET /checkout-sessions/:uuid

Tells the merchant that created a session where it stands, so an integration can tell a session still waiting for its customer from an abandoned one. (It was GET …/:uuid/status.)

  • Response CheckoutSessionStatus: { uuid, status, txHash?, message?, lastAttempt?, expiresAt? }. uuid is the session's prefixed id, even when it was read by its bare uuid. status is one of four: created (open, waiting for the customer), waitingConfirmation (a transaction was sent), and the two final ones, completed and expired. txHash is set once the customer's transaction is sent (a completed subscription has none). message may explain an expiry. expiresAt is present while the session is open (when it can no longer be paid), and on the answer of POST /checkout-sessions/:uuid/expire (when it was expired); absent once the session is gone (completed, or expired at its time).
  • A failed attempt is not a status. When the customer's transaction fails (funds, signature, network…), the session goes back to created with lastAttempt { status: "failed", code, message?, txHash?, at }: the error code (errors, e.g. insufficient_funds, token_not_approved, tx_not_sent), the reason, the attempt's transaction if it was sent, and when it failed. A transaction sent but not final in time is not a failed attempt (it may still land): the session stays waitingConfirmation while QBitFlow follows it, and can't be expired meanwhile (409) (Payments and transactions § Transaction status). While the session hasn't expired, the customer can try again from the same checkout; the next attempt replaces lastAttempt (waitingConfirmation, then completed, or another failed attempt). Never cancel an order on lastAttempt: fulfil on payment.completed / subscription.created (or completed here), give up on checkout.expired (or expired here). A session whose last attempt failed can be expired (below).
  • A free trial's checkout sends no transaction: its subscription is created (and subscription.created sent, its status: "trial") when the customer starts it, while the session still answers created; once the session is gone it answers completed.
  • Only the session's space sees it (spaces): the organization's sessions from its space, a member's from theirs, in the session's mode. Anything else → 404, never 403.
  • A completed session keeps answering completed after it leaves the session store (a few minutes after completion): the recorded payment or subscription answers instead, with the same ownership rules.
  • A session that expired unpaid, or that its merchant expired, answers expired (from its expiry: checkout.expired was sent) for as long as QBitFlow keeps its expiry's record (the workflow history's retention, days after the expiry), then 404. An unknown id, another owner's session, and a product link's visit never engaged are 404.
  • :uuid: a prefixed session id (pay@…, sub@…) or its bare uuid (txuuid): malformed → 400 validation_failed on uuid. Another kind of id (refund@…) → 404.

Expiring a session — POST /checkout-sessions/:uuid/expire

Ends a session the caller created before its expiresAt (an order cancelled, a cart changed), so its customer can no longer pay it. No body.

  • Success is 200 with the CheckoutSessionStatus { uuid, status: "expired", message, expiresAt } (expiresAt: when it was expired). From then on its intent and its send answer 404 (as an expired session), GET /hosted/checkout-sessions/:uuid answers 404, GET /checkout-sessions/:uuid answers expired, and checkout.expired is sent once, its data's expiresAt being when it was expired. Its reference is free again.
  • Expiring an expired session (by its time or by its merchant) answers 200 again, with status: "expired": idempotent. Only the first answer carries expiresAt (when it was expired); a later one, the session being gone, doesn't.
  • A session whose customer paid it, or sent a payment that still runs, or started its free trial, can't be expired: 409 tx_already_sent. A payment that failed doesn't prevent it. A payment sent in the instant before the expiry is waited for: if it completes, the answer is 409 and the payment is recorded (payment.completed).
  • A product link's visit never engaged (no intent) can be expired too.
  • Only the session's space may expire it: another space's or organization's session, or an unknown id → 404. :uuid malformed → 400 validation_failed on uuid. A GET → 405 method_not_allowed (Allow: POST).

The codes named here

  • 400validation_failed

    path/query/body failed binding or validation, or a business rule about one input (a field named in errors)

  • 403policy_disabled

    a member doing what their organization's policy doesn't allow them, reads of what the policy governs included

  • 403processing_fee_set_by_organization

    a member's checkout whose fees.processingFee differs from their organization's checkout.customerPaysProcessingFee (Checkout sessions § Fees)

  • 404not_found

    unknown or out-of-scope resource; also any route hidden by a guard

  • 409unique_violation

    a unique value already exists (e.g. a duplicate reference or email)

  • 409tx_already_sent

    a transaction sent already: running, or completed (Payments and transactions § Sending a signed transaction)

  • 410merchant_closed

    a checkout of a removed member's or a closed organization's space: it takes no payment (Checkout sessions)

  • 409merchant_not_ready

    opening a checkout nobody could pay: the space's wallets (the organization's, while it holds the member's funds) accept no currency, or no token for a subscription; a subscription for a member whose funds the organization holds (fundsHeld: one-time payments only until trusted) (Checkout sessions); at a payer's intent (POST /hosted/payments/intent, POST /hosted/subscriptions/intent), a member's checkout whose organization takes a fee on it and has no wallet on the paid currency's chain anymore (organizationFeeNoWallet: a guard, the fee never dropped from a new payment; removing the organization's wallet of a chain already removes that chain's currencies from its members, so an open checkout meets currency_not_accepted first; a subscription signed before keeps billing without the fee, Members and held funds)

  • 405method_not_allowed

    the path exists, not with this method; the Allow header lists its methods. Answered before any credential is read, like the 404 of a path no route has

  • 422insufficient_funds

    doesn't hold enough of the token (or SOL, for a native payment): top up

  • 422token_not_approved

    hasn't approved Permit2 for the token (EVM): approve it, then retry