For AI agents: markdown of this page — /docs-content-en/infra/event-subscriptions/handler.md documentation index — /llms.txt

Application-side handler

Receiving a delivered event in the application on a Black Hole server. The platform delivers each event as a POST request to the application subdomain at the appPath path in application/x-www-form-urlencoded format — this is the standard Bitrix24 event body. The handler reads event and data, checks auth[application_token] against its own application_token, and responds 2xx. The platform retries delivery on a non-2xx response and wakes a sleeping server.

javascript
// Express handler: Bitrix24 form fields arrive in req.body
import express from 'express'

const APP_TOKEN = process.env.APPLICATION_TOKEN // application_token of the app
const app = express()
app.use(express.urlencoded({ extended: true }))

app.post('/api/webhooks/b24', (req, res) => {
  // Verify the app token — events without a valid token are rejected
  if (req.body.auth?.application_token !== APP_TOKEN) {
    return res.status(403).json({ ok: false })
  }

  const event = req.body.event              // e.g. "ONTASKADD"
  const taskId = req.body.data?.FIELDS?.ID  // Entity ID from the event

  // A repeated event must not run twice — the platform may deliver it again
  console.log(`Event ${event}, task #${taskId}`)

  // Respond 2xx, otherwise the platform will retry delivery
  res.status(200).json({ ok: true })
})

Event body

Example of the body that arrives in the handler:

event=ONTASKADD&data[FIELDS][ID]=20152&ts=1780843200&auth[access_token]=<Bitrix24 token>&auth[refresh_token]=<Bitrix24 refresh token>&auth[application_token]=<app application_token>&auth[domain]=your-portal.bitrix24.com&auth[member_id]=...

The body carries the Bitrix24 user tokens. The handler checks auth[application_token] to reject foreign events. auth[access_token] is a Bitrix24 token for direct Bitrix24 REST calls. It does not work as a V1 API key directly, but it can be exchanged for a session token — this is the second authorization method below.

Authorizing V1 API calls from the handler

The handler often calls the V1 API in response to an event — reading a related entity or creating a new one. The platform delivers the event itself and does not add authorization headers: the request carries only Content-Type and the event body. The handler sets the call authorization itself. There are two working methods — the choice depends on whose key the calls should run under.

Method 1. Personal key `vibe_api_`

The shortest path: a personal key in the X-Api-Key header, no session needed. Calls run on behalf of the key owner. Suitable when they do not have to run under the same application as the subscription.

javascript
app.post('/api/webhooks/b24', async (req, res) => {
  if (req.body.auth?.application_token !== APP_TOKEN) {
    return res.status(403).json({ ok: false })
  }

  const taskId = req.body.data?.FIELDS?.ID

  // V1 API call — personal key in the X-Api-Key header
  const task = await fetch(
    `https://vibecode.bitrix24.com/v1/tasks/${taskId}`,
    { headers: { 'X-Api-Key': process.env.VIBE_API_KEY } },
  ).then((r) => r.json())

  console.log('Task from event:', task.data)

  res.status(200).json({ ok: true })
})

A personal vibe_api_ key is created in your account — the creation procedure is described in Keys and authorization. Bitrix24 scopes are attached to the key at issue time: mark the data groups the handler accesses (for the tasks example — tasks).

Method 2. Authorization key `vibe_app_` under the same app

If calls must run under the same authorization key as the subscription, that is, on behalf of the account user, the handler needs a session. The event body carries this user's tokens: auth[access_token], auth[member_id], auth[domain], and auth[refresh_token]. They are exchanged for a vibe_session_ session token via POST /v1/oauth/placement-session with the body { app_key, access_token, member_id, domain, refresh_token }. Pass the resulting session in Authorization: Bearer alongside X-Api-Key:

Terminal
curl https://vibecode.bitrix24.com/v1/tasks/20152 \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"

How to obtain a session from event tokens — the Application on your own server section in Keys and authorization. The session lives for 24 hours, with no renewal.

Common authorization mistakes

Three header combinations that will not work, and the response code for each:

What was passed Response Reason
Authorization key vibe_app_ in X-Api-Key without Bearer 401 TOKEN_MISSING The key itself has no account tokens — you need a session in Authorization: Bearer alongside X-Api-Key (Method 2)
Authorization key vibe_app_ in Authorization: Bearer 401 WRONG_AUTH_SCHEME Bearer carries a vibe_session_ session token, not the app key
The auth[access_token] token from the event in Authorization: Bearer 401 MISSING_API_KEY This is a Bitrix24 token, not a V1 key. In Bearer the V1 API accepts only vibe_ tokens — a Bitrix24 token is first exchanged for a session (Method 2)

A detailed breakdown of TOKEN_MISSINGBot platform troubleshooting.

Known specifics

  • Delivery goes through the tunnel — the application needs no public URL. The platform registers the handler on its side, Bitrix24 sends the event there, and the platform delivers it to the application through the Black Hole tunnel with an auth[application_token] check. The application needs no public address of its own.
  • A sleeping server wakes up for delivery. If the server is asleep at the moment of the event, the platform wakes it and delivers the event after the tunnel connects. So the first event after idle time arrives with a wake-up delay.
  • Delivery is reliable, but not infinite. Failed deliveries are retried with a growing delay. After attempts are exhausted, delivery moves to FAILED, the subscription is marked DEGRADED, and the owner receives a notification. Delivery statuses are visible in GET /v1/infra/servers/:id/event-subscriptions.
  • A repeated delivery must not duplicate the effect. The same event may arrive again. Process it so that a second receipt with the same data[FIELDS][ID] does not do the work twice.

See also