v3.0.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.
invalid_tokenOAuth access token is not valid — revoked, malformed, or issued for another application. Unlike expired_token, refreshing does not help; the application has to obtain a new grant.
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_FRAME_REFRESH_AUTH_TIMEOUTrefreshAuth() — the parent window did not answer within 10 seconds (slow, navigated away, or blocked). The previous token is kept; retry.
JSSDK_FRAME_REFRESH_AUTH_BAD_RESPONSErefreshAuth() — the parent window answered without a usable AUTH_EXPIRES. The previous token is kept rather than overwritten with an unusable one; retry. The message names the field, never its value.
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_PULL_PUBLIC_IDS_UNAVAILABLEPullClient.sendMessage() could not resolve the recipients' channels, so there was nobody to send to. The usual cause is not a fault: pull.channel.public.list is not part of the application REST surface, and an application's Pull client is documented as receive-only — the back end publishes with pull.application.event.add. Not raised when every recipient's channel is already cached and unexpired — the startup config call prefills that cache from publicChannels, which carries the current user's own channel, so a send addressed to yourself often skips the lookup entirely. Never raised by sendMessageToChannels(), which takes channel ids from the caller and does no lookup. Before 3.0.0 this failure resolved as success and the message was silently dropped.
JSSDK_PULL_SEND_REFUSEDPullClient.sendMessage() / sendMessageToChannels() handed the frame to the transport and it would not take it — the WebSocket is not open, or long-polling has no publication path. The connector log carries the specific reason. Note that the absence of this error is not a delivery receipt: it means the transport accepted the frame, and long-polling does not await the response. When an accepted frame then goes nowhere, the reason is usually not an error at all but a WebSocket close code: the push server is reported to refuse a frame by closing the connection rather than by answering. CloseReasons names them: WRONG_REQUEST_DATA (4013), NO_CHANNELS_FOUND (4017), PRIVATE_CHANNEL_NOT_ALLOWED (4020), INVALID_CHANNEL_SIGNATURE (4021) and others. They never reach a caller as an error, only on the socket's close event. Before 3.0.0 the refusal was returned as a false the caller had to notice.
JSSDK_PULL_PUBLISHING_DISABLEDA send was attempted on a portal that has not enabled client publishing (publish_enabled). Publish from your back end with pull.application.event.add instead. Before 3.0.0 this threw a bare Error with no code to branch on.
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_IDEMPOTENCY_KEY_BROWSERidempotencyKey on a restApi:v3 call in a browser. The portal's CORS preflight does not allow the Idempotency-Key header, so a cross-origin request carrying it is refused (#573). Thrown before anything leaves; make idempotent writes from a server.
JSSDK_HTTP_REDIRECT_BLOCKEDA redirect answered a request whose effective maxRedirects is 0 — by default a restApi:v3 batch or a restApi:v3 call with idempotencyKey, 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 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.
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. A v3 rate-limit error (BITRIX_REST_V3_EXCEPTION_RATELIMITEXCEPTION) also arrives as 429 and is classified here, by status alone — see Three layers produce a throttling error.
  • Any other transient code → exponential backoff with jitter.

OVERLOAD_LIMIT is the third of those layers. It is in the built-in hard-code list, but that does not decide what happens to it: the status matchers run first, so at the 503 it is documented with, the rate-limit branch claims it and it is retried. The hard-code listing decides only how it is delivered once the retries are exhausted.

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

Pull close codes

These are not error codes and never reach a caller as one. The push server refuses a frame by closing the WebSocket with a numeric code and a reason string, so they arrive on the socket's close event and nowhere else. CloseReasons names them.

The numbers are what the close event gives you. What each one means comes from an audit of the push server's own sources rather than from published documentation, so treat the descriptions below as informed guidance rather than as a contract.

The enum holds both directions. Codes below 4010 are the ones the client sends when PullClient ends the connection itself (NORMAL_CLOSURE, CHANNEL_EXPIRED, CONFIG_EXPIRED, STUCK and the rest) — those never indicate a rejected message.

CodeNameWhat it means
4013WRONG_REQUEST_DATAThe frame did not decode, or carried no requests.
4014REQUEST_COMMAND_NOT_ALLOWEDNot incomingMessages or channelStats.
4015WRONG_REQUEST_COMMANDNo handler for the command.
4016TOO_MANY_MESSAGESMore than 100 messages in one batch.
4017NO_CHANNELS_FOUNDThe message addressed nobody.
4018TOO_MANY_CHANNELSMore than 100 channels in one request.
4019INVALID_CHANNEL_IDA trusted-connection check; not reachable from a browser.
4020PRIVATE_CHANNEL_NOT_ALLOWEDA client may not publish to a private channel.
4021INVALID_CHANNEL_SIGNATUREThe channel signature did not verify.

Three more sit in the same numeric range and are connection-level — they say the connection is unusable, not that a particular message was rejected: WRONG_CHANNEL_ID (4010), NO_PUBLIC_CHANNEL_ID (4012) and TOO_MANY_CONNECTIONS (4029). Testing for "is it 401x" would misclassify all three, so use isFrameRefusalCloseCode() instead.

A close code is evidence when it arrives. Its absence proves nothing about the message you encoded. A field written at a number the server does not declare is skipped: the frame parses, is broadcast, and carries nothing useful — no close, no error, no sign.Note the limit of that, because it cuts the other way too. A message that is well-formed but semantically wrong does produce a close code — no receivers is 4017, a private channel 4020, a bad signature 4021. So the table above is worth checking first; it is only the encoding itself that a silent run fails to exonerate.