Fleeexdocs

Errors

The typed errors the SDK raises, the three different 402s, and every status and code the API answers with.

The SDK raises three kinds of error.

PaymentRequiredError

Thrown when the proxy or getBalance() answers 402. Carries the top-up link, when there is one.

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);
    else showSupportMessage(); // no link, see below
  }
}
PropertyTypeNotes
status402Always 402.
codestring | undefinedWhich of the three 402s this is.
topupUrlstring | undefinedSigned, short-lived link to send the end user through Checkout. Genuinely optional, so always check it before redirecting.
correlationIdstring | undefinedTrace id for support and debugging.
messagestringGeneric by design, and never reveals whether a wallet exists.

The onPaymentRequired hook (see options) fires for the same condition before the error is thrown.

The three 402s

402 is not one situation. The code says which, and the three have different remedies. Branching on it is the difference between telling a user to add funds and telling them something they can't fix by paying.

CodeMeaningtopupUrlWhat to do
PAYMENT_REQUIREDThe (app, user) has no authorized wallet, or the balance can't cover the call.alwaysSend the end user to topupUrl to authorize or fund.
APP_SPEND_CAP_EXCEEDEDYour app reached the monthly spend cap the user set on it, and their wallet may still be full.usuallyTell them this app hit the cap you set, which they can raise, rather than "add funds". The cap is checked once up front and re-checked when the charge is written; the second one loses a race rarely, and raises the same code without a link.
WALLET_SUSPENDEDThe wallet is blocked after money went back out (a refund or chargeback the balance couldn't cover).neverStop retrying and route the end user to support. Funding the wallet does not lift the block.

So treat topupUrl as optional on any 402, whatever the code, because two of the three can arrive without one.

The first two are deliberately indistinguishable from each other in the message: "no wallet" and "empty wallet" return an identical body, because telling them apart would reveal whether a given user has a Fleeex wallet at all. A user who revoked your app is indistinguishable from one who never authorized it, for the same reason.

WALLET_SUSPENDED carries no amount, no shortfall and no dispute wording. Your app isn't a party to it, and the end user's payment history isn't its business.

FleeexApiError

Any other non-2xx from a Fleeex billing helper (getConnection(), getUsageSummary(), or a non-402 from getBalance()), for example a 400 when a redirectUri isn't on the app's allow-list.

PropertyTypeNotes
statusnumberHTTP status.
codestring | undefinedError code from the envelope.
correlationIdstring | undefinedTrace id.

OpenAI errors

For the proxy path, any error other than 402 is raised by the OpenAI SDK as OpenAI.APIError (and its subclasses). Handle these exactly as you would with the OpenAI SDK, but read the code, because several are Fleeex-specific and mean different things.

import OpenAI from "openai";
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);
  } else if (err instanceof OpenAI.APIError) {
    console.error(err.status, err.code, err.message);
  } else {
    throw err;
  }
}

Every status and code

The whole data-plane surface: proxied calls, GET /v1/models, and the billing helpers.

StatusCodeWhen
400BAD_REQUESTAn unsupported or disabled model (the message echoes what you sent, not what an alias resolved to). An unsupported parameter, named, with the accepted ones listed. A malformed body, or one nesting more than 32 levels deep. A missing or malformed x-fleeex-user. A redirectUri outside the app's allow-list.
401UNAUTHORIZEDThe API key is missing, invalid or revoked.
402PAYMENT_REQUIREDNo authorized wallet, or insufficient balance. Always carries topupUrl.
402APP_SPEND_CAP_EXCEEDEDThe per-app monthly cap. Usually carries topupUrl, see above.
402WALLET_SUSPENDEDThe wallet is blocked after a claw-back. Never carries topupUrl.
403APP_SUSPENDEDYour app was suspended by a Fleeex operator. Deliberately not a 401: the credential is valid and correctly names your app, so rotating a key or retrying changes nothing. Stop calling. An in-flight stream is not cut, but the next request is refused.
403MODEL_NOT_ENTITLEDThe model is real but reserved for other apps, see Choosing a model. It reaches no provider and reserves nothing, and GET /v1/models simply omits it, so a caller that lists first never meets this.
404NOT_FOUNDGET /v1/models/{model} when no model or alias by that name is served.
413PAYLOAD_TOO_LARGEThe request body exceeds 1 MB.
429TOO_MANY_REQUESTSFleeex's per-app rate limit. It buckets on your app, so every key of the app, a sandbox key included, shares one limit.
429CONCURRENT_STREAM_LIMITThis end user already has the maximum number of streams open. Sent with a Retry-After header, and refused before any reservation or provider call, so it costs nothing. Retry when one of that user's streams finishes; the remedy is fewer streams in flight, not a slower request rate.
429UPSTREAM_THROTTLEDThe model provider throttled the call. Retriable, unlike UPSTREAM_PROVIDER_ERROR.
502UPSTREAM_PROVIDER_ERRORThe provider rejected the call (model access, an unsupported invocation, a schema outside its subset, a timeout, an outage). The reservation is released and nothing is charged.
500INTERNAL_SERVER_ERRORA Fleeex fault. The detail is logged server-side against the correlationId; the response says nothing more.

A refused call costs nothing: a 400, 402, 403 or the stream limit short-circuits before the provider is reached, so no completion is ever generated and then discarded.

The error envelope

Fleeex's backend emits a consistent body, exposed as the FleeexErrorBody type.

interface FleeexErrorBody {
  error?: { code?: string; message?: string; correlationId?: string };
  topupUrl?: string;
}

correlationId is the one thing worth logging on your side: it's the handle that ties your failed call to the server-side record of it. It's echoed on every response, not only errors, and you can supply your own with an x-correlation-id request header.

A message is never a stack trace, a cloud SDK error or an internal identifier, and for a 500, never anything but the generic text above.