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

# SDK events

`aegis.music` carries the [events WebSocket](/events). Subscribing is the only
thing that opens it.

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

off() // unsubscribes
```

## Subscribing

|                         |                                                   |
| ----------------------- | ------------------------------------------------- |
| `on(event, listener)`   | Fires every time. Returns an unsubscribe function |
| `once(event, listener)` | Fires once, then unsubscribes itself              |
| `off(event, listener)`  | Removes a listener by reference                   |
| `listening`             | Whether the socket is currently open              |
| `disconnect()`          | Closes permanently                                |

The returned unsubscribe function and `off` do the same thing — use whichever
reads better.

`off` needs the same function reference you subscribed with, which is why the
returned function exists.

```ts
function onTrackStarted(event) { /* ... */ }

aegis.music.on('trackEnded', onTrackStarted)
aegis.music.off('trackEnded', onTrackStarted)
```

## Events

| Event          | Payload                                                     |
| -------------- | ----------------------------------------------------------- |
| `trackStarted` | `botId`, `guildId`, `track`                                 |
| `trackEnded`   | `botId`, `guildId`, `reason`                                |
| `queueEmptied` | `botId`, `guildId`                                          |
| `playerClosed` | `botId`, `guildId`, `reason` (`'idle'` \| `'disconnected'`) |
| `botStopped`   | `botId`, `reason`                                           |

`MUSIC_EVENTS` holds the same list as a runtime array.

Payload types are enforced in both directions, so a typo or a field that does
not belong to that event is a compile error:

```ts
aegis.music.on('queueEmptied', (data) => data.track)
//                                 ^ Type 'queueEmptied' has no 'track'
```

## Lifecycle

The socket opens **lazily** on the first subscription — constructing a client
does not connect anything.

It closes when the last listener goes away, whether through `off` or the
returned unsubscribe function. Nothing left to hear means nothing to keep
open.

An unexpected close schedules a reconnect after 5 seconds, but only while a
listener still exists. The delay is fixed: there is no backoff and no retry cap,
so a socket that keeps failing will keep retrying every 5 seconds for as long as
you are subscribed.

> **Warning**
>
> `disconnect()` is permanent. It marks the socket closed, and subscribing again
> afterwards will **not** reopen it. Treat it as shutting the client down for
> events, not as a pause.

## Payload shape

Listeners receive the whole message, `type` field included — not just the parts
that vary. A non-JSON frame is logged and skipped rather than thrown, and a
listener that throws is isolated so the remaining listeners still run.

## Custom sockets

The default socket uses the global `WebSocket`. If you need to supply your own
implementation — in a test, or to add instrumentation — pass a factory:

```ts
new Aegis({
  apiKey,
  createSocket: (url, headers) => myWebSocket(url, headers),
})
```

The factory receives the resolved `wss://` URL and the `Authorization` header,
and must return something shaped like a `WebSocket`.

> **Warning**
>
> `createSocket` is accepted at runtime but is not declared on `AegisOptions`,
> so TypeScript rejects it as an excess property on an object literal. Cast the
> options object if you use it.