
## 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](/docs/bots/events/user-events) (ONIMV2*). Requires a prior subscription — the setup order is described on the User events page. Without a subscription the request returns `502 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 app

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

```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 |
|------|-----|---------|
| `events` | array | Array of events (see [event types](/docs/bots/events/bot-events)) |
| `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 |
| `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 code-logic branches rely on `persisted` and the presence of events, and treat `hint` as a hint |

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

```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 |
|------|-----|---------|
| 400 | `INVALID_BOT_ID` | `botId` is not a number |
| 404 | `BOT_NOT_FOUND` | No bot with this ID found |
| 403 | `BOT_ACCESS_DENIED` | Bot belongs to another API key |
| 502 | `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](/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`. Obtaining the `session_token` for an OAuth app — see [Keys and authorization](/docs/keys-auth).

**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 itself appears 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.

**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

  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)
    }

    await new Promise(r => setTimeout(r, 3000))
  }
}

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 a message](/docs/bots/messages/send)
- [Reply to a command](/docs/bots/commands/answer)
- [Bot platform](/docs/bots)
- [Limits and optimization](/docs/optimization)
