---
title: "Error Codes"
description: "Codes with a pinned classification — \"hard\" (thrown) and \"soft\" (returned as AjaxError inside AjaxResult) — and the category rule that covers everything else on restApi:v3."
canonical_url: "https://bitrix24.github.io/b24jssdk/docs/working-with-the-rest-api/error-codes"
last_updated: "2026-09-14"
---
# Error Codes

> Codes with a pinned classification — "hard" (thrown) and "soft" (returned as AjaxError inside AjaxResult) — and the category rule that covers everything else on restApi:v3.

A failed call reaches you in one of two ways, and the difference is **delivery, not severity**:

- **Hard** — the promise rejects and the SDK throws `AjaxError`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} (or a subclass). Handle it in `try / catch`.
- **Soft** — the promise resolves; the error is packed into the returned `AjaxResult`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} and `response.isSuccess === false`. Handle it with an `if`.

Two things this split does **not** decide. It does not affect retries — any 4xx other than 408 and 429 stops retrying regardless, and a pinned code of either kind stops retrying too. And an *unlisted* code behaves like a hard one: anything not classified as soft is thrown.

Everything else (network errors, 5xx, rate-limit codes) is subject to the retry strategy described in [Limiters](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/limiters.md).

## The tables are not a catalogue

The two tables below list codes whose classification is **pinned**. They are not the set of codes a portal can send.

A `restApi:v3` code is derived from the exception class that raised it, so the set a build can produce grows with every portal module. One on-premise build was measured to ship at least **39 distinct v3 codes**; the pinned soft list holds **nine**. The rest are thrown today — including `INVALIDPAGINATIONEXCEPTION`, `UNKNOWNFILTEROPERATOREXCEPTION` and `INVALIDORDEREXCEPTION`, which are the same caller mistake, in the same request, at the same HTTP 400, as the `INVALIDSELECTEXCEPTION` and `INVALIDFILTEREXCEPTION` that are pinned soft.

**Do not match on the `BITRIX_REST_V3_EXCEPTION_` prefix, and do not derive a code by stripping it.** Modules ship their own v3 exceptions with short unprefixed codes — `NOTE_SEARCH_QUERY_TOO_SHORT`, `NOTE_FILE_TOO_LARGE`, `CRM_EMAIL_INVALID_REQUEST` and others. Match on the whole string or not at all. The `message` is localised and must never be matched on.

## Classification by category (`restApi:v3`)

Rather than growing the list, the SDK decides from the **response itself**: an error that arrived in the v3 error envelope carrying an HTTP **4xx other than 401, 408 or 429** is soft, whatever its code. This needs no configuration — it is the behaviour.

```ts
// const $b24 = ...
const response = await $b24.actions.v3.call.make({
  method: 'main.eventlog.list',
  params: { pagination: { limit: 0 } }
})

// Reached for any 4xx the portal reports, not only the nine pinned codes.
if (!response.isSuccess) {
  console.log(response.getErrorMessages().join('; '))
}
```

The order of decision is fixed: a code in the hard list throws; a code in the soft list is soft; then the category rule; then, as today, it throws. So a pinned classification — including your own `hardErrorCodes` / `softErrorCodes` — always outranks the category.

What the rule does **not** touch:

- **5xx** — not caller-addressable, so retry-then-throw stays right.
- **401** — the auth-refresh path owns it.
- **408 and 429** — the two retryable 4xx; an error still being retried has not been classified yet.
- **`restApi:v2`** — the flat error body is not a v3 envelope. The decision is made from the body that actually arrived, never from the client's version, because a gateway in front of the v3 controller can answer in the flat shape.

**403 is soft**, deliberately. A permission refusal is something the caller can act on, and `…ACCESSDENIEDEXCEPTION` is already pinned soft — excluding 403 wholesale would leave one 403 returned and its neighbour thrown.

One 403 is pinned hard against that rule: `BITRIX_REST_V3_EXCEPTION_INSUFFICIENTSCOPEEXCEPTION`. It means the application's OAuth grant is missing a scope — a configuration fault rather than a per-record permission check — and the v2 spelling of the same condition, `insufficient_scope`, has always been thrown. The two are different strings, so without pinning the v3 form the same failure would have been delivered one way on v2 and another on v3.

