API reference

Errors

One body for every failure. Branch on its code; here is every code a key can meet, and what to do.

The 51 codes an API key can meet, from the API's error catalog and its SDK surface. Each endpoint lists the ones it can answer and links here.

The error body

An error body

{  "error": "Invalid request",  "code": "validation_failed",  "details": {    "errors": [{"field":"name","message":"name must be at least 2 characters"}]  },  "requestId": "0b9f6f3e-7c1a-4d8e-9a51-2f4c8e7d1a90"}
  1. error: A sentence, safe to show to a person. Never branch on it.
  2. code: The stable identifier to branch on, always present, with the HTTP status.
  3. details: Structured data for the codes that have some (below). Fields are named as you sent them (frequency.unit when nested), from the body, path or query, every failing one at once.
  4. requestId: The request's id, also the X-Request-Id header. Log it; quote it to support.
Handle errors

errors-handling.sh

# A failure answers its HTTP status and { error, code, details, requestId }: branch on the status# and the code, quote requestId to support. An order nobody paid yet is not an error here: the# page has no item.curl "https://api.qbitflow.app/v2/payments?reference=order-1042" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of Payment ({ items, hasMore }), items empty when nobody paid that order
try {	const payment = await client.payments.getByReference('order-1042');	console.log(`order-1042 paid: ${payment.amountUsd} USD`);} catch (err) {	if (err instanceof NotFoundError) {		console.log('no payment for order-1042 yet');	} else if (err instanceof ValidationError) {		// Refused before sending (status undefined) or by the API (400).		for (const f of err.fieldErrors) console.log(`${f.field}: ${f.message}`);	} else if (err instanceof ApiError) {		// Branch on status and code, never on the message. Quote requestId to support.		console.log(`QBitFlow error ${err.status} ${err.code} (request ${err.requestId})`);		if (isRetryable(err)) console.log('transient: try again later');	} else {		throw err;	}}
try:    payment = client.payments.get_by_reference("order-1042")    print("paid:", payment.uuid)except qbitflow.ValidationError as exc:  # status None: refused before sending    for field_error in exc.field_errors:        print(f"{field_error.field}: {field_error.message}")except qbitflow.NotFoundError:    print("no payment for order-1042 yet")except qbitflow.ApiError as exc:  # the base of every error the SDK raises    print(f"QBitFlow error {exc.status} {exc.code} (request {exc.request_id})")    if qbitflow.is_retryable(exc):  # network, 5xx, 429: worth trying again later        print("transient: try again later")
payment, err := client.Payments.GetByReference(ctx, "order-1042")var (	validation *qbitflow.ValidationError	notFound   *qbitflow.NotFoundError	apiErr     *qbitflow.APIError)switch {case err == nil:	fmt.Println("order-1042 paid by", payment.UUID)case errors.As(err, &validation):	// StatusCode 0: refused before sending; 400: refused by the API.	for _, f := range validation.FieldErrors {		fmt.Printf("%s: %s\n", f.Field, f.Message)	}case errors.As(err, &notFound):	fmt.Println("no payment for order-1042")case errors.As(err, &apiErr):	// Every SDK error embeds an *APIError: branch on Code, quote RequestID to support.	fmt.Printf("QBitFlow error %d %s (request %s), retryable: %v\n",		apiErr.StatusCode, apiErr.Code, apiErr.RequestID, qbitflow.IsRetryable(err))default:	log.Fatal(err)}
try {	$payment = $client->payments->getByReference('order-1042');	echo "order-1042 was paid by {$payment->uuid}\n";} catch (\QBitFlow\Exceptions\ValidationException $e) {	foreach ($e->fieldErrors as $fieldError) { // refused before sending (status 0), or a 400		echo "{$fieldError->field}: {$fieldError->message}\n";	}} catch (\QBitFlow\Exceptions\NotFoundException) {	echo "No payment for order-1042 yet\n";} catch (\QBitFlow\Exceptions\ApiException $e) {	// Branch on apiCode, never on the message; quote the request id to support.	printf("QBitFlow error %d %s (request %s), retryable: %s\n",		$e->status, $e->apiCode, $e->requestId, $e->isRetryable() ? 'yes' : 'no');}

Common codes

Any route may answer these, as its guards and inputs allow.

400 · Bad request

  • validation_failedHTTP 400

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

    What to do: Fix the inputs details.errors lists: each field is the name you sent (dotted when nested), and every failing input is reported at once.

    details: errors: [{ field, message, reason? }]; with some, max / min (decimal strings for money)

  • foreign_key_violationHTTP 400

    a referenced resource does not exist

    What to do: details.field names the reference that doesn't exist: check that id.

    details: field

  • invalid_signatureHTTP 400

    POST /webhooks/verify: the signature doesn't match, or is older than 5 minutes; a send route: the transaction isn't the payer's signature over what the intent prepared (Payments and transactions § Sending a signed transaction)

  • test_mode_onlyHTTP 400

    something only test mode does asked in live mode (a test bill, a faucet, a test payment)

  • sender_is_receiverHTTP 400

    a transfer whose paying wallet is the wallet it pays (a payment, a subscription, a refund, a release)

401 · Unauthorized

  • unauthorizedHTTP 401

    missing / invalid JWT or API key, an ended login

    What to do: Check the key: a missing, unknown, deleted or expired key answers this.

403 · Forbidden

  • forbiddenHTTP 403

    authenticated but not allowed (role, peer resource)

    What to do: The key can't do this: the route needs an admin key, the On-Behalf-Of value isn't a member this key may act for, or a key wrote a members.* policy (policies change in the dashboard).

  • policy_disabledHTTP 403

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

    What to do: A member's key, while the organization's policy details.policy is off for members: the organization turns it on in its settings. details.field names the input it refuses (a checkout's successUrl or cancelUrl, members.checkoutRedirects): send it without. Organization keys never get it.

    details: policy; field when the policy governs a request field (members.checkoutRedirects, on by default: a member's successUrl / cancelUrl, routes/checkout-sessions.md)

  • processing_fee_set_by_organizationHTTP 403

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

    details: field (fees.processingFee)

404 · Not found

  • not_foundHTTP 404

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

    What to do: Check the id and the key's mode (test or live): a resource of another space answers the same, so existence is never revealed.

405

  • method_not_allowedHTTP 405

    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

409 · Conflict

  • unique_violationHTTP 409

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

    What to do: details.field names the value already used in this space (the value itself is never echoed): pick another one.

    details: field (never the value)

  • conflictHTTP 409

    a state conflict no more specific code covers

  • tx_already_sentHTTP 409

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

  • held_funds_pendingHTTP 409

    removing a member (DELETE /members/:userUuid, DELETE /team/:userUuid for an admin who kept a personal space), or closing an organization, while the organization holds a member's live funds: release them first (Members and held funds); deleting the organization's live wallet of a chain (DELETE /wallets/:uuid) while it holds members' live funds in that chain's currencies, which their release is paid from (Wallets)

    details: DELETE /wallets/:uuid: heldEntries (an integer: the held lines on that chain not settled yet)

  • member_has_no_walletHTTP 409

    trusting a member (POST /members/:userUuid/trust) whose own wallets accept none of the organization's currencies: trusted, nobody could pay them (their checkouts would all answer merchant_not_ready), and trust can't be undone. Trust them once they've added one (Members and held funds)

  • merchant_not_readyHTTP 409

    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)

    details: reason: noWallet, organizationNoWallet, noTokenWallet, fundsHeld, organizationFeeNoWallet

  • idempotency_key_in_useHTTP 409

    the Idempotency-Key's first request is still running (idempotency): retry once it answered

    What to do: Retry once the first request has answered, with the same key.

410 · Gone

  • merchant_closedHTTP 410

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

413 · Content too large

  • request_too_largeHTTP 413

    an Idempotent route's body over 1 MiB (idempotency)

422 · Unprocessable content

  • idempotency_key_reusedHTTP 422

    the Idempotency-Key was used for another request (idempotency): send a new key

    What to do: Send a new key: one key names one operation.

429 · Too many requests

  • rate_limit_exceededHTTP 429

    rate limit hit (see below)

    What to do: Wait details.retryAfterSeconds (also sent as the Retry-After header), then send the same request again, with the same Idempotency-Key: the refused one did nothing.

    details: limit, periodSeconds, retryAfterSeconds; limitedBy (and what it counts) on a route with several limits

500 · Internal server error

  • internalHTTP 500

    server error

    What to do: Retry a read or an idempotent create; quote requestId when asking support.

Codes of particular areas

409 · Conflict

  • already_joinedHTTP 409

    inviting someone already in the organization with that role and mode (Invitations)

  • subscription_already_stopped_or_inactiveHTTP 409

    cancelling a subscription, raising its allowance or its maximum, once it is stopped (it ends with its paid period; customer-side routes), a merchant's POST /subscriptions/:uuid/cancel?immediate=false of a stopped one, or a merchant's POST /subscriptions/:uuid/cancel or /test-bill of a cancelled one

504 · Gateway timeout

  • timeoutHTTP 504

    the request ran past the server's deadline: retry (idempotent reads) or check the resource before retrying a write

    What to do: Retry a read; check the resource before retrying a write.

Chain refusals

What a network refuses (a revert, a program's error, a node's refusal) is decoded the same way on every chain. A key reads them as a checkout's failed attempt (lastAttempt.code on GET /checkout-sessions/:uuid) and as a failing bill's failureCode. details.reason names the source, for support only: never branch on it.

400 · Bad request

  • tx_mismatchHTTP 400

    The payer sent what QBitFlow didn't prepare, or too late (§ Sending a signed transaction). tx_expired also ends an EVM token payment whose network fee fell too far since its intent, and a Solana transaction whose blockhash expired before it landed: create the intent again

  • tx_expiredHTTP 400

    The payer sent what QBitFlow didn't prepare, or too late (§ Sending a signed transaction). tx_expired also ends an EVM token payment whose network fee fell too far since its intent, and a Solana transaction whose blockhash expired before it landed: create the intent again

  • tx_replacedHTTP 400

    The payer the wallet replaced the transaction with another one (a cancel, other values): it never ran (a speed-up with the same values is followed)

409 · Conflict

  • authorization_usedHTTP 409

    The payer signed something already used: prepare the transaction again

  • subscription_not_activeHTTP 409

    The payer asked for what the subscription's state refuses

  • already_existsHTTP 409

    The payer asked for what the subscription's state refuses

  • payment_not_dueHTTP 409

    The payer asked for what the subscription's state refuses

  • cannot_cancel_overdueHTTP 409

    The payer asked for what the subscription's state refuses

  • no_changeHTTP 409

    The payer asked for what the subscription's state refuses

  • payment_in_flightHTTP 409

    The payer sent a token payment or a subscription (both sent and paid for by QBitFlow's operator) while another one from the same wallet in the same token is still confirming, or failed under 2 minutes ago: retry in a couple of minutes. Every casing of an EVM address is the same wallet

422 · Unprocessable content

  • insufficient_fundsHTTP 422

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

  • insufficient_native_fundsHTTP 422

    The payer can't pay the network fee (ETH, SOL…): top up the network's coin

  • insufficient_allowanceHTTP 422

    The payer granted QBitFlow less than this amount: increase the allowance

  • allowance_expiredHTTP 422

    The payer granted an allowance that expired: approve again

  • token_not_approvedHTTP 422

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

  • signature_expiredHTTP 422

    The payer signed too long ago: sign again (a free trial's message: ask a new intent)

  • max_amount_exceededHTTP 422

    The payer a bill above the subscription's maximum per period (the price moved): approve a higher maximum

  • invalid_amountHTTP 422

    The payer asked for an amount outside the subscription's limits (allowance_too_low, Solana only: a top-up of fewer periods than were spent since the last one granted, which its program refuses until its upgrade; details.minPeriods, the fewest periods it takes)

  • allowance_too_lowHTTP 422

    The payer asked for an amount outside the subscription's limits (allowance_too_low, Solana only: a top-up of fewer periods than were spent since the last one granted, which its program refuses until its upgrade; details.minPeriods, the fewest periods it takes)

  • max_increase_exceededHTTP 422

    The payer asked for an amount outside the subscription's limits (allowance_too_low, Solana only: a top-up of fewer periods than were spent since the last one granted, which its program refuses until its upgrade; details.minPeriods, the fewest periods it takes)

  • max_below_current_cycleHTTP 422

    The payer asked for an amount outside the subscription's limits (allowance_too_low, Solana only: a top-up of fewer periods than were spent since the last one granted, which its program refuses until its upgrade; details.minPeriods, the fewest periods it takes)

  • unsupported_tokenHTTP 422

    The payer uses a token the network doesn't support

  • tx_failedHTTP 422

    The payer can't act: the transaction failed for a reason no decoder recognized

  • transaction_refusedHTTP 422

    The payer can't act: QBitFlow's side (reported, fixed by QBitFlow)

503 · Service unavailable

  • network_unavailableHTTP 503

    The payer can't act: QBitFlow can't send on the network for now, or has no fresh USD price for the currency (missing, older than 5 minutes, or a stablecoin off its peg by more than 5 %), retry later

Codes only the dashboard meets

The other codes answer the dashboard's own routes (sign-in, the team, re-authentication, wallet signing): an API key never reaches them.

The 46 codes only the dashboard meets

token_expiredsession_endedrefresh_conflictplan_requiredinvalid_confirmation_tokeninvalid_unsubscribe_linkclient_closed_requestblockchain_errorinvalid_credentialsno_organizationstaff_view_read_onlystaff_view_endedinvalid_oidc_codeinvalid_onboarding_tokenidentity_already_linkedidentity_in_uselast_sign_in_methodupstream_errorreauth_requiredinvalid_passwordreauth_method_unavailableinvalid_invitationinvitation_email_mismatchwebhook_endpoint_not_failingintent_requiredtrial_not_cancellablecancellation_too_earlytrial_already_usedsubscription_not_pausablesubscription_not_pausedbill_in_progresssignature_already_usedcurrency_not_acceptedrefunds_disabledrefund_window_expiredrefund_already_answeredrefund_decided_by_organizationnothing_to_releaseheld_funds_owedheld_funds_changedheld_funds_release_in_flightrefund_in_flightheld_funds_releasedrefund_already_existsemail_refusedwallet_has_active_subscriptions