---
title: "Security for event-receiver apps"
description: "Two hardening patterns for any server that receives Bitrix24 outbound events or OAuth install/uninstall callbacks: reply 200 before you verify, and compare application_token in constant time."
canonical_url: "https://bitrix24.github.io/b24jssdk/docs/working-with-the-rest-api/security"
last_updated: "2026-07-03"
---
# Security for event-receiver apps

> Two hardening patterns for any server that receives Bitrix24 outbound events or OAuth install/uninstall callbacks: reply 200 before you verify, and compare application_token in constant time.

## Overview

This page is for servers that expose a **public HTTP endpoint Bitrix24 POSTs to**:

- **Outbound event receivers** — an app that registers events with `event.bind` (`ONCRMDEALADD`, `ONCRMDEALUPDATE`, …) and processes them, as in the [webhook handler recipe](https://bitrix24.github.io/b24jssdk/raw/docs/examples/webhook-handler.md).
- **OAuth apps** — a marketplace app that handles `ONAPPINSTALL` / `ONAPPUPDATE` / `ONAPPUNINSTALL` callbacks and stores per-portal credentials, as in the [OAuth install recipe](https://bitrix24.github.io/b24jssdk/raw/docs/examples/oauth-install.md).

Both share the same threat model: the URL is reachable by anyone, and Bitrix24 authenticates each delivery with an `application_token` in the payload. Two patterns keep these endpoints correct and safe. They are independent of the SDK transport surface — they live in your HTTP handler — so they are documented here rather than on an action page.

> [!NOTE]
> Credential redaction inside the SDK is a separate concern — see [Logging & Credential Redaction](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/logging.md). This page is about the **request-handling** layer around the SDK.

## Pattern 1 — Reply 200 first, verify after

Bitrix24 doesn't call your handler in real time — it queues events on a separate server and **watches how fast your handler answers**. A handler that runs verification or processing *before* replying answers slowly, and Bitrix24 starts delivering to it with **lower priority** (longer pauses between calls). And there is **no automatic retry**: if your handler errors or times out, Bitrix24 records the failure and never re-sends that event — "no second chance" (see the [events overview](https://apidocs.bitrix24.com/api-reference/events/index.html){rel="[\"nofollow\"]"}). So a slow-or-throwing handler both degrades your delivery rate **and** silently drops events.

The fix is ordering: send `2xx` **first**, then verify and process. The acknowledgement goes out fast (keeping you off the slow-handler penalty), and verification/processing no longer block it. If losing an event is unacceptable, don't rely on live delivery — use [offline events](https://apidocs.bitrix24.com/api-reference/events/offline-events.html){rel="[\"nofollow\"]"} as the durable channel.

```ts
import { type Request, type Response } from 'express'

// Stand-ins for your real verification / processing logic.
function verifyOrigin(req: Request): boolean { return Boolean(req.body?.auth) }
async function handleEvent(payload: unknown): Promise<void> { /* … */ }

// WRONG — verify + process run BEFORE the 200 reply.
async function onWebhookWrong(req: Request, res: Response) {
  if (!verifyOrigin(req)) return  // may be slow, may throw
  await handleEvent(req.body)     // if this throws, the next line never runs…
  res.sendStatus(200)             // …so the ack never goes out — the event is
                                  // lost (no retry) and the slow path gets you
                                  // deprioritized
}
```

```ts
import { type Request, type Response } from 'express'

function verifyOrigin(req: Request): boolean { return Boolean(req.body?.auth) }
async function handleEvent(payload: unknown): Promise<void> { /* … */ }

// CORRECT — acknowledge first, verify + process after.
async function onWebhook(req: Request, res: Response) {
  const payload = req.body

  res.sendStatus(200)             // 1. reply immediately — fast ack avoids the
                                  //    slow-handler penalty

  if (!verifyOrigin(req)) return  // 2. drop spoofed / stale events (already acked)
  await handleEvent(payload)      // 3. safe: Bitrix24 already has its 200
}
```

The [webhook handler recipe](https://bitrix24.github.io/b24jssdk/raw/docs/examples/webhook-handler.md) replies with a small JSON acknowledgement — `res.status(200).json({ status: 'ok' })` — which is equivalent to `res.sendStatus(200)`; either works as long as it goes out first.

## Pattern 2 — Verify application_token in constant time

Bitrix24 includes an `application_token` in the `auth` block of every event. It is the shared secret that proves the request really came from your portal — without checking it, anyone who learns the URL can replay arbitrary events.

Compare it with a **constant-time** function. A plain `===` (or `!=`) returns as soon as it hits the first differing byte, so an attacker can recover the token one character at a time by measuring response latency. Node's `crypto.timingSafeEqual` compares in constant time, but it **throws when the two buffers differ in length** — so pre-check the length first (which also avoids leaking length):

```ts
import { type Request, type Response } from 'express'
import { timingSafeEqual } from 'node:crypto'

// Constant-time compare. `timingSafeEqual` throws on a length mismatch, so we
// pre-check length first — this also avoids leaking length via the exception.
function safeEqual(a: string, b: string): boolean {
  const ab = Buffer.from(a, 'utf8')
  const bb = Buffer.from(b, 'utf8')
  if (ab.length !== bb.length) return false
  return timingSafeEqual(ab, bb)
}

const expectedApplicationToken = process.env.B24_APPLICATION_TOKEN ?? ''

// Called AFTER res.sendStatus(200) (Pattern 1). Returns false → drop the event.
function verifyEvent(req: Request, _res: Response): boolean {
  const incomingToken = req.body?.auth?.application_token ?? ''
  return safeEqual(incomingToken, expectedApplicationToken)
}
```

Store the expected token out of band (an env var such as `B24_APPLICATION_TOKEN`, sourced from the Bitrix24 dev console → Local Application → `application_token`) and fail closed when it is unset.

### OAuth uninstall: verify before deleting

The same rule is critical on the **uninstall** callback. `ONAPPUNINSTALL` deletes a portal's stored credentials — so if you delete without verifying, anyone who can reach `/uninstall` can wipe a portal's tokens just by guessing its `member_id`. Always compare the incoming `application_token` against the one you recorded at install time, in constant time, before removing anything:

```ts
import { type Request, type Response } from 'express'

// `safeEqual` is the constant-time helper from Pattern 2 above.
declare function safeEqual(a: string, b: string): boolean
declare function getCredentials(memberId: string): Promise<{ applicationToken: string } | null>
declare function deleteCredentials(memberId: string): Promise<void>

async function handleUninstall(req: Request, res: Response) {
  res.sendStatus(200)  // reply first — Pattern 1 applies to uninstall too

  const memberId = req.body?.auth?.member_id ?? ''
  const receivedToken = req.body?.auth?.application_token ?? ''
  if (!memberId || !receivedToken) return

  const stored = await getCredentials(memberId)
  if (!stored) return  // idempotent — nothing to delete

  // ALWAYS verify application_token before deleting. Without this check, anyone
  // who reaches /uninstall could wipe a portal's tokens by guessing member_id.
  if (!safeEqual(stored.applicationToken, receivedToken)) return

  await deleteCredentials(memberId)
}
```

## Checklist

**Every event-receiver endpoint** (`event.bind` handlers):

- Serve the handler over **HTTPS** at a publicly reachable URL — Bitrix24 calls it from its own servers, and the `application_token` travels in the request body, so plain HTTP would expose it in transit.
- Reply `2xx` **before** verifying or processing (Pattern 1).
- Require an `application_token` env var and **fail closed** if it is unset.
- Compare the incoming `application_token` with `safeEqual` (Pattern 2), never `===`.
- Drop — do not error — on a mismatch (return the `2xx` you already sent; don't reveal which check failed).
- Never log the `application_token` (or the raw `auth` block). Log a decision, not the secret.

**OAuth apps** (install / uninstall callbacks), additionally:

- Persist `application_token` alongside the tokens at install time.
- On uninstall, verify `application_token` **before** deleting credentials.
- Treat a missing record as idempotent success (no error — it is already gone).
- Store tokens with restrictive permissions (file mode `0o600` or a datastore with per-tenant isolation).

## See also

- [Webhook handler recipe](https://bitrix24.github.io/b24jssdk/raw/docs/examples/webhook-handler.md) — full outbound-event server these patterns come from.
- [OAuth install recipe](https://bitrix24.github.io/b24jssdk/raw/docs/examples/oauth-install.md) — install/uninstall handshake with per-portal storage.
- [Outbound event registration recipe](https://bitrix24.github.io/b24jssdk/raw/docs/examples/event-registration.md) — `event.bind` / `event.unbind` from the SDK.
- [Logging & Credential Redaction](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/logging.md) — what the SDK strips before request data reaches a logger.

## Sitemap

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