> [!CAUTION]
> **Changed in 3.0.0.** Through the `2.x` line this rule was opt-in, behind a `classifyV3ErrorsByCategory` parameter; in `3.0.0` it is the behaviour and the parameter is gone. That changes how an error is delivered: a v3 4xx that threw under `2.x` now resolves, so a `try / catch` around the call stops firing and control falls through into the success path. Move that handling to `if (!response.isSuccess)` — see the [migration guide](https://bitrix24.github.io/b24jssdk/raw/docs/getting-started/migration/v3.md#behaviour-v3-errors-are-classified-by-response-category).

## Pinned hard codes (thrown)

These codes propagate through the promise rejection path and are never softened by the category rule. Wrap your calls in `try / catch` if you need to handle them centrally.

| Code | Meaning |
| --- | --- |
| `ERR_BAD_REQUEST` | Malformed request — usually fix the calling code. |
| `JSSDK_UNKNOWN_ERROR` | SDK internal error of unknown shape. |
| `100` | Generic legacy 100 code. |
| `INTERNAL_SERVER_ERROR` | Server-side 500. |
| `ERROR_UNEXPECTED_ANSWER` | Bitrix24 returned a body the SDK could not parse. |
| `PORTAL_DELETED` | Portal no longer exists. |
| `ERROR_BATCH_METHOD_NOT_ALLOWED` | Batch contains a method that is not allowed inside a batch. |
| `ERROR_BATCH_LENGTH_EXCEEDED` | Batch is over 50 commands. Use `batchByChunk`. |
| `NO_AUTH_FOUND` | Missing credentials. |
| `INVALID_REQUEST` | Bitrix24 rejected the request shape. |
| `OVERLOAD_LIMIT` | Tariff overload limit hit (different from rate limit). |
| `expired_token` | OAuth access token expired and refresh failed. |
| `BITRIX_REST_V3_EXCEPTION_INSUFFICIENTSCOPEEXCEPTION` | REST v3: the application's OAuth grant lacks a scope this method needs. The v3 spelling of `insufficient_scope`; pinned so the category rule does not soften it at 403. |
| `ACCESS_DENIED` | Permission denied for the user. |
| `INVALID_CREDENTIALS` | Webhook secret / OAuth credentials invalid. |
| `user_access_error` | User cannot access the resource. |
| `insufficient_scope` | OAuth token scope is too narrow for the call. |
| `ERROR_MANIFEST_IS_NOT_AVAILABLE` | App manifest fetch failed. |
| `allowed_only_intranet_user` | Endpoint requires an intranet user. |
| `NOT_FOUND` | Resource does not exist. |
| `INVALID_ARG_VALUE` | One of the arguments has a value Bitrix24 rejected. |
| `PULL_DISABLED` | `PullClient.start()` — Push & Pull server is disabled on the portal. |
| `PULL_DISPOSED` | `PullClient.start()` called after `destroy()` — create a new instance. |
| `JSSDK_FRAME_DISPOSED` | A frame command was still waiting on the parent window when `B24Frame.destroy()` ran — nothing can answer it now. |
| `JSSDK_FRAME_BAD_PAYLOAD` | The parent window answered a frame command with a payload that is not valid JSON. |
| `JSSDK_FRAME_APP_SID_NOT_INIT` | `getAppSid()` before the frame handshake completed — the parent never delivered an `appSid`. |
| `JSSDK_FRAME_INSTALL_ALREADY_FINISHED` | `installFinish()` called outside install mode — guard with `isInstallMode`. |
| `JSSDK_HOOK_URL_EMPTY` | `B24Hook.fromWebhookUrl()` — the URL is empty. |
| `JSSDK_HOOK_URL_INVALID` | `B24Hook.fromWebhookUrl()` — the URL does not parse. The message never echoes the URL: it carries the secret. |
| `JSSDK_HOOK_URL_NOT_HTTPS` | `B24Hook.fromWebhookUrl()` — webhooks require HTTPS. |
| `JSSDK_HOOK_URL_MALFORMED` | `B24Hook.fromWebhookUrl()` — the path is not `/rest/<userId>/<secret>` or `/rest/api/<userId>/<secret>`. |
| `JSSDK_HOOK_URL_USER_ID_NOT_NUMERIC` | `B24Hook.fromWebhookUrl()` — the userId segment is not numeric (a transposed URL puts the secret there; the message never echoes it). |
| `JSSDK_OAUTH_TOKEN_REFRESH_FAILED` | The OAuth server answered the token refresh with an error payload. |
| `JSSDK_OAUTH_TOKEN_REFRESH_BAD_STATUS` | The token refresh returned a non-200 status (carried as `status`). |
| `JSSDK_OAUTH_TOKEN_REFRESH_NO_DATA` | The token refresh produced no authorization data. |
| `JSSDK_OAUTH_IS_ADMIN_NOT_INIT` | `isAdmin` read before `B24OAuth.initIsAdmin()` populated it. |
| `JSSDK_OAUTH_PROFILE_FAILED` | The `profile` call behind `initIsAdmin()` failed; the description carries the portal's error text. |
| `JSSDK_HELPER_NOT_INIT` | A `useB24Helper()` accessor before `initB24Helper()`. |
| `JSSDK_HELPER_PULL_CLIENT_NOT_INIT` | A Pull accessor before `usePullClient()`. |
| `JSSDK_HTTP_INVALID_IDEMPOTENCY_KEY` | `idempotencyKey` is not 1-255 printable ASCII characters (`0x21-0x7E`). `restApi:v3` only — the v2 transport drops the key with a warning instead. Thrown before the request leaves; the message never echoes the key. |
| `JSSDK_HTTP_REDIRECT_BLOCKED` | A redirect answered a request whose effective `maxRedirects` is `0` — by default just one, a `restApi:v3` batch on a non-hook transport carrying an access token a redirect to a subdomain would take along; set `maxRedirects: 0` yourself and every status-0 answer lands here. Raised where the refusal is otherwise invisible: on the `fetch` adapter the response is opaque (status 0, empty body) and axios resolves it. Outside a browser the same refusal arrives as a plain 301; with the default configuration a hook transport never reaches this code at all, since a webhook sends no token and so never carries `maxRedirects: 0`. Deterministic, so it is not retried. Point the SDK at the final URL. |

## Pinned soft codes (returned in `AjaxResult`)

These codes do not throw — they appear in `result.getErrors()` (and `getErrorMessages()`). Check `isSuccess` first. With the category rule on, many more v3 codes join them without being listed here.

| Code | Meaning |
| --- | --- |
| `ERROR_ENTITY_NOT_FOUND` | REST v2: entity row does not exist. |
| `BITRIX_REST_V3_EXCEPTION_ACCESSDENIEDEXCEPTION` | REST v3: permission denied. |
| `BITRIX_REST_V3_EXCEPTION_INVALIDJSONEXCEPTION` | REST v3: payload is not valid JSON. |
| `BITRIX_REST_V3_EXCEPTION_INVALIDFILTEREXCEPTION` | REST v3: malformed filter. |
| `BITRIX_REST_V3_EXCEPTION_INVALIDSELECTEXCEPTION` | REST v3: malformed `select` — **or** a batch whose body is not a list of commands. The portal reads every top-level entry of a batch body as a command and needs `method` and `query` on each, so one entry missing `query`, or an extra entry that is not a command at all, fails the whole batch under this code. Where the credential travels differs per runtime, and a browser is the awkward one: see [BatchV3 Limitations](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/batch-rest-api-ver3.md#limitations). |
| `BITRIX_REST_V3_EXCEPTION_ENTITYNOTFOUNDEXCEPTION` | REST v3: entity does not exist. |
| `BITRIX_REST_V3_EXCEPTION_METHODNOTFOUNDEXCEPTION` | REST v3: method does not exist. |
| `BITRIX_REST_V3_EXCEPTION_UNKNOWNDTOPROPERTYEXCEPTION` | REST v3: unknown DTO property. |
| `BITRIX_REST_V3_EXCEPTION_VALIDATION_REQUESTVALIDATIONEXCEPTION` | REST v3: request-level validation failed. `error.validation[]` carries field-level details. |
| `BITRIX_REST_V3_EXCEPTION_VALIDATION_DTOVALIDATIONEXCEPTION` | REST v3: DTO-level validation failed. |

## Retry-Triggering Codes

These are not in the catalog above — they are detected by the [`RestrictionManager`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/limiters.md) and trigger automatic retries with backoff:

- HTTP 503 / `QUERY_LIMIT_EXCEEDED` — rate limit.
- HTTP 429 / `OPERATION_TIME_LIMIT` — operating limit.
- Any other transient code → exponential backoff with jitter.

After `maxRetries` (default `3`) the SDK gives up and surfaces the underlying error with its real code (e.g. `QUERY_LIMIT_EXCEEDED`) — thrown for hard codes, or returned in the `AjaxResult` for soft codes.

> [!CAUTION]
> Currently `NETWORK_ERROR` and `REQUEST_TIMEOUT` are **not** in the hard list, which means non-idempotent calls (e.g. `crm.documentgenerator.document.add`) can be retried after a server-side timeout and create duplicates. If you hit this, consider wrapping the call in a manual idempotency key on your side.

## Working with `AjaxError`

Both buckets surface as [`AjaxError`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""}](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/ajax-error.ts){rel="[\"nofollow\"]"} (a subclass of `SdkError`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""}). Useful properties:

```ts-type
class AjaxError extends SdkError {
  readonly code: string
  readonly message: string
  readonly status: number
  readonly requestInfo?: { method?: string, params?: TypeCallParams, requestId?: string, url?: string }
}
```

Pattern for unified handling:

```ts
import { AjaxError } from '@bitrix24/b24jssdk'

async function callWithUnifiedErrorHandling(method: string, params: Record<string, unknown>, requestId: string) {
  try {
    const response = await $b24.actions.v3.call.make({ method, params, requestId })
    if (!response.isSuccess) {
      for (const error of response.getErrors()) {
        if (error instanceof AjaxError) {
          // Soft error
          console.warn(error.code, error.requestInfo?.method)
        }
      }
      return null
    }
    return response.getData()
  } catch (error) {
    if (error instanceof AjaxError) {
      // Hard error
      console.error('SDK threw', error.code, error.status)
    }
    throw error
  }
}
```

## Sitemap

See the full [sitemap](/b24jssdk/sitemap.md) for all pages.
