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

# Music

The Music API is the largest part of Aegis and the only one that behaves like a
stateful service rather than a set of lookups. Every other feature answers a
question; this one remembers.

## How it is shaped

Almost every route hangs off a guild:

```
/v1/music/guilds/{guildId}/<action>
```

`{guildId}` is a Discord snowflake. There is no "create a player" call and no
player id to hold onto — you name the guild and the player materialises on first
use.

Three consequences follow from that:

* **You supply the guild, every time.** The API does not track which guild you
  mean across calls.
* **State persists.** `play` queues behind whatever is already playing; `queue`
  reads back everything the player knows.
* **Mutating routes return the new state**, so you usually do not need a second
  read to confirm what happened.

## Requirements

The Music API acts as a Discord bot, and it works out which one from your
[API key](/authentication) rather than from the request. If your key is not
linked to a bot, every music endpoint answers:

```json
{
  "error": {
    "code": "forbidden",
    "message": "This API key is not linked to a bot. Link it to one to use the music API."
  }
}
```

Concurrency is bounded by plan — 5 simultaneous players on `free`, up to 100 on
`scale`. Going over returns `403` with code `player_limit`. See
[rate limits](/rate-limits).

## Playing something

Give it a search query and it plays what it finds:

```ts
await aegis.music.play(guildId, {
  query: 'lofi hip hop',
  channelId: voiceChannelId,
})
```

`channelId` is the voice channel to join. It is required the first time, when the
bot is not already connected in that guild.

The response tells you what happened:

```ts
const { track, position, added } = await aegis.music.play(guildId, { query: 'lofi' })
// position: 0 means it started playing; anything higher means it queued
// added:    how many tracks were queued — a playlist adds several
```

## Searching, then playing

`search` looks through a provider without touching the player, and returns an
opaque `encoded` token per result:

```ts
const { results } = await aegis.music.search({ query: 'rick astley', limit: 5 })

await aegis.music.play(guildId, { track: { encoded: results[0].encoded } })
```

Searching then playing `results[0]` is not the same as playing the query —
search lets someone pick from a list before anything starts. `provider` accepts
`youtube` (the default) or `spotify`.

## Reading the player

```ts
const state = await aegis.music.queue(guildId)

state.current // the track playing now, or null
state.queue // everything behind it
state.paused
state.positionMs
state.volume
state.loop // 'off' | 'track' | 'queue'
state.channelId
```

## Controlling playback

|                                      |                                                    |
| ------------------------------------ | -------------------------------------------------- |
| `pause(guildId)` / `resume(guildId)` | Toggle playback                                    |
| `skip(guildId)`                      | Next track; `track` is `null` if the queue ran out |
| `restart(guildId)`                   | Replay the current track from the start            |
| `stop(guildId)`                      | Stop and leave; clears the queue                   |
| `seek(guildId, { position })`        | Jump to a position in milliseconds                 |

## The queue

```ts
const { queue } = await aegis.music.shuffle(guildId)
await aegis.music.clearQueue(guildId)
await aegis.music.removeFromQueue(guildId, 0) // 0 is the next track up
await aegis.music.moveInQueue(guildId, 3, { to: 0 })
```

Queue indices are zero-based and capped at 200 entries.

## Volume and muting

```ts
await aegis.music.setVolume(guildId, { volume: 60 })
await aegis.music.mute(guildId) // or { muted: false }
await aegis.music.unmute(guildId)
```

Volume runs from 0 to 200, where 100 is normal. Mute is separate from volume, so
unmuting restores the level you had rather than resetting it.

## Loop mode

```ts
await aegis.music.setLoop(guildId, { mode: 'queue' })
const { loop } = await aegis.music.loop(guildId)
```

`track` repeats one song, `queue` cycles the whole rotation — and keeps going
even when the queue holds a single track.

## Audio filters

`filters` applies DSP to playback in one call. Every field is optional; send `{}`
to clear them all.

```ts
await aegis.music.setFilters(guildId, {
  equalizer: [{ band: 0, gain: 0.5 }],
  timescale: { speed: 1.5, pitch: 1, rate: 1 },
  lowPass: { smoothing: 20 },
})
```

The fifteen filters are `equalizer`, `karaoke`, `timescale`, `tremolo`,
`vibrato`, `rotation`, `distortion`, `channelMix`, `lowPass`, `echo`, `chorus`,
`compressor`, `highpass`, `phaser`, and `spatial`. Each takes its own range —
`equalizer` bands run from 0 (25 Hz) to 14 (16 kHz), for instance — so check the
[API reference](/api-reference) for the schema before composing a preset.

## Fading

Volume ramps for track starts, ends, seeks, and ducking under other audio. Each
section takes a duration in milliseconds and a curve.

```ts
await aegis.music.setFading(guildId, {
  enabled: true,
  trackStart: { duration: 2000, curve: 'linear' },
  trackStop: { duration: 3000, curve: 'exponential' },
})
```

Durations cap at 30 seconds. Curves are `linear`, `exponential`, `logarithmic`,
and `s-curve`.

## Mix layers

An extra layer playing alongside the main track — instrumental, backing vocals,
anything.

```ts
const { layer } = await aegis.music.addMix(guildId, {
  track: { encoded },
  volume: 0.4,
})

await aegis.music.setMixVolume(guildId, layer.mixId, { volume: 0.6 })
await aegis.music.removeMix(guildId, layer.mixId)
```

## Track metadata

Two endpoints take an `encoded` token and return what search knows about it:

```ts
await aegis.music.meaning(guildId, { encoded }) // encyclopedic context, or null
await aegis.music.lyrics(guildId, { encoded, lang: 'en' })
```

`meaning` resolves with `meaning: null` when nothing is found — that is a
successful response, not an error. `lyrics` reports whether anything was found
and whether the timing is synced.

## Knowing when things happen

Rather than poll the player, subscribe to the [events socket](/events):

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

## Errors

Music uses its own error mapping, drawn from the same
[envelope](/errors) as everything else:

| Code                  | Status | Typical cause                                            |
| --------------------- | ------ | -------------------------------------------------------- |
| `bad_request`         | 400    | An index or track reference that makes no sense          |
| `forbidden`           | 403    | The key is not linked to a bot                           |
| `player_limit`        | 403    | The project is at its concurrent-player ceiling          |
| `not_found`           | 404    | No player in that guild yet                              |
| `conflict`            | 409    | Nothing playing, already connected, or the queue is full |
| `validation_error`    | 422    | A body field failed validation                           |
| `service_unavailable` | 503    | The music workers are down or timed out                  |

Music routes take a JSON body, so a request with the wrong `Content-Type` is
rejected with `415` before any of this applies.