Para agentes de IA: markdown desta página — /docs-content-en/bots/events.md índice da documentação — /llms.txt
Os artigos da documentação estão disponíveis atualmente em inglês.
Events
Receive incoming bot events by polling and process them: new messages, commands, reactions, and the bot being added to a chat.
Scope: imbot | Base URL: https://vibecode.bitrix24.com/v1 | Authorization: X-Api-Key
Get events (polling)
GET /v1/bots/:botId/events
The primary mechanism for receiving incoming messages and commands. The bot periodically requests new events.
Vibecode stores lastOffset in the database — on the first request without offset, the stored value is used. This lets the bot resume from where it stopped after a restart.
Parameters
| Parameter | Type | Req. | Default | Description |
|---|---|---|---|---|
botId (path) |
number | yes | — | Bot ID |
offset (query) |
number | no | from DB | Starting position. Without the parameter — the value stored in the DB. offset=0 — start from the beginning |
limit (query) |
number | no | 100 |
Maximum number of events (1-1000) |
withUserEvents (query) |
boolean | no | false |
Include user events (ONIMV2*). Requires a prior subscription — the setup steps are described on the User events page. Without a subscription, the request returns 422 BITRIX_ERROR: User is not subscribed |
Examples
curl — personal key
curl "https://vibecode.bitrix24.com/v1/bots/42/events?limit=50" \
-H "X-Api-Key: YOUR_API_KEY"
curl — OAuth app
curl "https://vibecode.bitrix24.com/v1/bots/42/events?limit=50" \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Authorization: Bearer USER_SESSION_TOKEN"
JavaScript — personal key
const res = await fetch('https://vibecode.bitrix24.com/v1/bots/42/events?limit=50', {
headers: {
'X-Api-Key': 'YOUR_API_KEY',
},
})
const { success, data } = await res.json()
console.log('Events:', data.events.length, 'More:', data.hasMore)
JavaScript — OAuth app
const res = await fetch('https://vibecode.bitrix24.com/v1/bots/42/events?limit=50', {
headers: {
'X-Api-Key': 'YOUR_APP_KEY',
'Authorization': 'Bearer USER_SESSION_TOKEN',
},
})
const { success, data } = await res.json()
Response fields
| Field | Type | Description |
|---|---|---|
events |
array | Array of events (see event types) |
events[].eventId |
number | Event ID — pass it as offset in the next request |
events[].type |
string | Event code (ONIMBOTV2MESSAGEADD, ONIMBOTV2COMMANDADD, etc.) |
events[].date |
string | Event date and time (ISO 8601) |
events[].data |
object | Event data (structure depends on the type, keys in camelCase) |
nextOffset |
number | Offset for the next request |
hasMore |
boolean | There are more unprocessed events |
storedOffset |
number | Current offset stored in the DB |
persisted |
boolean | true if lastOffset in the DB advanced (at least one event delivered). false — the response is empty, the cursor did not change |
nextPollAfterMs |
number | How many milliseconds to wait before the next request. Only reaches a bot whose events are delivered by webhook (eventMode is webhook) — the Event.get queue of such a bot is empty by Bitrix24 design. A bot in fetch mode never receives the field: an absent field means "keep polling at your current rate", so it never arrives empty or zero. When hasMore is true, drain the queue without waiting out the pause — it applies to the next empty poll |
hint |
string | Diagnostic message shown when the queue stays empty — points to the installation state on the Bitrix24 side. The empty-response counter is updated periodically rather than on every request, so the hint appears after several empty periods rather than strictly on the fifth request (at the recommended 2–5 second polling interval — after roughly a couple of minutes of continuously empty polling). The number N in the text is the count of observed empty periods, not the exact number of requests made. For branching in code, rely on persisted and the presence of events, and treat hint as advisory only |
Response example
There are new events (persisted: true):
{
"success": true,
"data": {
"events": [
{
"eventId": 35,
"type": "ONIMBOTV2MESSAGEADD",
"date": "2026-04-13T17:15:00+00:00",
"data": {
"dialogId": "chat123",
"message": { "id": 1501, "text": "Hi, bot!" },
"user": { "id": 1, "name": "John Brown" }
}
}
],
"nextOffset": 36,
"hasMore": false,
"storedOffset": 35,
"persisted": true
}
}
Queue stays empty (the hint field appears, the number in the text is the count of observed empty periods, not requests made):
{
"success": true,
"data": {
"events": [],
"nextOffset": 36,
"hasMore": false,
"storedOffset": 36,
"persisted": false,
"hint": "Events queue has stayed empty across 7 consecutive checks (the empty-poll counter is sampled periodically, not once per request, so this reflects sustained emptiness rather than the exact number of polls). Bot config: eventMode='fetch', code='support_bot'. If the bot is alive on B24 (chats receive messages, im.bot.list lists it) the most common cause is B24-side event-subscription decay — run POST /v1/bots/42/resubscribe first (lightweight, preserves openline / WELCOME_BOT bindings). If that does not help, verify: (1) eventMode is 'fetch' (current: 'fetch'); (2) your OAuth app's INSTALL event handler responded 200 to Bitrix24; (3) the bot was added to a chat where messages are being sent (it must be a participant for chat-message events); (4) only if all of the above are confirmed — try POST /v1/bots to re-register (destroys openline bindings; last resort)."
}
}
Error response example
404 — bot not found:
{
"success": false,
"error": {
"code": "BOT_NOT_FOUND",
"message": "Bot 999 not found. Register it first via POST /v1/bots."
}
}
Errors
| HTTP | Code | Description |
|---|---|---|
| 400 | INVALID_BOT_ID |
botId is not a number |
| 404 | BOT_NOT_FOUND |
No bot found with this ID |
| 403 | BOT_ACCESS_DENIED |
Bot belongs to another API key |
| 422 | BITRIX_ERROR |
Bitrix24 error (error text in message) |
| 403 | SCOPE_DENIED |
API key lacks the imbot scope |
| 401 | TOKEN_MISSING |
API key has no configured tokens |
Full list of common API errors — Errors.
Known specifics
Which token to use. For a personal key vibe_api_… the X-Api-Key header is enough. For an authorization key vibe_app_… you must add Authorization: Bearer <session_token> — without Bearer the request returns 401 TOKEN_MISSING. Obtaining the session_token for an OAuth app — see Keys and authorization.
Plan access on the international instance. Bot access on .com follows the Bitrix24 plan model.
For plan-related denials and recovery, see plan errors.
Server-side offset storage: Vibecode stores lastOffset in the database. On the first request without offset, the stored value is used. After events are received, lastOffset is updated automatically, without waiting for the write result.
offset=0: explicitly passing offset=0 starts from the beginning of history — for debugging or an initial load.
offset=N (a specific value): the event with the given ID is itself included in the response. To avoid duplicates, always pass nextOffset from the previous response, not the eventId of the last processed event.
Request chain:
GET /events → { nextOffset: 42, hasMore: true }
GET /events?offset=42 → { nextOffset: 55, hasMore: false }
GET /events?offset=55 → { events: [], hasMore: false }
Recommended polling interval: 2-5 seconds between requests — when the platform sends no nextPollAfterMs. When it does, wait as long as it asks: the value may change between platform releases, so clamp it with your own bounds instead of relying on a specific number.
Polling does not keep the machine online. If the loop below runs on a Black Hole server, disable auto-sleep on it. The idle timer counts inbound requests to the server, while polling only makes outbound calls — from the timer's point of view the server is idle, and after an hour it falls asleep together with the bot process. Turn it off with a PATCH /v1/infra/servers/:id/sleep call passing sleepAfterMinutes: null.
Polling loop (ready-to-use example):
const BOT_ID = 42
const API_KEY = 'YOUR_API_KEY'
const BASE = 'https://vibecode.bitrix24.com/v1'
async function pollEvents() {
let offset = undefined
while (true) {
try {
const url = new URL(`${BASE}/bots/${BOT_ID}/events`)
if (offset !== undefined) url.searchParams.set('offset', String(offset))
const res = await fetch(url, {
headers: { 'X-Api-Key': API_KEY },
})
const { data } = await res.json()
for (const event of data.events ?? []) {
await handleEvent(event)
}
if (data.nextOffset !== undefined) {
offset = data.nextOffset
}
} catch (err) {
console.error('Poll error:', err.message)
}
// The platform may ask you to poll less often (`nextPollAfterMs`); no field — your own interval.
await new Promise(r => setTimeout(r, Math.min(data?.nextPollAfterMs ?? 3000, 3600000)))
}
}
async function handleEvent(event) {
const { data } = event
switch (event.type) {
case 'ONIMBOTV2MESSAGEADD':
// Reply to the message
await fetch(`${BASE}/bots/${BOT_ID}/messages`, {
method: 'POST',
headers: { 'X-Api-Key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
dialogId: data.chat.dialogId,
fields: { message: `Received: ${data.message.text}` },
}),
})
break
case 'ONIMBOTV2COMMANDADD':
// Reply to the command
await fetch(`${BASE}/bots/${BOT_ID}/commands/${data.command.id}/answer`, {
method: 'POST',
headers: { 'X-Api-Key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
dialogId: data.chat.dialogId,
messageId: data.message.id,
fields: { message: `Command /${data.command.command}: ${data.command.params}` },
}),
})
break
}
}
pollEvents()