> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.aegi.run/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.aegi.run/_mcp/server.

# Events

Polling [`/v1/jobs`](/jobs) tells you when a render finished. The
[Music API](/music) is different: a player changes state continuously, and you
want to know the moment a track starts rather than noticing afterwards.

`GET /v1/events` is a WebSocket that pushes player events to you.

## Connecting

```bash
wss://api.aegi.run/v1/events
```

The protocol follows the base URL — `wss://` for `https://`, `ws://` for a
local `http://`.

Authenticate in the handshake, exactly like a normal request:

```http
Authorization: Bearer aegis.3f9a1c8e5b7d2049...
```

> **Note**
>
> Browsers cannot set headers on a WebSocket handshake, so the key may also be
> passed as `?key=`. Prefer the header everywhere else — a query string ends up
> in access logs.

## Events

Every message is a JSON object with a `type`.

| `type`         | Fields                       | Fires when                                     |
| -------------- | ---------------------------- | ---------------------------------------------- |
| `ready`        | `botId`, `events`            | Once, immediately after the socket opens       |
| `trackStarted` | `botId`, `guildId`, `track`  | A track begins playing                         |
| `trackEnded`   | `botId`, `guildId`, `reason` | A track stops, for any reason                  |
| `queueEmptied` | `botId`, `guildId`           | The queue runs dry                             |
| `playerClosed` | `botId`, `guildId`, `reason` | The player shuts down                          |
| `botStopped`   | `botId`, `reason`            | The bot itself goes offline                    |
| `ping`         | —                            | Every 30 seconds, to keep the connection alive |

`playerClosed.reason` is `idle` or `disconnected`. `botStopped` carries no
`guildId` because it is about the bot as a whole, not one player.

The `track` on `trackStarted` is the same shape the [Music API](/music)
returns — title, author, duration, artwork, and whether it is a stream.

```json
{
  "type": "trackStarted",
  "botId": "1200712345678901234",
  "guildId": "987654321098765432",
  "track": {
    "title": "Coffee With Milk",
    "author": "Some Artist",
    "lengthMs": 214000,
    "uri": null,
    "artworkUrl": "https://files.aegi.run/6b1f-album-art.jpg",
    "isStream": false
  }
}
```

## Filtering

By default a socket receives every event. Narrow it with a comma-separated
`events` query parameter:

```
wss://api.aegi.run/v1/events?events=trackStarted,trackEnded
```

## Which events you get

A socket only hears about the bot its key is linked to. A key with no bot linked
receives everything — handy while you are wiring things up, but not something to
rely on in production.

## Keepalives

The server sends `ping` every 30 seconds. Clients are not required to answer,
but if you sit behind a proxy that drops idle connections, reply with `pong`:

```ts
socket.addEventListener('message', (event) => {
  if (event.data === 'ping') socket.send('pong')
})
```

Any other text a client sends is ignored.

## In the SDK

The [JavaScript SDK](/sdk) wraps all of this. Subscribing opens the socket
lazily, and unsubscribing from the last event closes it again:

```ts
const off = aegis.music.on('trackStarted', ({ guildId, track }) => {
  console.log(`${track.title} started in ${guildId}`)
})

off() // unsubscribes; the socket closes when nothing is listening
```

See [SDK events](/sdk-events) for reconnect behaviour and lifecycle details.