Fleeexdocs

Balance and payments

Read the wallet balance and handle 402 Payment Required with a signed top-up link.

Fleeex is prepaid, so two things matter to an integration: knowing the balance, and gracefully handling the moment it runs out.

Read the balance

const { balanceMicros, currency } = await client.getBalance();
// balanceMicros is EUR micro-units: 1_000_000 = €1.00

getBalance() returns a BalanceResponse. Amounts are in micro-units to avoid floating-point drift, so divide by 1,000,000 for the major unit.

const eur = balanceMicros / 1_000_000; // e.g. 4_250_000 → 4.25

Like a proxied call, getBalance() throws PaymentRequiredError if the (app, user) has no funded, authorized wallet.

Amounts are pre-tax

The wallet holds tax-exclusive credits: VAT is computed and collected on top at Checkout, and the balance is credited with the pre-tax amount. So a user who paid €12.00 sees a balance below that, and the difference is not missing money. If you display the figure, label it as AI credit rather than as an amount paid. Their receipts live in their Fleeex account.

It is the wallet's balance, not your app's

One balance funds every app the user connected, so balanceMicros can move without your app doing anything. What bounds your app's share is the monthly cap the user sets on it, which is why hitting it is its own 402 code, below.

Handle 402 Payment Required

When the wallet isn't funded or authorized, both the proxy and getBalance() raise a typed PaymentRequiredError, usually carrying a signed, short-lived top-up link. Send the end user there to fund the wallet:

import { PaymentRequiredError } from "@fleeex/sdk";
 
try {
  await client.chat.completions.create({ model, messages });
} catch (err) {
  if (err instanceof PaymentRequiredError) {
    if (err.topupUrl) redirect(err.topupUrl); // send the end user through Checkout
    else showSupportMessage(); // a suspended wallet has no link, see below
  } else {
    throw err;
  }
}

PaymentRequiredError exposes topupUrl, code, correlationId, and status (always 402). The message is intentionally generic, since Fleeex never reveals whether a wallet exists, so the actionable parts are code and topupUrl.

402 is three different situations

Branch on code. Sending someone to Checkout when paying can't help them is worse than saying nothing:

codeMeanstopupUrl
PAYMENT_REQUIREDNo authorized wallet, or not enough balance.always
APP_SPEND_CAP_EXCEEDEDYour app hit the monthly cap the user set on it. Their wallet may still be full, so the message to show is "raise the cap", not "add funds".usually
WALLET_SUSPENDEDThe wallet is blocked after a refund or chargeback clawed money back out. Funding it does not lift the block, so route the user to support.never
switch (err.code) {
  case "WALLET_SUSPENDED":
    return showSupportMessage();
  case "APP_SPEND_CAP_EXCEEDED":
    return showCapReached(err.topupUrl); // may be undefined
  default:
    return err.topupUrl ? redirect(err.topupUrl) : showGenericMessage();
}

topupUrl is optional on any 402, so never dereference it unchecked. A spend-cap refusal that loses a race with the charge being written arrives without one.

See the error reference for the full contract, and Sandbox to reproduce all three on demand instead of waiting for them in production.

Handle it in one place

Repeating that try/catch everywhere is tedious. Register the onPaymentRequired hook once, and it fires whenever a 402 is mapped (from a proxied call or getBalance()), just before the error is thrown:

const client = new FleeexClient({
  apiKey,
  userId,
  onPaymentRequired: (err) => {
    if (err.topupUrl) redirect(err.topupUrl);
  },
});

The hook runs first; the call still rejects with the PaymentRequiredError, so any local try/catch continues to work.

Money is credited on the verified payment, not on the return

The link takes the end user through a hosted Checkout. Their balance is credited when the payment provider confirms it, never when they land back on your redirectUri, so a getBalance() immediately after the return can still read the old figure. Don't gate your UI on the redirect alone: poll, or subscribe to topup.completed and be told.

Avoid the 402 entirely

To check up front, before a first chat call, whether a user is connected and funded, use getConnection(), which returns status without ever raising a 402.