YTMDesktop2
API

Routes

Local HTTP and WebSocket API reference

Base URL (default): http://127.0.0.1:13091

When Require authorization is on, protected routes need:

Authorization: Bearer <token>

(Token alone without Bearer is also accepted.)

For browser sources / embeds that cannot set headers, pass the same token as a query param:

?token=<token>

Accepted on GET /track, GET /track/state, GET /track/thumbnail, and WebSocket /socket (same as before for the socket).

Auth endpoints (/auth/*) never require a token. GET / and static /embed/* pages are always public.

Quick reference

MethodPathAuthDescription
GET/NoAPI info + route list
POST/auth/requestcodeNoStart pairing — get a code
POST/auth/requestNoExchange code for token (blocks until approved)
GET/trackYesCurrent track payload
GET/track/stateYesPlayback state
GET/track/thumbnailYesCurrent album art (thumb-cache bytes)
POST/track/playYesPlay
POST/track/pauseYesPause
POST/track/toggle-play-stateYesToggle play/pause
POST/track/nextYesNext track
POST/track/prevYesPrevious track
POST/track/seekYesSeek to absolute time
POST/track/forwardYesSeek forward by seconds
POST/track/backwardYesSeek backward by seconds
POST/track/repeatYesCycle repeat mode
POST/track/shuffleYesToggle shuffle
POST/track/likeYesSet like
POST/track/dislikeYesSet dislike
POST/track/volumeYesSet / read volume
POST/track/volume-upYesRaise volume
POST/track/volume-downYesLower volume
POST/track/accentYesDominant color from artwork
POST/nav/watchYesPlay song by videoId
POST/nav/playlistYesOpen / play playlist
POST/nav/channelYesOpen channel / artist
POST/nav/queueYesAdd video or playlist to queue
GET/nav/queueYesList current queue items
POST/nav/queue/clearYesClear upcoming queue
POST/nav/openYesOpen ytmd:// or https URL (immediate)
POST/nav/homeYesNavigate to YouTube Music home
GET/socketYes*WebSocket track events
GET/socket/pingNoWebSocket ping/pong
GET/embed/now-playingNoFirst-party OBS now-playing HTML/JS

* Auth via Authorization header or ?token= query when required.


Info

GET /

Returns server metadata and registered internal route keys.

curl http://127.0.0.1:13091/
{
  "name": "YTMDesktop2 Api",
  "beta": false,
  "player": { "...": "player settings object" },
  "authRequired": true,
  "routes": [
    "api/routes",
    "api/track",
    "api/track/state",
    "api/track/next",
    "api/auth/requestcode",
    "api/auth/request"
  ]
}

Authentication

See also Authentication settings.

POST /auth/requestcode

Create a pairing code for a client. appId must be 2–32 lowercase alphanumeric characters.

curl -X POST http://127.0.0.1:13091/auth/requestcode \
  -H "Content-Type: application/json" \
  -d '{"appId":"myapp","appName":"My App","appVersion":"1.0.0"}'
{ "code": "AB12CD" }

Error (400):

{ "error": "appId must be 2-32 lowercase alphanumeric characters" }

POST /auth/request

Exchange the code for a token. Blocks until the user approves (or denies / times out) in Settings → Authentication.

curl -X POST http://127.0.0.1:13091/auth/request \
  -H "Content-Type: application/json" \
  -d '{"appId":"myapp","code":"AB12CD"}'
{ "token": "eyJ…" }

Error (400):

{ "error": "Invalid or expired auth code" }
{ "error": "Authorization timed out — user did not approve in time" }

Use the token on later requests:

curl http://127.0.0.1:13091/track \
  -H "Authorization: Bearer eyJ…"

Unauthorized (401):

{ "error": "unauthorized" }

Track — read

GET /track

Current track, or null if nothing is loaded.

curl http://127.0.0.1:13091/track \
  -H "Authorization: Bearer <token>"
{
  "id": "dQw4w9WgXcQ",
  "video": {
    "videoId": "dQw4w9WgXcQ",
    "title": "Never Gonna Give You Up",
    "lengthSeconds": "213",
    "channelId": "UCuAXFkgsw1L7xaCfnd5JJOw",
    "author": "Rick Astley",
    "thumbnail": {
      "thumbnails": [
        { "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg", "width": 480, "height": 360 }
      ]
    },
    "musicVideoType": "MUSIC_VIDEO_TYPE_ATV"
  },
  "context": {
    "title": "Never Gonna Give You Up",
    "urlCanonical": "https://music.youtube.com/watch?v=dQw4w9WgXcQ",
    "videoDetails": {
      "externalVideoId": "dQw4w9WgXcQ",
      "durationSeconds": "213"
    },
    "thumbnail": { "thumbnails": [] }
  },
  "meta": {
    "thumbnail": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
    "isAudioExclusive": true,
    "startedAt": 1710000000.123,
    "duration": 213,
    "isAlbum": true,
    "liked": false,
    "disliked": false
  },
  "music": { "album": "Whenever You Need Somebody" }
}

GET /track/state

Playback / progress snapshot.

curl http://127.0.0.1:13091/track/state \
  -H "Authorization: Bearer <token>"
{
  "id": "dQw4w9WgXcQ",
  "playing": true,
  "progress": 42.5,
  "uiProgress": 42.5,
  "duration": 213,
  "liked": false,
  "disliked": false,
  "startedAt": 1710000000.123,
  "percentage": 19.95,
  "eventType": "progress",
  "accent": "#c41e3a"
}

null when no active track state exists yet.

GET /track/thumbnail

Album art for the current track, served from the thumbnail cache (same bytes as tray ytmd-thumb://). Prefer this over hotlinking YouTube CDN URLs from OBS / scripts.

Optional query:

ParamRole
idCurrent videoId — cache-bust when the track changes
tokenAuth token when required (same as other GETs)
curl -OJ "http://127.0.0.1:13091/track/thumbnail?id=dQw4w9WgXcQ" \
  -H "Authorization: Bearer <token>"

Returns raw image bytes (Content-Type from cache, usually image/jpeg / image/webp).

StatusWhen
200Image body
302Non-CDN thumb URL — redirects to remote
404No track / no thumbnail
502Cache fetch failed

Track — controls

All control routes: POST, JSON body optional unless noted. Auth required when enabled.

Shared playback control response:

{ "isPlaying": true, "time": 12.4 }

POST /track/play

curl -X POST http://127.0.0.1:13091/track/play \
  -H "Authorization: Bearer <token>"

POST /track/pause

curl -X POST http://127.0.0.1:13091/track/pause \
  -H "Authorization: Bearer <token>"

POST /track/toggle-play-state

curl -X POST http://127.0.0.1:13091/track/toggle-play-state \
  -H "Authorization: Bearer <token>"

POST /track/next / POST /track/prev

curl -X POST http://127.0.0.1:13091/track/next \
  -H "Authorization: Bearer <token>"

POST /track/repeat / POST /track/shuffle

Cycles / toggles YouTube Music UI controls. Same { isPlaying, time } shape.

POST /track/seek

Absolute seek. Body:

{ "time": 90, "type": "seek" }

time is required (seconds). type optional ("seek").

curl -X POST http://127.0.0.1:13091/track/seek \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"time":90}'

POST /track/forward / POST /track/backward

Relative seek. Body:

{ "time": 10 }

time must be a non-zero number (seconds). Forward adds; backward subtracts.

curl -X POST http://127.0.0.1:13091/track/forward \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"time":15}'

POST /track/like / POST /track/dislike

Body is a JSON boolean (true / false).

curl -X POST http://127.0.0.1:13091/track/like \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d 'true'

Response: true / false / null (current like/dislike result from the player).

Volume

POST /track/volume

{ "volume": 50 }
{ "volume": 50 }

Omit volume to read/update via player defaults.

POST /track/volume-up / POST /track/volume-down

{ "amount": 5 }
{ "volume": 55 }

POST /track/accent

Extracts a dominant color from the current track artwork (hex string or null).

curl -X POST http://127.0.0.1:13091/track/accent \
  -H "Authorization: Bearer <token>"
"#c41e3a"

Immediate open/queue — same as deeplink Play / Add to queue / Open, no ask dialog (HTTP always acts). Auth when required. Mutations respond { "ok": true } unless noted.

POST /nav/open

Parse and act on a ytmd:// or https:// URL (music.youtube.com, youtube.com, youtu.be).

{ "url": "https://youtu.be/dQw4w9WgXcQ" }
{ "url": "ytmd://youtube.com/watch?v=dQw4w9WgXcQ" }

Response includes the parsed link:

{ "ok": true, "link": { "type": "watch", "videoId": "dQw4w9WgXcQ" } }

POST /nav/home

Go to YouTube Music home.

POST /nav/watch

Play a song. Radio/mix playlist ids (RD…) are stripped.

{ "videoId": "dQw4w9WgXcQ", "playlistId": "OLAK5uy_…" }

playlistId optional (album/playlist context only).

curl -X POST http://127.0.0.1:13091/nav/watch \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"videoId":"dQw4w9WgXcQ"}'

POST /nav/playlist

Open playlist page, or start playing when play is true.

{ "playlistId": "PL…", "play": true }

POST /nav/channel

Open artist/channel. Need channelId (UC…) and/or handle (with or without @).

{ "channelId": "UC…" }
{ "handle": "@ArtistName" }

POST /nav/queue

Append to current queue. Send either videoId or playlistId (not both — if both, videoId wins).

Video add needs an active queue context (play something first). Uses /music/get_queue + store update.

{ "videoId": "dQw4w9WgXcQ" }
{ "playlistId": "PL…" }

GET /nav/queue

Current queue snapshot (needs ytm store hook for ids/titles).

{
  "items": [{ "index": 0, "videoId": "…", "title": "…" }],
  "count": 1,
  "storeHooked": true
}

Empty / sparse items when store hook not ready yet.

POST /nav/queue/clear

Clear upcoming queue via player clearQueue.


WebSocket

GET /socket

Upgrade to WebSocket. When auth is required, pass Authorization: Bearer <token> or ?token=<token>.

On connect, the server pushes the current track (or "null"), then current playback state when available. Later broadcasts use:

{
  "event": "track:change",
  "data": [{ "video": { "videoId": "…" }, "meta": { "…" }, "…" }]
}
{
  "event": "track:state",
  "data": [{ "id": "…", "playing": true, "progress": 12.5, "duration": 210, "percentage": 6, "accent": "#3b82f6", "…" }]
}

track:state is rate-limited (~250ms buckets) so overlays stay smooth without polling GET /track/state.

Unauthorized sockets close with code 1008.

GET /socket/ping

Simple ping socket — any message gets Pong! (no auth).


Errors

StatusBodyWhen
401{ "error": "unauthorized" }Missing/invalid token on protected route
400{ "error": "…" }Auth validation / pairing failures; invalid /nav/* body or URL
404{ "error": "not found" }Unknown path
503{ "error": "…" }YouTube Music view not ready / missing
500{ "error": "failed to do requested operation (…)" }Track/nav command threw
500{ "error": "internal error" }Unhandled server error

CORS allows GET / POST / OPTIONS with Authorization and Content-Type from any origin (local tools / overlays).

On this page