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:

List refunds

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";}
Read a refund

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 Refund
const 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

WhatTest modeLive
PricesAt most $5: a product's, an inline price, and a checkout's amount with its feesNo cap
Paid withFaucet tokens, on test networksReal tokens, on the mainnets
Subscription frequencyAt least 5 minutesAt least 1 hour, and a price above $0.25
Subscription billsOnly when you run one (the test bill)On their due date, by QBitFlow
Redirect URLshttp:// or https://https:// only
Customer emailsNever sentSent
Webhook retries3 retries, 5 minutes, 30 minutes and 2 hours after the failed attempt1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours after the failed attempt, then every 12 hours, for 3 days
A failing endpointNever turned off, no emailIts 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 live

    a test key never sees or creates live data

  • the receiving wallets on the mainnets, per chain (dashboard: POST /wallets, POST /token-wallets), and the currencies they accept, before any checkout (a space whose wallets accept nothing opens none: 409 merchant_not_ready; check with 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 /invitations with a live key); their fee and trust are per mode

    a member of test only → 404 in live

  • set in live (PUT /settings/:key with a live credential); the dashboard can apply one change to both modes

    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.