For AI agents: markdown of this page — /docs-content-en/bots/events.md documentation index — /llms.txt
Documentation articles are currently available in English.
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 where it left off after a restart.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
botId (path) |
number | yes | — | Bot ID |
offset (query) |
number | no | from DB | Starting position. If omitted, the value stored in the DB is used. A passed offset acknowledges the events with a lower ID: they are removed from the queue and never arrive again. An offset below the stored value returns events but leaves the stored value unchanged |
limit (query) |
number | no | 100 |
Maximum number of events (1-200). Values outside this range are clamped to the nearest boundary rather than rejected |
withUserEvents (query) |
boolean | no | false |
Include user events (ONIMV2*). Requires subscribing to user events first — the setup steps are described on the User events page. Without that 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 application
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 application
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. For the next request, use nextOffset, not eventId: an offset equal to eventId returns that event again |
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 | true if more unprocessed events remain |
storedOffset |
number | The offset this request ran with: the passed offset, or the value stored in the DB when offset is omitted |
persisted |
boolean | true if lastOffset in the DB advanced: at least one event was delivered and the passed offset is not below the stored value. false — lastOffset did not change: the response is empty or the passed offset is below the stored value |
needsReregistration |
boolean | Optional. Present only as true on an empty response when the platform recorded a Bitrix24 account address change after this diagnostic became available and the bot registration must be replaced. Omitted when events are queued or no marker was recorded. An absent field does not rule out an earlier move |
nextPollAfterMs |
number | How many milliseconds to wait before the next request. On the international platform, this field only reaches a bot whose events are delivered by webhook (eventMode is webhook) — such a bot's Event.get queue is empty by Bitrix24 design. On this platform, 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. A request with withUserEvents=true never carries the field in either mode, so for a webhook bot its absence in that particular response does not mean "keep polling at your current rate" — hold the pause you received on a request without that parameter |
hint |
string | Diagnostic message shown when the events queue stays persistently 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. That accumulation applies to a bot in fetch mode: for a bot whose eventMode is webhook the hint arrives on the very first empty response and says that events went to webhookUrl — A webhook-delivered event. For branching in code, rely on persisted and the presence of events, and treat hint as advisory only |
If an empty events response also contains needsReregistration: true, the platform recorded a Bitrix24 account address change. POST /v1/bots/:botId/resubscribe cannot restore delivery in this state. For a standalone Bot, delete the old registration through DELETE /v1/bots/:botId before reusing the same code, or register a new unique code, then restore the Open Channels and WELCOME_BOT bindings. Repeating POST /v1/bots with the same code may reuse the old identity and does not guarantee a new bot ID. For a bot owned by an Agent or Managed Bot, do not call DELETE /v1/bots/:botId: use the owner resource's recovery lifecycle, or contact support if that recovery is unavailable.
An absent field does not rule out an address change that happened before this diagnostic became available. If the address changed and resubscription did not restore delivery, choose the same owner-aware recovery path.
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": {
"message": { "id": 1501, "text": "Hi, bot!" },
"chat": { "id": 123, "dialogId": "chat123", "type": "chat" },
"user": { "id": 1, "name": "John Brown" }
}
}
],
"nextOffset": 36,
"hasMore": false,
"storedOffset": 35,
"persisted": true
}
}
A persistently empty events queue (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 |
| 409 | BOT_EVENTS_BUSY |
Another poll for this bot is still running. Retry after one second, as indicated by the Retry-After: 1 header |
| 503 | BOT_EVENTS_UNAVAILABLE |
The poll state changed during the request, or the position could not be saved. No events were delivered. Retry after one second, as indicated by the Retry-After: 1 header |
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. To obtain the session_token for an OAuth application, see Keys and authorization.
Plan access. Bot access follows your Bitrix24 account plan. A plan-based rejection is final — stop the polling loop instead of retrying it. Response shape and recovery — Billing and plans.
Server-side offset storage: Vibecode stores lastOffset in the database. Every request without offset reads the stored value. When a request advances the position, the platform saves it first and only then returns the events with persisted: true. If the position cannot be saved, 503 BOT_EVENTS_UNAVAILABLE arrives without events.
One poll at a time. Only one poll runs for a bot at any moment. Wait for the response before sending the next request: an overlapping request receives 409 BOT_EVENTS_BUSY. On 409 and 503, retry after one second, as the Retry-After: 1 header indicates. A polling subsystem failure can return 503 to several bots at once. Polls of different bots do not wait for each other, even when they share a user-event queue.
Exactly-once processing is not guaranteed. The connection can break after the position has already been saved. So your application should guard against processing an event twice, and that guard should take the event type into account.
offset=0: event history cannot be replayed — acknowledged events are removed from the queue. An explicit offset=0 acknowledges nothing and shows the events still waiting for acknowledgement, including those the last request already received — a way to inspect the queue while debugging. Once the bot has received events, offset=0 is below the stored value, so lastOffset does not change, the response carries persisted: false, and the next request without offset continues from the stored value without losing new events.
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. A request with N equal to or above the stored value advances lastOffset the same way a request without offset does.
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 (the conditions under which the field is present, including a request with withUserEvents=true, are in the response-fields table). When it does, wait as long as the field specifies: 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) {
// Declared outside `try`: the pause below reads the response after `catch`.
let data
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 },
})
data = (await res.json()).data
for (const event of data.events ?? []) {
await handleEvent(event)
}
if (data.nextOffset !== undefined) {
offset = data.nextOffset
}
} catch (err) {
console.error('Poll error:', err.message)
}
// A pause from the platform (`nextPollAfterMs`) — the conditions for the field are in the response-fields table.
// This international-platform bot runs in `fetch` mode, so the field never arrives and the loop falls back to its 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()