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.

Get the user to the approval page

You get two forms back, and the complete one is better whenever you can use it:Every character a user types is a chance to mistype it and a reason to abandon halfway, so reach for verification_uri_complete first and fall back to the pair only when your device genuinely cannot get a URL in front of them.Ways to deliver the complete URL, by what your device can do:
Show user_code exactly as returned, hyphen included, wherever you show it. It is not case-sensitive and the hyphen is part of the format.
The complete URL pre-fills the code. It does not approve anything. The user still lands on the consent screen and still has to click Allow, so it is safe to put in an email, a log line or a chat message — it grants nothing on its own, and it is useless to anyone who is not signed in to the account they want to connect.device_code is the opposite: it is the credential your device polls with. Never display it, never log it, never put it in a URL.

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.