v2.2.0

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 (or a subclass). Handle it in try / catch.
  • Soft — the promise resolves; the error is packed into the returned AjaxResult 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.

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.

// 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.

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.

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.

CodeMeaning
ERR_BAD_REQUESTMalformed request — usually fix the calling code.
JSSDK_UNKNOWN_ERRORSDK internal error of unknown shape.
100Generic legacy 100 code.
INTERNAL_SERVER_ERRORServer-side 500.
ERROR_UNEXPECTED_ANSWERBitrix24 returned a body the SDK could not parse.
PORTAL_DELETEDPortal no longer exists.
ERROR_BATCH_METHOD_NOT_ALLOWEDBatch contains a method that is not allowed inside a batch.
ERROR_BATCH_LENGTH_EXCEEDEDBatch is over 50 commands. Use batchByChunk.
NO_AUTH_FOUNDMissing credentials.
INVALID_REQUESTBitrix24 rejected the request shape.
OVERLOAD_LIMITTariff overload limit hit (different from rate limit).
expired_tokenOAuth access token expired and refresh failed.
BITRIX_REST_V3_EXCEPTION_INSUFFICIENTSCOPEEXCEPTIONREST 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_DENIEDPermission denied for the user.
INVALID_CREDENTIALSWebhook secret / OAuth credentials invalid.
user_access_errorUser cannot access the resource.
insufficient_scopeOAuth token scope is too narrow for the call.
ERROR_MANIFEST_IS_NOT_AVAILABLEApp manifest fetch failed.
allowed_only_intranet_userEndpoint requires an intranet user.
NOT_FOUNDResource does not exist.
INVALID_ARG_VALUEOne of the arguments has a value Bitrix24 rejected.
PULL_DISABLEDPullClient.start() — Push & Pull server is disabled on the portal.
PULL_DISPOSEDPullClient.start() called after destroy() — create a new instance.
JSSDK_FRAME_DISPOSEDA frame command was still waiting on the parent window when B24Frame.destroy() ran — nothing can answer it now.
JSSDK_FRAME_BAD_PAYLOADThe parent window answered a frame command with a payload that is not valid JSON.
JSSDK_FRAME_APP_SID_NOT_INITgetAppSid() before the frame handshake completed — the parent never delivered an appSid.
JSSDK_FRAME_INSTALL_ALREADY_FINISHEDinstallFinish() called outside install mode — guard with isInstallMode.
JSSDK_HOOK_URL_EMPTYB24Hook.fromWebhookUrl() — the URL is empty.
JSSDK_HOOK_URL_INVALIDB24Hook.fromWebhookUrl() — the URL does not parse. The message never echoes the URL: it carries the secret.
JSSDK_HOOK_URL_NOT_HTTPSB24Hook.fromWebhookUrl() — webhooks require HTTPS.
JSSDK_HOOK_URL_MALFORMEDB24Hook.fromWebhookUrl() — the path is not /rest/<userId>/<secret> or /rest/api/<userId>/<secret>.
JSSDK_HOOK_URL_USER_ID_NOT_NUMERICB24Hook.fromWebhookUrl() — the userId segment is not numeric (a transposed URL puts the secret there; the message never echoes it).
JSSDK_OAUTH_TOKEN_REFRESH_FAILEDThe OAuth server answered the token refresh with an error payload.
JSSDK_OAUTH_TOKEN_REFRESH_BAD_STATUSThe token refresh returned a non-200 status (carried as status).
JSSDK_OAUTH_TOKEN_REFRESH_NO_DATAThe token refresh produced no authorization data.
JSSDK_OAUTH_IS_ADMIN_NOT_INITisAdmin read before B24OAuth.initIsAdmin() populated it.
JSSDK_OAUTH_PROFILE_FAILEDThe profile call behind initIsAdmin() failed; the description carries the portal's error text.
JSSDK_HELPER_NOT_INITA useB24Helper() accessor before initB24Helper().
JSSDK_HELPER_PULL_CLIENT_NOT_INITA Pull accessor before usePullClient().
JSSDK_HTTP_INVALID_IDEMPOTENCY_KEYidempotencyKey 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_BLOCKEDA 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.

CodeMeaning
ERROR_ENTITY_NOT_FOUNDREST v2: entity row does not exist.
BITRIX_REST_V3_EXCEPTION_ACCESSDENIEDEXCEPTIONREST v3: permission denied.
BITRIX_REST_V3_EXCEPTION_INVALIDJSONEXCEPTIONREST v3: payload is not valid JSON.
BITRIX_REST_V3_EXCEPTION_INVALIDFILTEREXCEPTIONREST v3: malformed filter.
BITRIX_REST_V3_EXCEPTION_INVALIDSELECTEXCEPTIONREST v3: malformed selector 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.
BITRIX_REST_V3_EXCEPTION_ENTITYNOTFOUNDEXCEPTIONREST v3: entity does not exist.
BITRIX_REST_V3_EXCEPTION_METHODNOTFOUNDEXCEPTIONREST v3: method does not exist.
BITRIX_REST_V3_EXCEPTION_UNKNOWNDTOPROPERTYEXCEPTIONREST v3: unknown DTO property.
BITRIX_REST_V3_EXCEPTION_VALIDATION_REQUESTVALIDATIONEXCEPTIONREST v3: request-level validation failed. error.validation[] carries field-level details.
BITRIX_REST_V3_EXCEPTION_VALIDATION_DTOVALIDATIONEXCEPTIONREST v3: DTO-level validation failed.

Retry-Triggering Codes

These are not in the catalog above — they are detected by the RestrictionManager 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.

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 (a subclass of SdkError). Useful properties:

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:

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
  }
}