Concepts

Payments and their statuses

A payment exists once the chain confirms it. Until then, four statuses say where it stands.

  1. created → waitingConfirmation: sent
  2. waitingConfirmation → created: attempt failed
  3. waitingConfirmation → completed (final): confirmed
  4. created → expired (final): expires unpaid

The statuses

StatusMeansWhat to do
createdwaiting for the customer, or the last attempt failed (lastAttempt)wait
waitingConfirmationa transaction was sentwait: never offer to pay again
completedfinal: confirmed on-chain and recordedfulfil (payment.completed)
expiredfinal: unpaid at expiresAt, or you expired itrelease the order (checkout.expired)

Confirmations

A transaction completes at the network's confirmation depth (one block on test networks; in live, each chain's own), counting only QBitFlow's contract's events. No route creates a payment: the chain does, and the payment and payment.completed are recorded together.

A sent transaction that isn't final in time is never a failure: it may still land, so it stays waitingConfirmation, and a second can't be signed (409 tx_already_sent).

Failed attempts: lastAttempt

A failed attempt sends the checkout back to created with lastAttempt: its code, the message the customer saw, txHash when something was sent, and at.

Avoid

Cancel the order on lastAttempt

It ends an attempt, not the checkout: the customer can retry from the same link.

Do

Give up on expired

checkout.expired is the end. Until then, wait.
What failedCodes
Not enough in the walletinsufficient_funds, insufficient_native_funds (the coin, for gas)
Allowance, approvalinsufficient_allowance, allowance_expired, token_not_approved
The signatureinvalid_signature, signature_expired
On the chaintx_failed (reverted), tx_replaced, tx_expired, tx_not_sent
QBitFlow's sidenetwork_unavailable (try later), transaction_refused

Most are caught before signing (QBitFlow checks the balance first). No webhook announces a failure; the dashboard logs each for 25 months (GET /payment-failures).

The payment

Read one with GET /payments/:uuid (pay@… or the bare uuid). To find it by your reference, filter the list: GET /payments (reference=) answers 0 or 1 item.

Here is a payment read:

Get a payment

payments-get.sh

# A payment by its uuid (pay@…, the checkout session's id). By your order reference, the list's# ?reference= filter: 0 or 1 item (none until the customer paid).curl https://api.qbitflow.app/v2/payments/$PAYMENT_UUID \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a Payment#curl "https://api.qbitflow.app/v2/payments?reference=order-1042" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of Payment ({ items, hasMore, nextCursor? }), items empty until the customer paid
const payment = await client.payments.get(paymentUuid); // pay@…, the checkout session's idconsole.log(payment.uuid, payment.reference, payment.amountUsd, 'USD', payment.metadata); // Runs GET /payments?reference= and returns its one payment: a NotFoundError (404,// not_found) until the customer paid.const byReference = await client.payments.getByReference('order-1042');console.log(`order-1042 was paid by ${byReference.from}`);
payment = client.payments.get(payment_uuid)  # pay@…, the id of its checkout sessionprint(payment.reference, payment.amount_usd, "USD", payment.tx_hash, payment.metadata) # Runs GET /payments?reference= and returns its one payment (NotFoundError when none).by_reference = client.payments.get_by_reference("order-1042")  # your order idprint(by_reference.uuid)
payment, err := client.Payments.Get(ctx, paymentUUID, nil) // pay@…if err != nil {	log.Fatal(err)}fmt.Println(payment.UUID, payment.Reference, payment.AmountUSD, "USD", payment.Metadata) // Runs GET /payments?reference= and returns its one payment.byReference, err := client.Payments.GetByReference(ctx, "order-1042") // your order idif err != nil {	log.Fatal(err) // a *qbitflow.NotFoundError (404, not_found) when no payment has this reference}fmt.Println("order-1042 was paid by", byReference.UUID)
$payment = $client->payments->get($paymentUuid); // pay@…echo "{$payment->uuid}: {$payment->amountUsd} USD, tx {$payment->txHash}\n"; // Runs GET /payments?reference= and returns its one payment (a NotFoundException when none).$payment = $client->payments->getByReference('order-1042'); // your order idecho "order-1042 was paid by {$payment->uuid}\n";

List payments newest first with GET /payments. Subscription bills are in the combined feed (GET /transactions):

List payments and bills together

transactions-list.sh

# One page of the feed: one-time payments and subscription bills together, newest first, each# with its type. ?type=payment or ?type=bill keeps one kind.curl "https://api.qbitflow.app/v2/transactions?limit=10" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of Transaction ({ items, hasMore, nextCursor? })
// One feed of one-time payments and subscription bills, newest first; each row reads like its// resource. `type` filters it to one kind (TransactionKind.Payment or TransactionKind.Bill).const feed = await client.transactions.list({ limit: 20 });for (const tx of feed.items) {	console.log(tx.type, tx.uuid, tx.amountUsd, 'USD');}
# One feed of one-time payments and subscription bills, newest first; each row reads like# its resource. type= filters it to one kind (TransactionKind.PAYMENT or .BILL).feed = client.transactions.list(limit=20)for tx in feed.items:    print(tx.type, tx.uuid, tx.amount_usd, "USD")
// One feed of one-time payments and subscription bills, newest first; each row reads like// its resource. Type filters it to one kind (TransactionKindPayment or TransactionKindBill).feed, err := client.Transactions.List(ctx, &qbitflow.TransactionListParams{Limit: 20})if err != nil {	log.Fatal(err)}for _, tx := range feed.Items {	fmt.Println(tx.Type, tx.UUID, tx.AmountUSD, "USD")}
// One feed of one-time payments and subscription bills, newest first; each row reads like its// resource. `type` filters it to one kind (TransactionKind::PAYMENT or TransactionKind::BILL).$feed = $client->transactions->list(new \QBitFlow\Params\TransactionListParams(limit: 20));foreach ($feed->items as $transaction) {	echo "{$transaction->type} {$transaction->uuid} {$transaction->amountUsd} USD\n";}

Here is a payment as payment.completed delivers it:

payment.completed· the body your endpoint receives

{  "createdAt": "2026-10-01T12:00:00Z",  "data": {    "amount": "10",    "amountMinUnits": "10000000",    "amountUsd": "10",    "confirmedAt": "2026-10-01T11:59:58Z",    "explorerUrl": "https://basescan.org/tx/0x1111111111111111111111111111111111111111111111111111111111111111",    "metadata": {      "crm:customer": "C-5521",      "orderId": "order-1042"    },    "priceUsd": "10",    "reference": "order-1042",    "settlement": {      "feePercent": "0.99",      "split": {…},      "transaction": {…}    },    "txHash": "0x1111111111111111111111111111111111111111111111111111111111111111",    "uuid": "pay@01a0f755-f200-7000-8000-000000000001",    … 17 more fields  },  "id": "evt_82c1e15d-c0d9-584f-a65a-fb4099925273",  "test": false,  "type": "payment.completed",  "version": "v2"}
The contract's example (webhooks.md § Examples)
  1. data.amount: What the customer paid in the token, exact; amountMinUnits is the same in its smallest unit and amountUsd its value in USD (amounts).
  2. data.confirmedAt: When the chain confirmed it; createdAt is when QBitFlow recorded it.
  3. data.explorerUrl: Its transaction on the chain's explorer: link to it rather than building one.
  4. data.metadata: Your checkout's own data, returned as you sent it.
  5. data.reference: Your order id, from the checkout.
  6. data.settlement: How the contract split it, as paid (fees).
  7. data.uuid: The checkout's id, now the payment's, for life.

A payment read adds refund and refundable. productName and productDescription copy what was paid for: product edits never change them.