Error Codes
The error codes and messages the SDK produces, what each one means, and what to do about it.
Most SDK failures are a plain Error whose message says what went wrong. A handful carry a machine-readable code you can branch on. This page lists both: the codes first, then the messages you are most likely to see.
How errors surface
| Where | What you get |
|---|---|
| Hooks | An error field (usually a string) on the hook result, for example useConnectedAgents(), PricingPage |
| Async calls you make | A rejected promise with an Error; read err.message, and err.code where this page lists one |
onError in the server config | (error, { method, path }) runs for every failed API request, before the error is thrown to you |
onUnauthorized | On the *Api client classes (WorkspaceApi and siblings): runs when a request gets a 401 |
| Tracking | An sdk_error event with component, action and code, never the message |
import BuildBase from '@buildbase/sdk';
const bb = new BuildBase({
serverUrl: process.env.BUILDBASE_SERVER_URL!,
orgId: process.env.BUILDBASE_ORG_ID!,
onError: (error, { method, path }) => {
console.error(`[buildbase] ${method} ${path}`, error.message);
},
});Aborted requests (an AbortError, from your own AbortController or an unmounted component) are treated as cancellations: the SDK does not report them, and you can ignore them too.
Error codes
Credits
| Code | When it happens | What to do |
|---|---|---|
INSUFFICIENT_CREDITS | useConsumeCredits() or bb.credits.consume() gets a 402: the balance is too low | Read err.available and err.requested to explain the gap, then offer openCreditStore() or a plan change |
import {
useConsumeCredits,
useSaaSAuth,
useSaaSWorkspaces,
} from '@buildbase/sdk/react';
function Generate() {
const { currentWorkspace } = useSaaSWorkspaces();
const { consumeCredits } = useConsumeCredits(currentWorkspace?._id);
const { openCreditStore } = useSaaSAuth();
async function run() {
try {
await consumeCredits({ amount: 10, description: 'AI generation' });
} catch (err: any) {
if (err.code === 'INSUFFICIENT_CREDITS') {
alert(
`You have ${err.available} credits, this needs ${err.requested}.`
);
openCreditStore();
return;
}
throw err;
}
}
return <button onClick={run}>Generate</button>;
}Account and security
The calls behind the Profile and Security screens (WorkspaceApi) keep the server's sentence as the message and carry its reason as err.code. Two of them can come from any change the user makes:
| Code | When it happens | What to do |
|---|---|---|
reauth_required | A 403: the change needs a sign-in from the last 10 minutes, and this session is older | isReauthRequired(err), then useSaaSAuth().requireReverification({ factors: err.factors }). See Confirm it's you |
read_only_session | A 403: an app owner is signed in as the user, read-only | isReadOnlySession(err). Show the message; retrying cannot succeed |
impersonation_sign_in_locked | A 403: an app owner is signed in as the user and tried to change how the person signs in (email, phone, username, password, second step, passkeys, linked accounts) | Show the message; it applies to every sign-in-as session, read-only or not |
Neither signs the user out. The rest are listed with their feature: Two-step sign-in, Phone sign-in, Connected Accounts, Usernames, Guest users and Deleting accounts.
Uploads and storage
An upload through the SDK (useUpload, UploadsApi) rejects with an Error that has err.status and err.code set. uploadFailureOf(err) turns them into a reason you can word yourself - tooLarge, invalid, disabled, userLimit, unavailable or failed - which useUpload also returns as failure. The same codes come back from the console API's asset uploads and from a form's file field, where your own code reads the response body.
| Code | Status | When it happens | What to do |
|---|---|---|---|
UPLOAD_TOO_LARGE | 413 | The file is larger than the route, the form field or the organization's end-user limit takes. From an upload intent, the body carries maxBytes, the limit in bytes | Say the limit with maxBytes and ask for a smaller file. On the 5 MB multipart route, upload direct instead |
INVALID_UPLOAD | 400 | The declared size or type is missing or not an image, an end-user upload names an unknown purpose, or what arrived is empty, larger than declared or not a supported image | Send JPEG, PNG, GIF, WebP or SVG with its real size, and pick the file again |
UPLOADS_DISABLED | 403 | The organization turned end-user uploads off | Hide the upload control: the public settings answer uploads.enabled: false |
USER_UPLOAD_LIMIT | 403 | The file would take this user past the organization's per-user total. The body carries heldBytes and limitBytes | Ask the user to remove an earlier file, or raise the per-user total in the console |
UPLOADS_UNAVAILABLE | 503, 501 | 503: the organization cannot take the file right now (storage full or a plan limit), said neutrally to an end user or a form visitor. 501: the server has no direct uploads configured | Show a "try later" message. As the organization, read the storage meter; on a self-hosted install, set up direct uploads |
UPLOAD_NOT_RECEIVED | 409 | complete was called before the PUT to storage finished | Complete once the PUT answers; retry after a moment |
UPLOAD_FINALIZING | 409 | Another call is completing the same upload right now | Retry after a moment; it answers the same file |
INLINE_FILE_TOO_LARGE | 413 | A JSON body carries a data: file over 16 KB in one field, named in field, while the storage gate is enforced | Upload the file and send its URL instead |
PLAN_LIMIT_REACHED | 402 | On the console API, with quotaSlug: "storage-gb": the upload does not fit under the storage ceiling. The body carries storage with usedBytes, reservedBytes, ceilingBytes, wantedBytes and cause (plan, trial or payment) | Delete files you no longer need or move to a plan with more storage; for trial, start the plan; for payment, fix the payment method. See Assets |
An end user and a form visitor never see PLAN_LIMIT_REACHED: the organization's plan is not theirs to act on, so they get UPLOADS_UNAVAILABLE with 503.
Agent token verification
These are the code values on AppBridgeError, thrown by verifyClientJwt(), verifyAppTokenRequest() and verifyAppRevokeRequest() when a request from BuildBase to your applicationTokenUrl or applicationRevokeUrl fails verification.
| Code | When it happens | What to do |
|---|---|---|
invalid_token | The token or client secret is missing, the JWT is malformed, or a claim has a bad type | Check the Authorization: Bearer header reaches your handler intact |
invalid_algorithm | The JWT header is not HS256 | Reject it; BuildBase only signs with HS256 |
invalid_signature | The signature does not match the client secret | Confirm the client secret in your environment matches the one in the dashboard |
invalid_secret | signClientJwt() was called without a signing secret | Check the environment variable holding the client secret is set where the code runs |
token_expired | exp has passed, beyond the 60-second clock tolerance | Check your server clock; widen clockToleranceSec only if your clock skew is known |
token_not_yet_valid | nbf is still in the future | Same as above: usually clock skew |
invalid_issuer | iss does not equal the issuer option you passed | Fix the expected issuer, or drop the option |
invalid_audience | aud does not contain the audience option you passed | Fix the expected audience, or drop the option |
handleAppTokenRequest() and handleAppRevokeRequest() catch these for you and return a 401 whose body message is the code, so you only handle AppBridgeError directly when you call the verify functions yourself.
Tracking codes
The code on an sdk_error tracking event is SDK_ERROR when an operation threw an Error, and UNKNOWN_ERROR when it threw something that was not an Error. Use component and action on the same event to find which operation failed.
Common error messages
These have no code; match on the situation rather than the exact string.
| Message | When it happens | What to do |
|---|---|---|
Invalid serverUrl: "...". Must be a valid URL. | Provider config has a serverUrl that does not parse | Include the protocol, e.g. https:// |
Invalid orgId: "...". Must be a valid MongoDB ObjectId (24 hexadecimal characters). | orgId is not a 24-character hex id | Copy the organization id from the dashboard |
Invalid version: "...". Only "v1" is currently supported. | version is not v1 | Pass ApiVersion.V1 |
SDK is not ready (missing serverUrl, version, or orgId) | An API call ran before the config was set | Mount SaaSOSProvider above the component, or pass all three to new BuildBase() |
use... must be used within a ...Provider | A hook rendered outside SaaSOSProvider | Move the provider higher in the tree |
Request timeout after 30000ms: GET ... | A request outlived the timeout option (30 seconds by default) | Raise timeout, or set maxRetries for transient failures (retries apply to network errors and 5xx) |
No workspace selected | A credit purchase ran with no current workspace | Wait for useSaaSWorkspaces() to load, or pick a workspace first |
Workspace not found | A delete targeted an id that is not in the user's loaded workspace list | Refresh the workspace list; the user may have been removed |
Only the workspace creator can delete the workspace | A non-owner called delete | Hide the action with usePermissions().isOwner |
Cannot remove the workspace owner / Cannot change the role of the workspace owner | An action targeted the owner | Filter the owner out of member actions |
Messages from the server
When the API responds with an error, the thrown message is the server's own message when it sends one. When it does not, the SDK falls back to <action> (<status>: <reason>), for example Failed to fetch credit balance (403: Forbidden - You do not have permission):
| Status | Fallback reason | Usually means |
|---|---|---|
| 400 | Bad Request | A required field is missing or malformed |
| 401 | Unauthorized - Please check your session | The session expired or was revoked; sign the user in again |
| 403 | Forbidden - You do not have permission | The user's role lacks the permission; see Permissions |
| 404 | Resource not found | The id is wrong, or the resource belongs to another workspace |
| 500 | Internal server error | Retry; if it persists, contact support |
| 502 | Bad Gateway | The server is unreachable or restarting; retry |
| 503 | Service unavailable | Same as 502 |
Some methods also attach the HTTP status as err.status. It is not set on every call, so check for it before relying on it.
Next steps
- Troubleshooting - symptoms and fixes by area.
- Server SDK - the
BuildBaseclient and its config options.