Refunds in a marketplace, then go live
Refund a seller's sale and know who bears it. Then go live, with your sellers invited again.
Refund a seller's sale
A refund is sent from a dashboard, as for any payment
(Dashboard/Payments/a payment/Refund), signed by the wallet
that was paid: the seller's, from their dashboard, for a direct sale; yours, from your dashboard,
for a held one. The seller of a held sale can't decide it: they get
403 refund_decided_by_organization. Sellers also answer their customers' requests, while your
members.refunds policy allows it (off, their customers can't ask, except for a sale you hold). Your code reads them all, each naming its seller:
refunds-list.sh
# The refunds waiting for an answer.curl "https://api.qbitflow.app/v2/refunds?status=pending" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of Refund ({ items, hasMore, nextCursor? })// The active ones: pending, waiting for an answer (no status: every status).const page = await client.refunds.list({ status: [RefundStatus.Pending] });for (const refund of page.items) { const { uuid, txUuid, initiatedBy, reason, amount, currency } = refund; console.log(`${uuid} of ${txUuid} (${initiatedBy}): ${amount} ${currency.symbol}, ${reason}`);}# The ones waiting for an answer (status None: every status).page = client. refunds. list( status=[qbitflow. RefundStatus. PENDING])for refund in page.items: print(refund.uuid, refund.tx_uuid, refund.initiated_by, refund.reason, refund.amount)// The active ones: pending, waiting for an answer (Status empty: every status).page, err := client.Refunds.List(ctx, &qbitflow.RefundListParams{ Status: []qbitflow. RefundStatus{qbitflow. RefundStatusPending},})if err != nil { log.Fatal(err)}for _, r := range page.Items { fmt.Println(r.UUID, r.TxUUID, r.InitiatedBy, r.Reason, r.Amount, r.Currency.Symbol)}// The active ones: pending, waiting for an answer (no status: every status).$page = $client->refunds->list(new \QBitFlow\Params\RefundListParams( status: [\QBitFlow\Enums\RefundStatus::PENDING],));foreach ($page->items as $refund) { echo "{$refund->uuid} {$refund->txUuid} ({$refund->initiatedBy}): {$refund->amount} {$refund->currency->symbol}, " . ($refund->reason ?? '-') . "\n";}refunds-get.sh
# One refund, by its id (refund@…): its status, its share of the payment and its amount.curl "https://api.qbitflow.app/v2/refunds/$REFUND_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a Refundconst refund = await client.refunds.get(refundUuid); // refund@…// pending (a customer's request), approved or rejected.const { uuid, status, refundPercent, amount, currency } = refund;console.log(`${uuid}: ${status}, ${refundPercent}% = ${amount} ${currency.symbol}`);refund = client.refunds.get(refund_uuid) # refund@…# pending (a customer's request), approved or rejected.print(refund.uuid, refund.status, f"{refund.refund_percent}%", refund.amount)refund, err := client.Refunds.Get(ctx, refundUUID) // refund@…if err != nil { log.Fatal(err)}// pending (a customer's request), approved or rejected.fmt.Println(refund.UUID, refund.Status, refund.RefundPercent+"%", refund.Amount, refund.Currency.Symbol)$refund = $client->refunds->get( $refundUuid); // refund@…// pending (a customer's request), approved or rejected.echo "{$refund->uuid} {$refund->status} {$refund->refundPercent}% {$refund->amount} {$refund->currency->symbol}\n";Who bears it:
- The seller. A refund returns its share of everything paid. QBitFlow keeps its fee, you keep your commission.
- You, past their share. For a held sale, each refund is a negative line in what you owe. A refund above their share (a full one always is) costs you the rest.
- Released is final. A held sale you've released can't be refunded, nor asked for:
409 held_funds_released.
The paid wallet must hold the share when it's sent, or preparing
it answers 422 insufficient_funds (a token) or 422 insufficient_native_funds (a chain's coin):
top it up, then send it again
(as for any refund).
Your endpoint hears each one, with the seller's userUuid: refund.approved once the transfer
confirms, and for a customer's request, refund.requested first, or refund.rejected.
Check it
Refund half of a held test seller's sale from your dashboard, signing with your test wallet. Once
it confirms, refund.approved arrives and their held funds show a negative line.
Pay with test tokens
Get QBFT, QBitFlow's test token, from the QBFT faucet, which also lists each test network's coin. The dashboard's Dashboard/Tools sends QBFT too (to each wallet once every 6 hours per chain) and can run a test payment into your wallet. Then pay a test checkout the way your customers will (how to pay).
What test mode does differently
| What | Test mode | Live |
|---|---|---|
| Prices | At most $5: a product's, an inline price, and a checkout's amount with its fees | No cap |
| Paid with | Faucet tokens, on test networks | Real tokens, on the mainnets |
| Subscription frequency | At least 5 minutes | At least 1 hour, and a price above $0.25 |
| Subscription bills | Only when you run one (the test bill) | On their due date, by QBitFlow |
| Redirect URLs | http:// or https:// | https:// only |
| Customer emails | Never sent | Sent |
| Webhook retries | 3 retries, 5 minutes, 30 minutes and 2 hours after the failed attempt | 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours after the failed attempt, then every 12 hours, for 3 days |
| A failing endpoint | Never turned off, no email | Its team emailed after 24 hours, turned off after 3 days |
Go live
Live is a separate set of spaces: nothing carries over. Switch the dashboard to Live, then redo what your integration needs, row by row:
0 of 7 done
a live key (
sk_live_…), created in the dashboard switched to livea test key never sees or creates live data
the receiving wallets on the mainnets, per chain (dashboard:
POST /wallets,POST /), and the currencies they accept, before any checkout (a space whose wallets accept nothing opens none:token-wallets 409 merchant_; check withnot_ ready GET /)wallets/ accepted-currencies test wallets are for test networks; the organization fee also needs the organization's own wallet on each chain (
GET /,organization/ wallets routes/)members. md created again in live (new
uuids)references may be the same as in test (unique per space and mode)
created again in live, each with its own new secret
test endpoints only receive test events; store the live secret separately
invited again in live (
POST /with a live key); their fee and trust are per modeinvitations a member of test only →
404in liveset in live (
PUT /with a live credential); the dashboard can apply one change to both modessettings/ :key values are per space and mode
nothing to do
owners and admins work in both modes
Then swap your server's QBITFLOW_API_KEY and QBITFLOW_WEBHOOK_SECRET for the live ones. Keep the
two secrets apart: each endpoint signs with its own.
Live redirects are https:// only: a live checkout's successUrl and cancelUrl, and your live
checkout.defaults setting, which payment links and checkouts without URLs use. http:// answers
400 validation_failed: check the setting in live before your first payment.
Check it
Make one small live payment with real tokens. Its event (payment.completed, or
subscription.created) reaches your live endpoint in
Dashboard/Settings/Events (Live), and your server acts on
it.
Invite your sellers again after creating your live endpoint, so member.joined reaches it.