> ## Documentation Index
> Fetch the complete documentation index at: https://docs.12m.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Events stream

> Cursor-based stream of every customer-observable event on an identity. Long-poll up to 25 seconds.

Same data as the webhooks stream, served pull-mode for environments
that can't expose a public URL — agents, MCP servers, dev environments
behind a firewall. Each event carries the same `seq` and `id` as the
matching webhook delivery, so you can mix push and pull and dedup
trivially.

## The cursor

`seq` is a monotonic per-identity integer. It strictly increases and is
never reused. Pass `cursor` from the response back as `since` on the
next call:

```js theme={null}
let cursor = 0;
while (running) {
  const r = await fetch(
    `https://api.inboxbase.ai/v1/identities/${handle}/events?since=${cursor}&timeoutMs=25000`,
    { headers: { Authorization: `Bearer ${KEY}` } },
  ).then((r) => r.json());
  for (const ev of r.events) await handle(ev);
  cursor = r.cursor;
}
```

The same event is never returned twice on the same connection.

## Long-poll

`timeoutMs` is the long-poll window, capped at 25 seconds. With
long-poll on, the call returns as soon as a new event lands; if
nothing arrives within the window, you get an empty response and you
call again immediately.

The loop above doesn't busy-wait — the long-poll absorbs idle time.

## When `hasMore: true`

If your `limit` was smaller than the number of events available beyond
your `since` cursor, `hasMore` is `true` on the response. Loop without
long-polling until `hasMore: false`, then resume long-polling from there.

## Recovery from missed webhooks

If you also use webhooks and your receiver was down, this endpoint is
how you backfill. Walk from the last `seq` you persisted; both delivery
modes share the cursor.

```js theme={null}
let cursor = await db.getLastWebhookSeq(handle);
for (;;) {
  const r = await fetch(
    `https://api.inboxbase.ai/v1/identities/${handle}/events?since=${cursor}&limit=200`,
    { headers: { Authorization: `Bearer ${KEY}` } },
  ).then((r) => r.json());
  for (const ev of r.events) await handle(ev);
  cursor = r.cursor;
  if (!r.hasMore) break;
}
await db.setLastWebhookSeq(handle, cursor);
```

See [Reacting to replies](/guides/reacting-to-replies) for the full
push/pull pattern.


## OpenAPI

````yaml GET /v1/identities/{handle}/events
openapi: 3.1.0
info:
  title: inboxbase.ai API
  version: 1.0.0
  description: >-
    Managed email infrastructure for outbound. Send, reply, and read threads
    through one API; we run the mailbox pool, rotation, warmup, and
    deliverability under the hood.
  contact:
    name: inboxbase.ai support
    email: support@inboxbase.ai
    url: https://inboxbase.ai
servers:
  - url: https://api.inboxbase.ai
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Send
    description: Sending email.
  - name: Conversations
    description: Reading threads.
  - name: Events
    description: Pull-mode event stream.
paths:
  /v1/identities/{handle}/events:
    get:
      tags:
        - Events
      summary: Pull events
      description: >-
        Cursor-based read of every customer-observable event on an identity.
        Optional long-poll up to 25 seconds. Same `seq` and `id` as the matching
        webhook deliveries.
      operationId: listEvents
      parameters:
        - $ref: '#/components/parameters/Handle'
        - name: since
          in: query
          description: >-
            Cursor. Returns events with `seq > since`. Default `0` walks the
            whole log.
          schema:
            type: integer
            format: int64
            default: 0
        - name: types
          in: query
          description: CSV of event types to filter on.
          schema:
            type: string
            example: email.replied,email.no_reply
        - name: limit
          in: query
          description: Maximum events per response, 1..200.
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: timeoutMs
          in: query
          description: >-
            Long-poll window. Hold the connection up to this many milliseconds
            for new events when the response would otherwise be empty. Capped at
            25,000.
          schema:
            type: integer
            minimum: 0
            maximum: 25000
            default: 0
      responses:
        '200':
          description: Events page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  parameters:
    Handle:
      name: handle
      in: path
      required: true
      description: Identity handle, URL-encoded.
      schema:
        type: string
        example: alice.acme@inboxbase.ai
  schemas:
    EventsResponse:
      type: object
      required:
        - identity
        - events
        - cursor
        - hasMore
      properties:
        identity:
          type: string
        events:
          type: array
          items:
            $ref: '#/components/schemas/IdentityEvent'
        cursor:
          type: integer
          format: int64
          description: Last `seq` returned. Pass back as `since` on the next call.
        hasMore:
          type: boolean
          description: '`true` when more events were available beyond `limit`.'
    IdentityEvent:
      type: object
      required:
        - seq
        - id
        - type
        - ts
        - data
      properties:
        seq:
          type: integer
          format: int64
          description: Monotonic per-identity cursor.
        id:
          type: string
          description: >-
            Globally-unique event id (`evt_...`). Same id as the matching
            webhook delivery.
        type:
          type: string
          enum:
            - email.sent
            - email.received
            - email.replied
            - email.no_reply
        convId:
          type:
            - string
            - 'null'
          pattern: ^conv_[0-9a-f]+$
        ts:
          type: integer
          format: int64
        tsIso:
          type: string
          format: date-time
        data:
          type: object
          description: >-
            Type-specific payload. Same shape as the matching webhook event's
            `data`.
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Short machine-friendly error code.
        details:
          type: object
          additionalProperties: true
          description: Optional structured context.
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Identity or conversation not in this org.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_live_...

````