# Events

Events tell you what happened at a hub: a door unlocked, a temperature changed, a device joined, the hub went offline. Each hub keeps its own ordered log of events, and 1key keeps the last 24 hours of it. Needs `events:read`.

## Reading events

Read a hub's events with a cursor. Start without one to get the oldest retained events, then pass `next_cursor` as `after` each time.

```curl
curl "https://api.1keyplatform.dev/v1/hubs/1key-xfy2-nmf4-5xwp/events?after=1042&wait=30" \
  -H "Authorization: Bearer $ONEKEY_API_KEY"
```

```json
{
  "data": [
    {
      "cursor": "1043",
      "type": "channel.observed",
      "hub_id": "1key-xfy2-nmf4-5xwp",
      "device_id": 7,
      "time": "2026-10-03T14:03:11.143000Z",
      "clock_trusted": true,
      "data": { "key": "lock", "endpoint": 0, "value": false }
    }
  ],
  "next_cursor": "1043",
  "has_more": false
}
```

`wait=30` holds the request until an event arrives or 30 seconds pass (up to 60), so a loop of requests gets events as they happen without busy-polling. `limit` is 1–500, default 100; when `has_more` is `true`, read again at once.

## A reader loop

```python
import os, requests

API = "https://api.1keyplatform.dev/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['ONEKEY_API_KEY']}"}

def follow(hub_id, cursor=None):
    while True:
        params = {"wait": 30, **({"after": cursor} if cursor else {})}
        res = requests.get(f"{API}/hubs/{hub_id}/events", headers=HEADERS, params=params, timeout=70)
        if res.status_code == 410:
            cursor = resync(hub_id)  # read current state, then continue from the newest event
            continue
        res.raise_for_status()
        page = res.json()
        for event in page["data"]:
            handle(event)       # must tolerate seeing the same event twice
        cursor = page["next_cursor"]
        save_cursor(hub_id, cursor)  # persist after handling
```

```javascript
async function follow(hubId, cursor) {
  for (;;) {
    const url = new URL(`https://api.1keyplatform.dev/v1/hubs/${hubId}/events`);
    url.searchParams.set("wait", "30");
    if (cursor) url.searchParams.set("after", cursor);
    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ONEKEY_API_KEY}` } });
    if (res.status === 410) { cursor = await resync(hubId); continue; }
    if (!res.ok) throw new Error(`events: ${res.status}`);
    const page = await res.json();
    for (const event of page.data) await handle(event);
    cursor = page.next_cursor;
    await saveCursor(hubId, cursor);
  }
}
```

## Streaming

For a held-open connection, use Server-Sent Events:

```curl
curl -N https://api.1keyplatform.dev/v1/hubs/1key-xfy2-nmf4-5xwp/events/stream \
  -H "Authorization: Bearer $ONEKEY_API_KEY" \
  -H "Last-Event-ID: 1042"
```

```text
id: 1043
event: channel.observed
data: {"cursor":"1043","type":"channel.observed","hub_id":"1key-xfy2-nmf4-5xwp","device_id":7,"time":"2026-10-03T14:03:11.143000Z","clock_trusted":true,"data":{"key":"lock","endpoint":0,"value":false}}

: keepalive
```

Each message's `id` is the event cursor. Without `Last-Event-ID`, the stream starts with the next new event. A `: keepalive` comment arrives every 30 seconds, and the server ends the stream after 15 minutes. Reconnect with `Last-Event-ID` set to the last `id` you processed; SSE clients do this automatically.

## Delivery rules

| Rule | What to do |
|---|---|
| Order is per hub | Events from one hub arrive in order. There is no order across hubs. |
| At least once | You may see an event again after a reconnect or retry. Deduplicate on (`hub_id`, `cursor`). |
| 24-hour retention | A cursor older than 24 hours gets `410 cursor_expired`. |
| Gaps | If 1key could not get some of a hub's events, a `hub.events_skipped` event says so. Re-read devices for current state. |

## Resyncing after 410

`410 cursor_expired` means events you had not read are gone. Read current state instead of replaying history: call `GET /hubs/{hub_id}/devices`, then read events without `after` and continue from the newest one.

## Cursors

A cursor looks like `1043`. Treat it as an opaque string: store it, compare it for equality, pass it back. Do not parse it or do arithmetic on it.

Event types and their data are listed in the [event reference](/docs/event-reference).
