# Events

> Every events endpoint, generated from the API's own schemas.

Base URL `https://pidgeon.ai/api/v1`. Every request carries `Authorization: Bearer pgn_live_…`. The machine-readable version of this page is https://pidgeon.ai/openapi.json.

## GET /events

List events

The same events a webhook delivers, readable after the fact — so a missed delivery is recoverable without replaying anything.

**Two directions.** `cursor` pages backwards through history, newest first, which is what a dashboard shows. `after` tails the log forwards, oldest first, which is what a caller that cannot receive a webhook does: ask for everything since the last event you saw, act on it, and keep the last `created_at` as the next cursor. Naming both is refused — they are different reads.

**Starting from nothing: pass `after=1970-01-01T00:00:00Z`.** With neither parameter this answers newest first, so a first-time consumer that takes the last row as its high-water mark stores the *oldest* event in the log and never advances. Any timestamp at or before the oldest event you hold will do; the epoch is the one nobody has to think about.

A forward page is ordered oldest first precisely so its **last** row is the high-water mark to resume from; `next_cursor` is that row either way.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | no |  |
| `cursor` | string | no |  |
| `type` | string | no |  |
| `identity_id` | uuid | no |  |
| `after` | date-time | no |  |

**Responses**

- `200` A page of events.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /events/socket

Events over a WebSocket

The event log over a WebSocket — `wss://pidgeon.ai/api/v1/events/socket` — with the same `type`, `identity_id` and `after` as `GET /events/stream`. All frames are JSON text.

**Authenticate** with the `Authorization` header on the upgrade, or — from a browser, which cannot set one — send `{"type":"auth","api_key":"pgn_live_…"}` as the first message within ten seconds. Never put the key in the URL.

**The server sends** `{"type":"ready","cursor"}`, then `{"type":"event","event":Event}` per event, oldest first. Before about 280 seconds it sends `{"type":"reconnect","cursor"}` and closes with 4000: reconnect with `after` set to that cursor and nothing is missed. 4401 means no key or a bad one; 4500 means the read failed — reconnect with the last cursor you saw.

Like the stream, it polls the log on our side of the wire every two seconds.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | no |  |
| `cursor` | string | no |  |
| `type` | string | no |  |
| `identity_id` | uuid | no |  |
| `after` | date-time | no |  |

**Responses**

- `101` Switching protocols: the socket is open.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /events/stream

Stream events as they happen

The event log held open, for a caller that cannot host a webhook endpoint — a script on a laptop, a notebook, an agent runtime with no public address.

**Server-sent events.** Each message carries the event as `data`, the event type as `event:`, and the cursor to resume from as `id:`. Authentication, filtering by `type` and `identity_id`, and the `after` cursor are all exactly as on `GET /events` — this is that read, held open.

**Reconnection is automatic and lossless.** A compliant client resends the last `id:` it saw as `Last-Event-ID`, which is preferred over `after`, so a dropped connection resumes with no gap and no repeat. The connection closes itself after about 280 seconds so that the last thing you receive is a cursor rather than a socket error; reconnect and carry on.

**It polls, every two seconds, on our side of the wire.** No push exists in the database. What this buys is one held connection instead of a request from your network on a timer, and being woken only when something happened.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | no |  |
| `cursor` | string | no |  |
| `type` | string | no |  |
| `identity_id` | uuid | no |  |
| `after` | date-time | no |  |

**Responses**

- `200` An open stream of events.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /events/{id}

Fetch one event

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` The event.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.
