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/
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.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 checkoutcheckout-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";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 feeBody
referencestringOptionalOptional 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=100productUuidstringOptionalUser must provide Optional: the product being purchased, one of the request's space's (unknown or another space's: 404 not_found)
productReferencestringOptionalOptional: 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=100productNamestringOptionalAn inline product's name (with priceUsd, without productUuid or productReference)
namemin=2max=100descriptionstringOptionalAn inline product's description, optional
textmin=2max=500priceUsdnumberOptionalAn 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=1000000successUrlstringOptionalOptional: 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=2048cancelUrlstringOptionalOptional: 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=2048customerUuidstringOptionalOptional: one of the space's customers (unknown or another space's: 404 not_found). The checkout then asks the customer nothing
customerReferencestringOptionalOptional: 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=100expiresInMinutesintegerOptionalOptional: 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=1440metadatamapOptionalOptional: 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
metadatafeesCheckoutFeesDtoOptionalOptional: 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
CheckoutFeesDtoprocessingFeebooleanOptionalOptional: 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 FeeItemDtoOptionalOptional: up to 10 lines of your own, shown in this order before the processing fee
max=103 fields of each
FeeItemDtolabelstringRequiredThe line's name, as the checkout shows it (one line, at most 40 characters): "VAT (20%)", "Shipping"
namemax=40descriptionstringOptionalOptional: more about the line, at most 200 characters
textmax=200amountUsdnumberRequiredThe 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.
linkstringThe checkout page for the customer: <merchant's checkout base URL>/checkout?uuid=<uuid>
uuidstringThe session's prefixed id (pay@…, sub@…), also the uuid of the payment or subscription it creates
expiresAtstringWhen the checkout can no longer be paid (then checkout.expired, if it wasn't paid)
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.differs from their organization'sprocessingFee checkout.(Checkout sessions § Fees)customerPaysProcessingFee - 409idempotency_
key_ in_ use the
Idempotency-Key's first request is still running (idempotency): retry once it answered - 422idempotency_
key_ reused the
Idempotency-Keywas used for another request (idempotency): send a new key - 413request_
too_ large an
Idempotentroute's body over 1 MiB (idempotency)
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Create subscription checkout session#
POST /checkout-sessions/
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.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 signedsubscriptions/main.go
// The Subscription exists, with the session's id (sub@…), once the customer signed: act on// the subscription.created webhook.session, err := client. CheckoutSessions. CreateSubscription( ctx, &qbitflow. CreateSubscriptionSessionParams{ ProductName: "T-shirt", Description: "Blue, size M", PriceUSD: "4.99", // USD per period, a decimal string Frequency: &qbitflow.Duration{Value: 1, Unit: qbitflow.DurationUnitMonths}, TrialPeriod: &qbitflow.Duration{Value: 7, Unit: qbitflow.DurationUnitDays}, Reference: "order-1043", SuccessURL: "https://shop.example.com/orders/success?uuid=" + qbitflow.PlaceholderUUID, CancelURL: "https://shop.example.com/orders/cancel",})if err != nil { log.Fatal(err)}fmt.Println("Redirect the customer to", session.Link)$session = $client->checkoutSessions->createSubscription( new \QBitFlow\Params\CreateSubscriptionSessionParams( productName: 'T-shirt', description: 'Blue, size M', priceUsd: '4.99', // USD per period, a decimal string reference: 'order-1043', frequency: \QBitFlow\Models\Duration::months( 1), trialPeriod: \QBitFlow\Models\Duration::days( 7), // the first bill comes after the trial successUrl: 'https://shop.example.com/orders/success?uuid=' . \QBitFlow\Placeholders::UUID, cancelUrl: 'https://shop.example.com/orders/cancel',));echo "Subscribe at {$session->link}\n";Body
referencestringOptionalOptional 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=100productUuidstringOptionalUser must provide Optional: the product being purchased, one of the request's space's (unknown or another space's: 404 not_found)
productReferencestringOptionalOptional: 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=100productNamestringOptionalAn inline product's name (with priceUsd, without productUuid or productReference)
namemin=2max=100descriptionstringOptionalAn inline product's description, optional
textmin=2max=500priceUsdnumberOptionalAn 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=1000000successUrlstringOptionalOptional: 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=2048cancelUrlstringOptionalOptional: 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=2048customerUuidstringOptionalOptional: one of the space's customers (unknown or another space's: 404 not_found). The checkout then asks the customer nothing
customerReferencestringOptionalOptional: 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=100expiresInMinutesintegerOptionalOptional: 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=1440metadatamapOptionalOptional: 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
metadatafrequencyDurationOptionalHow often it is billed: at least 1 hour (5 minutes in test mode), at most 1 year
2 fields of
DurationvalueintegerOptionalNumber of units; 0 means no duration
unitstringOptionalRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
trialPeriodDurationOptionalA free trial before the first bill ({"value": 0}: none, removing a product's)
2 fields of
DurationvalueintegerOptionalNumber of units; 0 means no duration
unitstringOptionalRequired when value > 0. "months" = 30 days, "years" = 365 days
oneof=seconds minutes hours days weeks months years
minPeriodsintegerOptionalThe periods the customer commits to before they can cancel (0: none, removing a product's), at most 1000
max=1000feesCheckoutFeesDtoOptionalOptional: 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
CheckoutFeesDtoprocessingFeebooleanOptionalOptional: 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 FeeItemDtoOptionalOptional: up to 10 lines of your own, shown in this order before the processing fee
max=103 fields of each
FeeItemDtolabelstringRequiredThe line's name, as the checkout shows it (one line, at most 40 characters): "VAT (20%)", "Shipping"
namemax=40descriptionstringOptionalOptional: more about the line, at most 200 characters
textmax=200amountUsdnumberRequiredThe 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.
linkstringThe checkout page for the customer: <merchant's checkout base URL>/checkout?uuid=<uuid>
uuidstringThe session's prefixed id (pay@…, sub@…), also the uuid of the payment or subscription it creates
expiresAtstringWhen the checkout can no longer be paid (then checkout.expired, if it wasn't paid)
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.differs from their organization'sprocessingFee checkout.(Checkout sessions § Fees)customerPaysProcessingFee - 409idempotency_
key_ in_ use the
Idempotency-Key's first request is still running (idempotency): retry once it answered - 422idempotency_
key_ reused the
Idempotency-Keywas used for another request (idempotency): send a new key - 413request_
too_ large an
Idempotentroute's body over 1 MiB (idempotency)
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Get checkout session#
GET /checkout-sessions/
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.sh
curl "https://api.qbitflow.app/v2/checkout-sessions/$SESSION_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a CheckoutSessionStatusconst 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",};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
uuidstringRequiredThe session's prefixed id (pay@…, sub@…), or its bare uuid
txuuid
Response
A CheckoutSessionStatus.
uuidstringThe session's prefixed id (pay@…, sub@…), also the id of the payment or subscription it creates
statusstringcreated: 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 recordcompletedConfirmed and recorded. FinalexpiredThe checkout expired unpaid (at its expiresAt, or expired by its merchant). Final
txHashstringOptionalHash of the customer's transaction, once sent (not kept for completed subscriptions)
messagestringOptionalOptional detail, e.g. why the session expired
lastAttemptAttemptOptionalThe 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
Attemptstatusstringfailed
One of:
failedThe attempt's transaction wasn't sent, didn't confirm or failed on the network: nothing was paid
codestringOptionalThe error code (errors): what to do about it (insufficient_funds, token_not_approved, tx_not_sent…)
messagestringOptionalThe reason, for the customer
txHashstringOptionalThe attempt's transaction, if it was sent
atstringWhen it failed
expiresAtstringOptionalWhen 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
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Expire checkout session#
POST /checkout-sessions/
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.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 itPath parameters
uuidstringRequiredThe session's prefixed id (pay@…, sub@…), or its bare uuid
txuuid
Response
A CheckoutSessionStatus.
uuidstringThe session's prefixed id (pay@…, sub@…), also the id of the payment or subscription it creates
statusstringcreated: 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 recordcompletedConfirmed and recorded. FinalexpiredThe checkout expired unpaid (at its expiresAt, or expired by its merchant). Final
txHashstringOptionalHash of the customer's transaction, once sent (not kept for completed subscriptions)
messagestringOptionalOptional detail, e.g. why the session expired
lastAttemptAttemptOptionalThe 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
Attemptstatusstringfailed
One of:
failedThe attempt's transaction wasn't sent, didn't confirm or failed on the network: nothing was paid
codestringOptionalThe error code (errors): what to do about it (insufficient_funds, token_not_approved, tx_not_sent…)
messagestringOptionalThe reason, for the customer
txHashstringOptionalThe attempt's transaction, if it was sent
atstringWhen it failed
expiresAtstringOptionalWhen 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
- 409tx_
already_ sent a transaction sent already: running, or completed (Payments and transactions § Sending a signed transaction)
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Edge cases
The contract's rules for these routes: Authentication & roles · Creating a session · Fees — fees (payments and subscriptions) · Session status — GET / · Expiring a session — POST /.
Read all 5
Authentication & roles
- A member creates subscription checkouts while the organization's
members.subscriptionspolicy allows it (on by default; off →403policy_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.productspolicy allows it (on by default). Off →403policy_disabled,details.policy: "members.products"(checked beforemembers.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/:uuidandPOST /checkout-sessions/:uuid/expireare refused the same way (403policy_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:uuidroute; a malformed value →400validation_failedonuuid.
Creating a session
Idempotency-Key(idempotency): both creates take one. A create retried with the same key (a timeout, a5xx) answers the firstCreatedCheckoutSessionagain (the sameuuid,linkandexpiresAt,Idempotent-Replayed: true) for 24 hours, and opens no second session (so no secondcheckout.expiredlater for an order paid on the first). SDKs send one on every create.- Success is HTTP
201with aCreatedCheckoutSession{ link, uuid, expiresAt }(all three always present). Theuuidispay@-prefixed for a payment session andsub@-prefixed for a subscription session.expiresAtis when the checkout can no longer be paid. If it wasn't paid by then,checkout.expiredis sent (Webhook events) and its status becomesexpired. 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
expiresAttells it), orexpiresInMinutes(optional, an integer from 10 to 1440, and never longer than the default lifetime:400validation_failedonexpiresInMinutesotherwise, its message giving the maximum). The merchant can end it earlier withPOST /checkout-sessions/:uuid/expire. - A checkout nobody could pay is refused:
409merchant_not_ready, before anything is written (no reference held, no product or customer created),details.reasonsaying 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:
410merchant_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 nocheckout.expired. - Product (payment & subscription): the request either names an existing product (
productUuidorproductReference) or gives an inline product (productName+priceUsd,descriptionoptional). Neither →400validation_failedon each ofproductNameandpriceUsdthat 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.
priceUsdis a JSON string holding an amount in USD ("12.50","1000"): above 0, at most 1,000,000, at most 2 decimals (theusdvalidator). A JSON number, a negative amount, a third decimal or an exponent →400validation_failedonpriceUsd. Responses write it back canonical ("12.5"). (It wasprice, 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
priceUsdabove it →400validation_failedonpriceUsd,details.max: "5"; a product above it (one priced before the cap was enforced) → the same onproductUuidorproductReference, whichever named it. Checked before anything is written. Live mode has no cap. productUuidis the product'suuid; a value that is not a UUID →400validation_failedonproductUuid.- 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→404not_found. - An inline product of a payment is a snapshot: the session, and the payment it records, carry its
productName,descriptionand price, and noproductUuid(no product is created). An inline product of a subscription names a hidden (isActive=false) product of the space, which the subscription'sproductUuidnames: the one with the sameproductName,descriptionand 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 (400validation_failedonfees, below) has already created the hidden product. It stays hidden and unlisted (GET /productsshows it only withincludeHidden=true), holds no checkout, and the next checkout with the same inline product reuses it. - The two forms are mutually exclusive:
productUuidorproductReferencetogether with any ofproductName/description/priceUsd→400validation_failedonproductUuid(orproductReference, 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 ofA–Z a–z 0–9 _ - . :; a value at most 500 characters, without control or bidirectional characters. A broken limit →400validation_failedonmetadata(too many keys, a malformed key: the key is never echoed) ormetadata.<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, andpayment.completed) or the subscription (Subscription.metadata, editable withPATCH /subscriptions/:uuid, and everysubscription.*event).checkout.expiredcarries 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:
409merchant_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.
frequencyandtrialPeriodareDurations ({ "value": <uint32>, "unit": "seconds" | "minutes" | "hours" | "days" | "weeks" | "months" | "years" },months= 30 days,years= 365 days). Every rule below answers400validation_failedwith the field path indetails.errors[].field:unitmust be one of the values above, and is required whenevervalue > 0(frequency.unit,trialPeriod.unit).- A subscription product's terms are the defaults: with a
productUuid(orproductReference) of a subscription product,frequency,trialPeriodandminPeriodsare all optional, and each one sent overrides the product's, the others kept.trialPeriod: { "value": 0 }andminPeriods: 0remove 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, withoutfrequencyin the request), a zero value and a duration too long to count in seconds all fail onfrequency. For a one-time product, the message names it: "Product <reference> has no subscription terms: pass frequency, or use a subscription product".trialPeriodis optional. Omitted (without a product's), or{ "value": 0 }(with or without a unit), means no trial. A trial longer than ~136 years →trialPeriod.minPeriodsis optional (0 to 1000).- A live subscription's price (inline, or its product's) must be above $0.25 → on
priceUsd(orproductUuid/productReference),details.min.
- The customer.
customerUuidandcustomerReferenceare optional. Give either, and the checkout asks the customer nothing (asksCustomerDetails: false, whatevercheckout.customerDetailssays; details sent at the intent are ignored):customerUuidmust be one of the space's customers (in the session's mode); otherwise →404.customerReferenceis 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'scustomerUuid; 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 ascustomerReference, whichpayment.completed,subscription.createdand 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 (theemails.customerReceiptssetting still applies).- Neither: the checkout collects what
checkout.customerDetailsasks (below).
successUrlandcancelUrlare optional (http_url, else400); when absent, the merchant'scheckout.defaultssetting gives them. In live mode they must behttps(the customer's browser goes there from the checkout): anhttp://URL →400validation_failedonsuccessUrl/cancelUrl, the setting's included (its message says so: change the setting, or give the URL). Test mode takeshttp(a local shop).- A member gives a checkout's
successUrlandcancelUrlwhile their organization'smembers.checkoutRedirectspolicy 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 →403policy_disabledwithdetails.policy: "members.checkoutRedirects"anddetails.field(successUrlfirst when both are sent), nothing created: their checkouts go where thecheckout.defaultssetting says (the organization's). The team gives them for a member whatever the policy (switched into their space, or an organization key withOn-Behalf-Of), and a qbf.cash handle gives its own (a person, with no organization above it). - Redirect placeholders. In
successUrlandcancelUrl(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.expiredcarries them assuccessUrlandcancelUrl. After a payment the customer first lands on QBitFlow's page for it (<checkout base>/manage?uuid=…), which then redirects to yoursuccessUrl;cancelUrlis used as is. A redirect proves nothing (anyone can open the URL): fulfil on the webhook (payment.completed,subscription.created) orGET /checkout-sessions/:uuid, never on the redirect alone. - A session the merchant didn't name a customer for (no
customerUuidorcustomerReference) records what the checkout asks:customerDetailsis the merchant'scheckout.customerDetailssetting (email, orfull, the default: first name, last name and email; absent when the setting isnone), andasksCustomerDetailstells whether the intent must send them (truewhencustomerDetailsis 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. Withnone, nothing the checkout sends is kept. referenceis 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 → otherwise409(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(thenamerule, 2–100),description(optional,text, 2–500). reference,productReferenceandcustomerReferenceuse thereferencevalidator (1–100 characters: ASCII letters, digits and. _ : @ -). Anything else →400validation_failedon 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, thenamerule on one line, 1–40 characters;description, optional, thetextrule, at most 200;amountUsd, theusdvalidator: a JSON string holding an amount above 0, at most 1,000,000, with at most 2 decimals) andfees.processingFee(optional boolean). Anything else on a line (a JSON number foramountUsdincluded) →400validation_failedon its path (fees.items[0].amountUsd,fees.items[0].label…); 11 lines →400onfees.items. - The processing fee (
processingFee: true, or omitted while the space'scheckout.customerPaysProcessingFeesetting is on;falseturns 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 feep(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 sendfees.processingFeeonly when it equals the organization'scheckout.customerPaysProcessingFee; a different value →403processing_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) carriesfees(the lines, in the order to show them: the merchant's, then the processing fee;[]without any) andamountUsd: the price plus every line, what the customer pays before the network fee.priceUsdstays 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'samountUsd, with itspriceUsdandfees(Payments and transactions). The network fee comes on top, quoted by the intent'sbreakdown. - Test mode:
amountUsd, fees included, is capped at $5 as a price is →400validation_failedonfees,details.max: "5". - A subscription's lines are locked with its price. The subscription keeps them as
fees(next topriceUsd, which stays the price), and every bill chargespriceUsdplus 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'samountUsdis 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) appliescheckout.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? }.uuidis the session's prefixed id, even when it was read by its bare uuid.statusis one of four:created(open, waiting for the customer),waitingConfirmation(a transaction was sent), and the two final ones,completedandexpired.txHashis set once the customer's transaction is sent (a completed subscription has none).messagemay explain an expiry.expiresAtis present while the session is open (when it can no longer be paid), and on the answer ofPOST /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
createdwithlastAttempt{ status: "failed", code, message?, txHash?, at }: the errorcode(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 stayswaitingConfirmationwhile 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 replaceslastAttempt(waitingConfirmation, thencompleted, or another failed attempt). Never cancel an order onlastAttempt: fulfil onpayment.completed/subscription.created(orcompletedhere), give up oncheckout.expired(orexpiredhere). A session whose last attempt failed can be expired (below). - A free trial's checkout sends no transaction: its subscription is created (and
subscription.createdsent, itsstatus: "trial") when the customer starts it, while the session still answerscreated; once the session is gone it answerscompleted. - 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, never403. - A completed session keeps answering
completedafter 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.expiredwas sent) for as long as QBitFlow keeps its expiry's record (the workflow history's retention, days after the expiry), then404. An unknown id, another owner's session, and a product link's visit never engaged are404. :uuid: a prefixed session id (pay@…,sub@…) or its bare uuid (txuuid): malformed →400validation_failedonuuid. 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
200with theCheckoutSessionStatus{ uuid, status: "expired", message, expiresAt }(expiresAt: when it was expired). From then on its intent and its send answer404(as an expired session),GET /hosted/checkout-sessions/:uuidanswers404,GET /checkout-sessions/:uuidanswersexpired, andcheckout.expiredis sent once, itsdata'sexpiresAtbeing when it was expired. Its reference is free again. - Expiring an expired session (by its time or by its merchant) answers
200again, withstatus: "expired": idempotent. Only the first answer carriesexpiresAt(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:
409tx_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 is409and 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.:uuidmalformed →400validation_failedonuuid. AGET→405method_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.differs from their organization'sprocessingFee checkout.(Checkout sessions § Fees)customerPaysProcessingFee - 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
referenceoremail) - 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 /), a member's checkout whose organization takes a fee on it and has no wallet on the paid currency's chain anymore (hosted/ subscriptions/ intent 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 meetscurrency_first; a subscription signed before keeps billing without the fee, Members and held funds)not_ accepted - 405method_
not_ allowed the path exists, not with this method; the
Allowheader lists its methods. Answered before any credential is read, like the404of 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