Skip to main content
This is the flow for anything that cannot reasonably open a browser or accept typed input β€” smart TVs, consoles, set-top boxes, watches, CLIs, media-server plugins. Your device shows a short code, the user types it on their phone, and your device polls until the approval lands. No client_secret, no redirect URI, no browser on the device.
The two URLs below can also be discovered rather than hardcoded. Simkl’s discovery document lists them as device_authorization_endpoint and token_endpoint, and includes urn:ietf:params:oauth:grant-type:device_code in its supported grants. Use whichever suits your library β€” the examples on this page set the URLs directly.
Any V2 client type can use this flow β€” Mobile, desktop & browser apps, TV, devices & command line, or Server apps & services. You do not have to register as a TV or device app to use the device flow.Pick TV, devices & command line when the device flow is the only flow your app will ever use: that type registers no redirect URI, which also means it cannot run the authorization code flow. If you have a web or mobile companion that signs users in through a browser, register Mobile, desktop & browser apps and use both flows from the one app.Client type is fixed at registration and cannot be changed later, so this is worth a moment’s thought.
An AUTH V1 app cannot use this flow. It gets 401 invalid_client with the message β€œThis client_id is not enabled for OAuth 2.0”. If you see that while holding credentials that work fine elsewhere, you are pointing V1 credentials at a V2 endpoint β€” register a V2 app rather than debugging the request.

The flow

Ask for a device code

Omitting scope gives you media:read only. If your device writes anything β€” scrobbling, marking watched β€” ask for media:write explicitly.

Show the code to the user

Display user_code exactly as returned, hyphen included, and point the user at verification_uri.If you can render a QR code, encode verification_uri_complete instead β€” it pre-fills the code, so the user types nothing at all. That is a meaningful difference on a TV.Keep device_code secret. It is the credential your device polls with, and it should never appear on screen.

Poll for approval

Poll POST /oauth2/token every interval seconds β€” 5 by default:
Until the user approves, every poll returns an error telling you what to do next. See the table below.

Store the tokens

Once approved you get the standard token response β€” access token valid 7 days, refresh token valid 180 days. From here the device behaves like any other client.Check the scope in that response before you rely on it. An unrecognised scope string is silently downgraded to read-only rather than rejected, so a typo in step 1 produces a token that works until your first write. See Scopes.

Poll responses

The deadline branch is not optional. Because a declining user produces authorization_pending forever, that check is the only thing that ends the loop when someone says no. Without it your device polls until the code expires. Note the two different status codes below. Everything in the first group is a normal part of polling; the second is a configuration problem that polling will never fix. The 401 is raised during client authentication, before your device_code is even looked at, so it tells you nothing about the code itself.
On slow_down, you must actually wait. The poll timer is reset by the attempt itself, including the attempt that was rejected for being too fast. So a client that retries immediately on slow_down keeps re-arming the window and can stay locked out until the code expires, polling furiously the whole time.Back off first, then poll. From the device’s point of view a self-inflicted lockout looks like Simkl being broken, which is why it is worth getting right.

Two things RFC 8628 leads you to expect that do not happen here

There is no deny signal. If the user clicks β€œno” on the approval page, nothing is recorded. Your device keeps receiving authorization_pending, exactly as if they had walked away from the screen.So do not wait for access_denied β€” it will never arrive. Implement your own timeout, and treat expires_in (900 seconds) as the outer bound. A sensible device gives up earlier than that and offers to start again.
authorization_pending covers two different situations. It means β€œnot approved yet”, and it also means β€œapproved, but another poll is currently claiming it” β€” which can happen briefly if your device has more than one poll in flight.You cannot tell them apart, and you do not need to. The correct response to both is to keep polling.

Code entry is more forgiving than it looks

The user_code is 8 characters from a restricted alphabet, displayed as XXXX-YYYY. When the user types it, Simkl normalises the input before matching:
  • Case is ignored β€” bdwp-hqpk works.
  • Hyphens and spaces are optional β€” BDWPHQPK and BDWP HQPK both work.
  • I and L are read as 1, and O as 0 β€” so the classic misreadings succeed rather than failing.
You do not need to validate or normalise the user’s typing yourself, and you should not build a stricter input mask than this. The alphabet already excludes the genuinely ambiguous characters.

Lifetimes

Fifteen minutes is generous for someone with their phone in hand and tight for someone who wandered off. Show a countdown if you have the screen space, and make starting over easy.

See also

POST /oauth2/device

Endpoint reference for the initial request.

POST /oauth2/token

The polling endpoint, and the other two grants.

Tokens and refresh

Keeping a device signed in without re-prompting.

Migrating from V1

Moving a device integration across from the older PIN flow.