
## Event delivered by webhook

New OAuth bots support `fetch` and `webhook`. The platform verifies bot ownership using the same OAuth application and an exact match on the bot ID and code, without sending `botToken` to the OAuth API. Delivery to `/api/bot/webhook` requires a recent, verified INSTALL or placement event from the bound application; otherwise registration returns `409 BOT_APP_EVENT_ENROLLMENT_REQUIRED`. A previously issued application token does not replace this verification. Events received before initial registration completes are retained in a bounded encrypted inbox. Bot-token changes for new application-owned bots are refused (`409 BOT_OAUTH_POLICY_INVALID`); unmarked legacy bots are not automatically converted. A standalone OAuth key without a bound application can use `fetch` or its own receiver, but is not trusted by the platform receiver.

The platform receiver is available to Agent and ManagedBot bots with a verified owner. An application bot without a verified owner receives `409 BOT_APP_EVENT_CONSUMER_REQUIRED` before any data is written to Bitrix24. It can use `fetch` or its own callback. Events for an already published bot after a Bitrix24 account address change may remain in encrypted quarantine until the recovered identity is verified and published. Delivery requires an unchanged callback binding, continued eligibility of the application, author and owner membership, and verified authority to deliver the recovered events. If that basis changes or cannot be verified, held events are permanently discarded while bot identity recovery continues. Bounded retention does not guarantee delivery. Each recovery can retain at most 256 events and 4 MiB of ciphertext for 72 hours. A duplicate event does not extend retention. On overflow, the receiver returns `503` without acknowledging receipt.

This page describes the body of an event Bitrix24 sends directly to a bot, the fields it contains, and how the handler verifies the request's authenticity.

## 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 events as POST requests to `webhookUrl`. Delivery to your own handler goes directly to that handler without involving Vibecode. When the destination is the platform receiver `/api/bot/webhook`, the platform authenticates and accepts events for onward delivery.

Polling returns no events for a bot in this mode: [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`. The response also includes `nextPollAfterMs`, except when `withUserEvents=true`.

See [Troubleshooting](/docs/bots/troubleshooting) for address requirements, the response timeout, the lack of guaranteed redelivery, and a comparison of the two modes.

## Request body

The body format is `application/x-www-form-urlencoded`; nesting is expressed with square brackets. The examples below show only the fields used on this page. 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, including numbers and booleans, arrive as strings because of the body format. Convert `data.message.id` to a number before comparing it.

## Authenticity verification

The `webhookUrl` address is reachable from the internet, so the handler must distinguish between events from your Bitrix24 account and other requests. Check `auth[application_token]`; its expected value depends on the bot registration contract used with [POST /v1/bots](/docs/bots/management/create).

| Registration class | Expected `auth[application_token]` |
|------------------|------------------------------------|
| Personal key `vibe_api_…` | The string `custom` followed by the bot token |
| OAuth bot registered under the previous contract | The string `custom` followed by the bot token |
| New application-owned OAuth bot | Top-level `auth.application_token` from a recent, verified application enrollment; not `botToken` |

The custom-token instructions below apply to personal-key bots using an inbound webhook and OAuth bots registered under the previous contract. Existing legacy bots retain that contract; using an OAuth key does not convert them into new application-owned bots. For new application-owned bots, the receiver below verifies the top-level application token from trusted server-side configuration; nested `bot.auth` does not confirm enrollment. The platform's ownership check during registration separately verifies the bot ID and code.

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.

For a personal-key bot using an inbound webhook or an OAuth bot registered under the previous contract, the same custom-token value arrives either at the top level in `auth` or nested in `data[bot][auth]` — check both places. If it does not match, respond with `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

### Personal key or previous-contract OAuth bot

This example uses the custom bot token for a personal-key bot with an inbound webhook or an OAuth bot registered under the previous contract. For a new application-owned OAuth bot, use the separate example below.

```javascript
import express from 'express'

const BOT_TOKEN = process.env.BOT_TOKEN // token set via PATCH fields.botToken
if (!BOT_TOKEN) {
  throw new Error('Set BOT_TOKEN')
}
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)

  // Respond before processing: Bitrix24 waits only a limited time for a response
  // and does not guarantee 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)
```

### New application-owned OAuth bot

This minimal receiver verifies the request's authenticity. Run it separately from the custom bot-token example. Set `APPLICATION_TOKEN` to the registered application token from a verified application enrollment and keep it in trusted server-side configuration. Never take it from the incoming request. The receiver compares only the top-level `auth.application_token` with that configured value. The application is the stable identity: numeric `data.bot.id` is event data used for routing, not an authentication pin. The simplified event body above does not need `data.bot.code`.

After `409 BOT_ID_REPLACED`, use `error.details.botId` for subsequent Bot API calls. The receiver continues accepting events authenticated by the same configured application token when the remote bot ID changes; no receiver configuration update is required before quarantine release. Never replace the trusted application token with a value learned from an incoming event.

```javascript
import express from 'express'

const APPLICATION_TOKEN = process.env.APPLICATION_TOKEN
if (!APPLICATION_TOKEN) {
  throw new Error('Set APPLICATION_TOKEN')
}

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

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

  res.sendStatus(200)
})

app.listen(3000)
```

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

## 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)
- [Recover a bot registration](/docs/bots/management/registrations-recover)
- [Re-subscribe to events](/docs/bots/management/resubscribe)
- [Send message](/docs/bots/messages/send)
- [Troubleshooting](/docs/bots/troubleshooting)
- [Bot platform](/docs/bots)
