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. - OAuth apps — a marketplace app that handles
ONAPPINSTALL/ONAPPUPDATE/ONAPPUNINSTALLcallbacks and stores per-portal credentials, as in the OAuth install recipe.
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.
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). 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 as the durable channel.
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
}
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 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):
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:
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_tokentravels in the request body, so plain HTTP would expose it in transit. - Reply
2xxbefore verifying or processing (Pattern 1). - Require an
application_tokenenv var and fail closed if it is unset. - Compare the incoming
application_tokenwithsafeEqual(Pattern 2), never===. - Drop — do not error — on a mismatch (return the
2xxyou already sent; don't reveal which check failed). - Never log the
application_token(or the rawauthblock). Log a decision, not the secret.
OAuth apps (install / uninstall callbacks), additionally:
- Persist
application_tokenalongside the tokens at install time. - On uninstall, verify
application_tokenbefore deleting credentials. - Treat a missing record as idempotent success (no error — it is already gone).
- Store tokens with restrictive permissions (file mode
0o600or a datastore with per-tenant isolation).
See also
- Webhook handler recipe — full outbound-event server these patterns come from.
- OAuth install recipe — install/uninstall handshake with per-portal storage.
- Outbound event registration recipe —
event.bind/event.unbindfrom the SDK. - Logging & Credential Redaction — what the SDK strips before request data reaches a logger.
Discovering v3 methods
Use rest.documentation.openapi to fetch the portal's own machine-readable list of every available REST API v3 method — the source of truth the SDK relies on instead of a hardcoded allowlist. Especially useful for AI agents and codegen.
Call
A method for making Bitrix24 REST API version 2 calls.