---
title: "Logging & Credential Redaction"
description: "What the SDK redacts automatically before any request information enters the logger or an AjaxError, what it does not redact, and how to wire a custom logger via setLogger(...) without re-introducing credential leaks."
canonical_url: "https://bitrix24.github.io/b24jssdk/docs/working-with-the-rest-api/logging"
last_updated: "2026-07-03"
---
# Logging & Credential Redaction

> What the SDK redacts automatically before any request information enters the logger or an AjaxError, what it does not redact, and how to wire a custom logger via setLogger(...) without re-introducing credential leaks.

## Overview

The SDK redacts a fixed set of credential-bearing keys before request data
can reach a logger sink or an error object. The redactor
([`redact.ts`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/redact.ts){rel="[\"nofollow\"]"})
is a single source of truth, shared across the SDK.

In the **HTTP layer** (since v1.1.2) it strips those keys from any request
payload before it can reach:

- the **logger context** of `post/send`, `post/response`, `post/catchError`
info-level entries (and the lower-level `http request starting` /
`_logRequest` debug entry),
- the `requestInfo.params` field carried by [`AjaxError`](#ajaxerror--tojson--tostring),
which is what consumers see via `AjaxError.toString()` / `toJSON()`.

The same redactor — plus a few safe-projection fixes — also guards the
credential surfaces **outside** the HTTP layer: the pull client, the frame
`postMessage` channel, and hook error messages. See
[Non-HTTP surfaces](#non-http-surfaces) below.

This means a user wiring a custom logger through `B24Hook.setLogger(...)`,
`B24Frame.setLogger(...)` or `B24OAuth.setLogger(...)` does **not** need
to add their own redaction for the standard credential keys listed below —
the SDK already strips them on the way out.

Caller-supplied custom fields are **not** covered. Anything you stuff into
`params` under a non-standard key (e.g. `mySecretField`) reaches the logger
verbatim. See [What the SDK does not redact](#what-the-sdk-does-not-redact).

## What the SDK redacts automatically

The single source of truth is
[`packages/jssdk/src/core/http/redact.ts`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/redact.ts){rel="[\"nofollow\"]"}.

```ts
// packages/jssdk/src/core/http/redact.ts
export const SENSITIVE_PARAM_KEYS: readonly string[] = [
  'auth',
  'password',
  'token',
  'secret',
  'access_token',
  'refresh_token',
  'client_secret',
  'application_token',
  'sessid',
  'key',
  'signature'
]
```

Wherever any of these keys appears — matched **case-insensitively**, so
`Auth` / `TOKEN` / `Access_Token` are caught too — the value is replaced
with `'***REDACTED***'` (the exported `REDACTED_PLACEHOLDER`) before the
payload is serialised for logging or stored on an error object.

The walk descends **two levels** into nested objects and arrays. That
covers the batch shape `{ cmd: [{ method, params: { ...creds... } }, ...] }`
where the credential lives at `cmd[i].params.<key>`, plus flat one-level
shapes like `{ data: { token } }`. Anything deeper than that is **not**
walked — keep secrets close to the top of the payload if you want them
caught, or redact at the callsite.

The redactor also scans **string values** for query-string credential
pairs (`<key>=<value>`) and masks the value in place. This covers the
serialised batch command shape `cmd[i] = 'method?auth=<token>&…'`, where
the credential is text rather than an object key. Only a `key=value` pair
whose key is one of the canonical keys is masked, and a query boundary
(`?` / `&` / start of string) is required — so a value-position `=` is
left alone.

The `key` entry is deliberately broad: it masks any property literally
named `key` (and any `?key=…` query pair), not only API-key-shaped values.
In Bitrix24 REST `key` is a credential parameter, so this is a conservative
choice — be aware a non-credential field named `key` will also show as
`***REDACTED***` in your logs.

The `signature` entry (added in #43 for the Pull channel HMAC) is broad in
the same way: any property named `signature` and any `?signature=…` query
pair is masked. In Bitrix24 push/pull `signature` is the channel HMAC, so the
breadth is accepted — a non-credential field named `signature` will also
render as `***REDACTED***`.

### HTTP callsites that consume the redactor

| Where | Log channel | Level | Source |
| --- | --- | --- | --- |
| `post/send` (request being dispatched) | logger context `params` | `info` | [`abstract-http.ts:544`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/abstract-http.ts#L544){rel="[\"nofollow\"]"} |
| `post/response` (success body) | logger context `result` | `info` | [`abstract-http.ts:561`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/abstract-http.ts#L561){rel="[\"nofollow\"]"} |
| `post/catchError` (axios error) | logger context `responseData` | `info` | [`abstract-http.ts:506`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/abstract-http.ts#L506){rel="[\"nofollow\"]"} |
| `http request starting` (`_logRequest`) | logger context `params` | `debug` | [`abstract-http.ts:678`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/abstract-http.ts#L678){rel="[\"nofollow\"]"} |
| `AjaxError` constructor | `requestInfo.params` (visible via `toJSON()` / `toString()`) | n/a | [`ajax-error.ts:44`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/ajax-error.ts#L44){rel="[\"nofollow\"]"} |

The other log callsites in [`abstract-http.ts`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/abstract-http.ts){rel="[\"nofollow\"]"}
(`http request attempt`, `http request successful`, `http request failed`,
`http refreshing auth token`, `http auth error detected`, retry-wait
logs) carry only `requestId`, `method`, `api`, `attempt`, `duration`,
and `AjaxError` code/message/status — none of them touch `params` or
the webhook URL, so they have no redaction concern.

The logged payloads are also **length-capped**: `post/send` `params`,
`post/response` `result`, and `post/catchError` `responseData` are each
truncated to a 100-character prefix + `...` once they exceed 300 characters, so
a large body (an HTML error page, a big validation dump) can't flood a wired
logger sink. The cap is applied **after** redaction, so it never affects what is
masked — only how much is written (#236).

The redaction is **level-independent** — it runs on the data before it
is handed to the logger, so it applies whether your handler is filtering
at `DEBUG`, `INFO`, `ERROR`, or anything else.

### Non-HTTP surfaces

The #43 audit extended the same protection to the credential surfaces that
live outside the HTTP layer. These do not pass through `post/*`, so they are
guarded at their own callsites:

| Surface | What was carrying a credential | How it's guarded |
| --- | --- | --- |
| **Pull** — `logMessage` (`onPull*Event` info logs), `broadcastMessages` error warning, `attachCommandHandler` debug | app-defined `params` / `extra` may hold a credential-shaped key (e.g. a channel `signature`) | run through `redactSensitiveParams` before logging |
| **Pull** — JSON-RPC `unknown id` / `unknown rpc packet` / `unknown rpc command in batch` error logs | an opaque server frame can carry a `signature` | run through `redactSensitiveParams` |
| **Pull** — `CHANNEL_EXPIRE` "new config for … channel set" console log | stringified the `TypeChanel` object, which includes its `signature` | emits `[updated]` instead of the object |
| **Pull** — unparseable raw wire frame | the raw bytes could embed a `signature` | logs only the frame's `byteLength`, never its content |
| **Frame** — `B24Frame.init()` "init data" debug log | the handshake `data` carries `AUTH_ID` / `REFRESH_ID` / `MEMBER_ID` and the app `*_OPTIONS` stores | logs a fixed allowlist projection (`PLACEMENT` / `LANG` / `INSTALL` / `IS_ADMIN` / `FIRST_RUN`) |
| **Frame** — inbound `MessageManager._runCallback` | `event.data` carries the refreshed `AUTH_ID` / `REFRESH_ID` | logs only the callback `id` and `origin`, never the payload |
| **Frame** — outbound `MessageManager.send` | the assembled `cmd` string carries serialised `setAppOption` / `setUserOption` values | logs only the `command` + callback key, never the `cmd` string |
| **Hook** — `B24Hook.fromWebhookUrl` parse / format errors | the webhook URL embeds the secret in its path (`/rest/<userId>/<secret>/`) | the thrown messages are generic, never echoing the URL |

The pull `localStorage` config cache (push `jwt` + channel signatures) persists
only for the life of the client: `PullClient.destroy()` removes it on teardown,
and a stale entry is evicted on load. While the client is live the cache remains
a **documented, accepted** shared-origin trade-off (it avoids a config round-trip
on every reconnect) — see [#242](https://github.com/bitrix24/b24jssdk/issues/242){rel="[\"nofollow\"]"}.

## Webhook URL is no longer logged

The full webhook URL (`https://<portal>/rest/<userId>/<secret>/<method>.json`)
used to enter the `post/send` info log. Since v1.1.2 only the bare REST
method name (e.g. `user.current`, `crm.item.add`) is logged; the
`post/response` and `post/catchError` callsites have always been URL-free
and stay that way. `AjaxQuery` (the `requestInfo` shape carried by
`AjaxError`) no longer types a `url` field at all — see
[`ajax-result.ts:12-16`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/ajax-result.ts#L12-L16){rel="[\"nofollow\"]"} —
so a future change cannot accidentally re-introduce the leak through
error rendering.

Log archives from SDK versions `>= 1.1.0, < 1.1.2` with a custom
logger wired may contain the webhook URL pattern
(`/rest/{userId}/{secret}/`) or the auth-token pattern
(`"auth":"..."`) that are no longer produced since v1.1.2. See
[Auditing your log archives](#auditing-your-log-archives) below for
patterns to verify your sinks are clean.

## AjaxError — `toJSON` / `toString`

`AjaxError` accepts a `requestInfo` containing the caller-supplied
`params`. The constructor passes those `params` through the same
redactor, so the credential keys above never survive on the error
object — neither in `toJSON()` output, nor in the stringified form, nor
in any log line that subsequently includes the error.

```ts-type
// What you get out of AjaxError.toJSON().requestInfo.params,
// no matter what the caller passed in:
{
  ID: 42,
  access_token: '***REDACTED***',
  auth: '***REDACTED***'
}
```

## What the SDK does not redact

The SDK knows about the canonical keys listed above, matched
**case-insensitively** by exact key name. The redactor will **not** strip:

- **Custom payload fields you invented** — e.g. `mySecretField`,
`apiKeyHeader`, `x_internal_token`, anything whose key is not in
[`SENSITIVE_PARAM_KEYS`](#what-the-sdk-redacts-automatically).
- **Credentials embedded inside string values, except canonical
query-string pairs** — a `<canonical-key>=<value>` pair inside a string
(e.g. a batch `cmd[i]`) **is** masked, but a secret that is not in
`key=value` form — a bare token pasted into a `description`, or a
bracketed/encoded query key like `auth[application_token]=…` — is not.
- **Data more than two levels deep** — anything past `cmd[i].params.<key>`
depth is walked past, not into.
- **Custom request headers you set on the underlying axios client** —
e.g. an `Authorization: Bearer <token>` header configured via your
own axios interceptor. The redactor only touches `params`, not
headers, and headers are not part of the SDK's standard logger
context anyway — but they do live on `AxiosError.config.headers`, see
the next bullet.

> [!WARNING]
> **Don't log `err.originalError` directly.** `AjaxError` (via its
> [`SdkError`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/sdk-error.ts#L15){rel="[\"nofollow\"]"}
> base) exposes a public `originalError` field that, for transport
> failures, holds the **raw** `AxiosError`. That `AxiosError.config.url`
> contains the full webhook URL
> (`/rest/{userId}/{secret}/method.json`) and `AxiosError.config.headers`
> can contain `Authorization`. Neither is redacted by the SDK — they
> were never inside `params`. Use `err.code`, `err.status`, `err.message`,
> or `err.requestInfo` (which **is** redacted), and never blanket-stringify
> `err.originalError` into any logger sink.

Stripping the items above is **caller responsibility**. Either don't put
secrets into non-standard keys / non-standard log channels to begin
with, or redact in your custom logger's handler / processor.

## Recommended pattern: wiring a custom logger

The safe pattern is to leave the SDK's redaction in place and bolt your
custom sink on top — never replace the SDK-side scrubbing with a
homegrown one downstream of it.

// @check-ignore: full example — sendToRemoteAggregator is a placeholder, not declared

```ts
import {
  B24Hook,
  Logger,
  ConsoleV2Handler,
  LogLevel
} from '@bitrix24/b24jssdk'

const b24 = new B24Hook({ /* … */ })

const $logger = new Logger('app')
$logger.pushHandler(new ConsoleV2Handler(LogLevel.INFO))

// Optional belt-and-braces processor: redact any *custom* keys the SDK
// does not know about. Standard keys (auth / token / …) are already
// scrubbed before they reach this point.
$logger.pushProcessor((record) => {
  if (record.context?.params && typeof record.context.params === 'string') {
    record.context.params = record.context.params.replace(
      /"mySecretField":"[^"]*"/g,
      '"mySecretField":"***REDACTED***"'
    )
  }
  return record
})

b24.setLogger($logger)
```

What to **avoid**:

// @check-ignore: anti-pattern illustration — b24 and sendToRemoteAggregator are undeclared placeholders

```ts
// DON'T — re-emitting context without inspection forwards arbitrary
// caller-supplied data into a remote sink, and bypassing LoggerInterface
// with `as any` hides the fact that handlers must implement all 8
// log-level methods (log / debug / info / notice / warning / error /
// critical / alert / emergency). A partial implementation will silently
// drop entries at the unimplemented levels.
b24.setLogger({
  async info(message, context) {
    await sendToRemoteAggregator({
      msg: message,
      // `context.params` on `post/send` is already a redacted *string*,
      // but a custom action wrapper may also call this `info` with a
      // raw `params` object (or with an `error` whose `originalError`
      // is the unredacted `AxiosError`). Don't forward `context`
      // blindly — pick the fields you actually want.
      ctx: context
    })
  }
  // ...other 7 methods omitted — the cast below is what lets this
  // compile despite the incomplete interface; in real code, implement
  // `LoggerInterface` fully (or extend `AbstractLogger`).
} as any)
```

Three concrete failure modes to guard against:

1. **Logging the original request object yourself.** If your wrapper
captures `params` before calling into the SDK and logs it, the SDK's
redactor never runs on that copy. Always redact in your own log path
too.
2. **Reaching into `err.originalError` / `err.cause`.** `AjaxError`'s
`requestInfo.params` is scrubbed, so `String(err)` and
`JSON.stringify(err)` are safe by themselves. But if you log
`err.originalError`, you may be looking at a raw `AxiosError` whose
`config.url` still holds the full webhook URL. See the warning
block in [What the SDK does not redact](#what-the-sdk-does-not-redact).
3. **Custom or deeply-nested secrets in `post/response`.** The
`post/response` channel now runs `response.data.result` through the
same redactor, so canonical credential keys at depth ≤ 2 are masked.
But a secret under a **custom** key, or nested deeper than two levels,
still reaches your sink — the same limits as everywhere else (see
[What the SDK does not redact](#what-the-sdk-does-not-redact)).

## Auditing your log archives

Scan your log archives to verify that no credential patterns are present
— a useful routine check, especially if your setup pipes logs to a
third-party aggregator or retains them long-term.

**Webhook URL in request logs** — grep (Linux/macOS) / ripgrep / PowerShell:

```bash
grep -rEi '/rest/[0-9]+/[a-zA-Z0-9]+/' /var/log/myapp/
rg -i '/rest/\d+/[a-zA-Z0-9]+/' /var/log/myapp/
```

```powershell
Select-String -Path 'C:\logs\myapp\*.log' -Pattern '/rest/\d+/[a-zA-Z0-9]+/'
```

**Credential keys in serialised params** — grep (Linux/macOS) / ripgrep / PowerShell:

```bash
grep -rE '"(auth|password|token|secret|access_token|refresh_token|client_secret|application_token|sessid)":\s*"[^"]+"' /var/log/myapp/
rg '"(auth|password|token|secret|access_token|refresh_token|client_secret|application_token|sessid)":\s*"[^"]+"' /var/log/myapp/
```

```powershell
Select-String -Path 'C:\logs\myapp\*.log' -Pattern '"(auth|password|token|secret|access_token|refresh_token|client_secret|application_token|sessid)":\s*"[^"]+"'
```

Replace `/var/log/myapp/` (or `C:\logs\myapp\`) with your actual log
paths. Common sinks to check: stdout captures (Docker logs, systemd
journal, PM2), files written via `StreamHandler`, and third-party
aggregators (Datadog, Logtail, Splunk, Papertrail, etc.).

The webhook URL pattern was produced by `post/send` entries in SDK
versions `>= 1.1.0, < 1.1.2` when a custom logger was wired. The
credential-key patterns could appear in `post/send` for any version in
that range where the corresponding key was present in request params.
Both are suppressed by the SDK since v1.1.2.

`client_secret`, `application_token` and `sessid` were added to the
redactor in v1.3 — include them (above) when scanning archives from
endpoints that pass those params. The canonical list also covers `key`,
omitted from the patterns above because `"key":` is too common to grep
without heavy noise — scan for it manually if your integration uses `key`
as a credential.

`signature` was added in #43 for the push/pull channel HMAC. Archives from
older versions that wired a custom logger with the pull client active may
contain `"signature":"…"` — scan for `"signature":\s*"[^"]+"` if you used the
pull server, keeping in mind it (like `key`) can be noisy.

If either pattern matches in your archives, rotate the affected
credentials: delete and recreate the webhook to generate a new secret,
or revoke and reissue the OAuth / Frame token.

## Advanced: reading the source

For details beyond what is summarised here, the canonical references are:

- [`packages/jssdk/src/core/http/redact.ts`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/redact.ts){rel="[\"nofollow\"]"}
— the key list, placeholder, and walk depth.
- [`packages/jssdk/src/core/http/abstract-http.ts`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/abstract-http.ts){rel="[\"nofollow\"]"}
— every callsite that pipes data through the redactor.
- [`packages/jssdk/src/core/http/ajax-error.ts`](https://github.com/bitrix24/b24jssdk/blob/main/packages/jssdk/src/core/http/ajax-error.ts){rel="[\"nofollow\"]"}
— error-construction redaction and the `requestInfo` shape.

See also the [Logger page](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/logger.md) for
the logging framework itself (channels, handlers, processors,
formatters) and the [Errors page](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/errors.md)
for `AjaxError` semantics.

## Sitemap

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