
## Event delivered by webhook

What the body of an event Bitrix24 sends straight to the bot looks like, which fields it carries, and how the handler verifies that the request is genuine.

## When such an event arrives

The delivery mode is set when the bot is registered — [POST /v1/bots](/docs/bots/management/create) with `eventMode: "webhook"` and the `webhookUrl` address. Bitrix24 sends every event as a POST request directly to that address; the Vibecode platform takes no part in delivery.

Polling stays empty for such a bot: [GET /v1/bots/:botId/events](/docs/bots/events/polling) returns an empty `events` array. While that array is empty, the response also carries a `hint` saying that events go to `webhookUrl`. Alongside it comes `nextPollAfterMs`, which is absent from the response to a request with `withUserEvents=true`.

Address requirements, the response timeout, the absence of redelivery, and a comparison of the two modes — [Troubleshooting](/docs/bots/troubleshooting).

## Request body

The body format is `application/x-www-form-urlencoded`; nesting is expressed with square brackets. Below, a "new message to the bot" event is split across lines for readability; in the request it is a single line with no breaks.

```
event=ONIMBOTV2MESSAGEADD
&data[bot][id]=42
&data[message][id]=1501
&data[message][text]=Hello%2C+bot
&data[message][params][FILE_ID][0]=15423
&data[chat][id]=123
&data[chat][dialogId]=chat123
&data[chat][type]=chat
&data[user][id]=1
&auth[application_token]=customFAKE-bot-token-0123456789abcdef01
&auth[domain]=your-portal.bitrix24.com
&auth[member_id]=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
```

The parsed body is an object with three top-level keys:

```json
{
  "event": "ONIMBOTV2MESSAGEADD",
  "data": {
    "bot": { "id": "42" },
    "message": {
      "id": "1501",
      "text": "Hello, bot",
      "params": { "FILE_ID": ["15423"] }
    },
    "chat": { "id": "123", "dialogId": "chat123", "type": "chat" },
    "user": { "id": "1" }
  },
  "auth": {
    "application_token": "customFAKE-bot-token-0123456789abcdef01",
    "domain": "your-portal.bitrix24.com",
    "member_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
  }
}
```

## Body fields

| Field | Description |
|------|---------|
| `event` | Event type code. The same set of codes as in polling — [Bot events](/docs/bots/events/bot-events) |
| `data` | Event data. Mirrors the `data` object of the same event in polling; values arrive as strings |
| `data.bot.id` | Bot ID. The same value goes into the path when replying — `POST /v1/bots/:botId/messages` |
| `data.chat.dialogId` | Dialog ID to reply to: `chatXXX` for a group chat, the user ID for a private one |
| `data.message.id` | Message ID — for quoting via `replyId` |
| `data.message.params.FILE_ID` | Array of attached file IDs. To download — [GET /files/:fileId](/docs/bots/files/download) |
| `auth.application_token` | Value used to verify authenticity — see the section below |
| `auth.domain` | Address of the Bitrix24 account the event came from |
| `auth.member_id` | Bitrix24 account identifier |

All field values arrive as strings — numbers and booleans too, which follows from the body format. So compare `data.message.id` after converting it to a number, not directly.

## Authenticity verification

The `webhookUrl` address is reachable from the internet, so the handler must tell an event from your Bitrix24 account apart from an outside request. The marker is the `auth[application_token]` field, and the value to expect depends on which key registered the bot through [POST /v1/bots](/docs/bots/management/create).

| Registration key | Expected `auth[application_token]` |
|------------------|------------------------------------|
| Personal key `vibe_api_…` | The string `custom` followed by the bot token |
| Key bound to an OAuth application | That application's `application_token` — the same one the application verifies [subscription events](/docs/infra/event-subscriptions/handler) against |

The rest of this section covers the first case. For the second, compare against the application's `application_token`; the bot token plays no part in that check.

The platform issues the bot token itself at registration and never shows it in responses. To get a value to compare against, set your own token — [PATCH /v1/bots/:botId](/docs/bots/management/update) with the `fields.botToken` field: 32 to 40 characters from the `[A-Za-z0-9_-]` alphabet. A value outside these bounds is rejected with `400 BOT_TOKEN_INVALID`, and the bot token stays unchanged.

The same value arrives either at the top level in `auth` or nested in `data[bot][auth]` — check both places. If it does not match, respond `403` and do not process the event.

The bot token is a secret: it also authorizes incoming events. Store it next to the API key and do not publish it in client-side code.

## Handler

Below is the application's own code, not a call to the Vibecode API, so there is no cURL example here. The example is written for a bot registered with a personal key, so the expected value is built from the bot token. If the bot was registered with an OAuth application's key, put that application's `application_token` into `EXPECTED_TOKEN` instead.

```javascript
import express from 'express'

const BOT_TOKEN = process.env.BOT_TOKEN // token set via PATCH fields.botToken
const EXPECTED_TOKEN = 'custom' + BOT_TOKEN
const VIBE = 'https://vibecode.bitrix24.com/v1'

const app = express()
app.use(express.urlencoded({ extended: true }))

app.post('/bot-events', (req, res) => {
  const incoming =
    req.body.auth?.application_token ?? req.body.data?.bot?.auth?.application_token
  if (incoming !== EXPECTED_TOKEN) return res.sendStatus(403)

  // The response goes out before processing: Bitrix24 waits a limited time for it,
  // and the event has no redelivery.
  res.sendStatus(200)
  handleEvent(req.body).catch((err) => console.error('Handler error:', err.message))
})

async function handleEvent({ event, data }) {
  if (event !== 'ONIMBOTV2MESSAGEADD') return
  // The bot's own reply comes back — without this check the handler loops.
  if (String(data.message?.authorId) === String(data.bot?.id)) return

  await fetch(`${VIBE}/bots/${data.bot.id}/messages`, {
    method: 'POST',
    headers: { 'X-Api-Key': process.env.VIBE_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      dialogId: data.chat.dialogId,
      fields: { message: `Received: ${data.message.text}` },
    }),
  })
}

app.listen(3000)
```

Routing is driven by the `event` field; the set of types is the same as in polling. Full filtering of incoming messages, including how to skip system ones, and the `data` structure of each type — [Bot events](/docs/bots/events/bot-events).

## See also

- [Get events (polling)](/docs/bots/events/polling)
- [Bot events](/docs/bots/events/bot-events)
- [Register a bot](/docs/bots/management/create)
- [Update a bot](/docs/bots/management/update)
- [Send message](/docs/bots/messages/send)
- [Troubleshooting](/docs/bots/troubleshooting)
- [Bot platform](/docs/bots)
