
## Get events (polling)

`GET /v1/bots/:botId/events`

The primary mechanism for receiving incoming messages and commands. The bot periodically requests new events. The request does not wait for them to appear: the response arrives immediately, with an empty events array when the queue is empty.

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.

The generic `filter` parameter is not applied by this endpoint. By default, the API preserves the previous HTTP 200 response and data, adds `Warning: 299 - "filter is not supported on this endpoint and was ignored"`, and records unsupported-filter use in the server log.

## 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. An explicit `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](/docs/bots/events/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

```bash
curl "https://vibecode.bitrix24.com/v1/bots/42/events?limit=50" \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth application

```bash
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

```javascript
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

```javascript
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 |
|------|-----|---------|
| `success` | boolean | Always `true` on success |
| `data` | object | Batch of events and queue state |
| `data.events` | array | Array of events (see [event types](/docs/bots/events/bot-events)) |
| `data.events[].eventId` | number | Event ID. For the next request, use `nextOffset`, not `eventId`: an `offset` equal to `eventId` returns that event again |
| `data.events[].type` | string | Event code (`ONIMBOTV2MESSAGEADD`, `ONIMBOTV2COMMANDADD`, etc.) |
| `data.events[].date` | string | Event date and time (ISO 8601) |
| `data.events[].data` | object | Event data (structure depends on the type, keys in camelCase) |
| `data.nextOffset` | number | Offset for the next request |
| `data.hasMore` | boolean | `true` if more unprocessed events remain |
| `data.storedOffset` | number | The offset this request ran with: the specified `offset`, or the value stored in the DB when `offset` is omitted |
| `data.persisted` | boolean | `true` if `lastOffset` in the DB advanced: at least one event was delivered and the specified `offset` is not below the stored value. `false` — `lastOffset` did not change: the response is empty or the specified `offset` is below the stored value |
| `data.nextPollAfterMs` | number | How many milliseconds to wait before the next request. Sent only to a bot whose events are delivered by webhook (`eventMode` is `webhook`) — such a bot's `Event.get` queue 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. 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" — keep using the pause you received on a request without that parameter |
| `data.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](/docs/bots/events/webhook). For branching in code, rely on `persisted` and the presence of events, and treat `hint` as advisory only |

If the Bitrix24 account address changed after the bot was registered, polling returns `409 BOT_RECOVERY_IN_PROGRESS` with the `Retry-After: 5` header: the platform automatically restores the bot's registration and Open Channels bindings. Until recovery is complete, every call that changes the bot, including [`POST /v1/bots/:botId/resubscribe`](/docs/bots/management/resubscribe) and `DELETE /v1/bots/:botId`, returns the same error. Keep polling with that delay, and contact support if the recovery does not finish.

An address change made before this diagnostic became available does not produce a `409`. If the address changed and resubscription did not restore delivery, choose the recovery path for the bot's owner type in [Re-subscribe to events](/docs/bots/management/resubscribe).

## Response example

There are new events (`persisted: true`):

```json
{
  "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):

```json
{
  "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:

```json
{
  "success": false,
  "error": {
    "code": "BOT_NOT_FOUND",
    "message": "Bot 999 not found. Register it first via POST /v1/bots."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|---------|
| 200 | `Warning: 299` | The filter is ignored by default; previous data is preserved and the warning is sent in the `Warning` header. |
| 400 (strict mode only) | `UNSUPPORTED_FILTER` | The server mode is disabled by default; until it is enabled, the endpoint preserves HTTP 200 and the previous data. |
| 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 |
| 410 | `BOT_DISABLED` | The bot is disabled and does not process API calls. How to restore it — [Bot disabled](/docs/bots/troubleshooting#bot-disabled-bot_disabled) |
| 401 | `TOKEN_MISSING` | API key has no configured tokens |
| 409 | `BOT_RECOVERY_IN_PROGRESS` | The portal address changed after the bot was registered and the registration is being recovered. No events were delivered. Retry after the delay in the `Retry-After: 5` header |
| 409 | `BOT_EVENTS_BUSY` | An overlapping poll for this bot is still running or has already advanced the stored position. No events were delivered. Retry after one second, as indicated by the `Retry-After: 1` header |
| 409 | `BOT_ID_REPLACED` | The request uses a previous bot ID. Use the current ID from `error.details.botId`. No `Retry-After` header |
| 409 | `BOT_UNAVAILABLE` | The bot's stored policy or state must be repaired on the platform side. No events were delivered. There is no `Retry-After` header and retrying will not help. Contact support and quote the request time |
| 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 |
| 500 | `INTERNAL_ERROR` | A transient internal error. Do not process events or change `offset`; retry after 5 seconds, doubling the delay up to 60 seconds. Reset the delay after success |

Full list of common API errors — [Errors](/docs/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](/docs/keys-auth).

**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](/docs/errors/billing).

**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` is returned without events.

**One poll at a time.** Wait for the response before sending the next request. When polls for one bot overlap, only one of them advances the stored position and returns those events with `persisted: true`. An overlapping poll receives `409 BOT_EVENTS_BUSY` with no events. A poll that does not advance the position (an explicit `offset` below the stored value, or an empty queue) may return `200` instead. An overlapping request may reach Bitrix24 before it is refused, and it counts against the Bitrix24 request limit. For `429 RATE_LIMITED` and `429 CALLER_RATE_LIMITED`, wait for the delay in `Retry-After`. Among `409` / `503` responses, retry only `BOT_EVENTS_BUSY` and `BOT_EVENTS_UNAVAILABLE` after `Retry-After: 1`, or `BOT_RECOVERY_IN_PROGRESS` after `Retry-After: 5`. Other `409` responses are terminal for this polling loop: stop it and address the error. For `BOT_ID_REPLACED`, update the request path using `error.details.botId`, then restart; for `BOT_UNAVAILABLE`, contact support. Other policy or authorization refusals require repairing their cause before a new loop starts. Do not infer a retry delay when no `Retry-After` is returned. Polls of different bots do not wait for each other, even when they share a user-event queue.

For `500 INTERNAL_ERROR`, retry with the same `offset` after an increasing 5–60 second delay without `Retry-After`; reset the delay after a successful response. This is separate from the `Retry-After` rule for `429` / `409` / `503`.

**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 — a way to inspect the queue while debugging. Events received by a request without `offset` are acknowledged right away, so an `offset=0` response no longer includes them. 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 goes to sleep along with the bot process. Turn it off with a [`PATCH /v1/infra/servers/:id/sleep`](/docs/infra/lifecycle/sleep) call passing `sleepAfterMinutes: null`.

**Polling loop (ready-to-use example):**

```javascript
const BOT_ID = 42
const API_KEY = 'YOUR_API_KEY'
const BASE = 'https://vibecode.bitrix24.com/v1'

async function pollEvents() {
  let offset = undefined
  let backoffMs = 5000

  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 },
      })
      const payload = await res.json()
      if (!res.ok) {
        if (res.status === 500 && payload.error?.code === 'INTERNAL_ERROR') {
          await new Promise(r => setTimeout(r, backoffMs))
          backoffMs = Math.min(backoffMs * 2, 60000)
          continue
        }
        const retryable =
          (res.status === 409 && ['BOT_EVENTS_BUSY', 'BOT_RECOVERY_IN_PROGRESS'].includes(payload.error?.code)) ||
          (res.status === 503 && payload.error?.code === 'BOT_EVENTS_UNAVAILABLE') ||
          (res.status === 429 && ['RATE_LIMITED', 'CALLER_RATE_LIMITED'].includes(payload.error?.code))
        const retryAfter = Number(res.headers.get('Retry-After'))
        if (retryable && Number.isFinite(retryAfter) && retryAfter > 0) {
          data = { nextPollAfterMs: retryAfter * 1000 }
        } else {
          console.error('Polling stopped:', payload.error)
          return
        }
      } else {
        data = payload.data
        backoffMs = 5000
      }

      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.
    // A bot in `fetch` mode never receives the field, so 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()
```

## See also

- [Troubleshooting](/docs/bots/troubleshooting)
- [Bot events](/docs/bots/events/bot-events)
- [User events](/docs/bots/events/user-events)
- [Send message](/docs/bots/messages/send)
- [Reply to a command](/docs/bots/commands/answer)
- [Configure auto-sleep](/docs/infra/lifecycle/sleep)
- [Bot platform](/docs/bots)
- [Limits and optimization](/docs/optimization)
