# 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`](/tracking/events) event with `component`, `action` and `code`, never the message           |

```typescript
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 |

```tsx
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](/authentication/reverification) |
| `read_only_session`            | A 403: an app owner is [signed in as the user](/authentication/sign-in-as), 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](/authentication/sign-in-as) 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](/authentication/two-step#your-own-screens), [Phone sign-in](/authentication/phone#a-phone-on-a-signed-in-account), [Connected Accounts](/authentication/connected-accounts#the-api), [Usernames](/authentication/usernames#choosing-one), [Guest users](/authentication/guests#the-api) and [Deleting accounts](/authentication/account-deletion#the-api).

### 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](/assets/overview#when-an-upload-does-not-fit) |

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](/permissions/overview)    |
| 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](/reference/troubleshooting#getting-help) |
| 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](/reference/troubleshooting) - symptoms and fixes by area.
- [Server SDK](/server-sdk/overview) - the `BuildBase` client and its config options.
