Skip to main content
V2 issues short-lived access tokens paired with long-lived refresh tokens, so every V2 app needs refresh logic.

Lifetimes

An app used even occasionally never needs to prompt the user again: each refresh renews both windows. An app left untouched for six months does.

Refreshing

Read this before you write the refresh call, because the rule it describes decides how you structure the whole thing. A grant is what the user created when they approved your app. Each grant holds one access token and one refresh token, and refreshing replaces the access token on that grant — the previous one stops working immediately rather than expiring naturally. So one grant supports exactly one live access token, and two processes that independently refresh the same grant will cut each other off. Each refresh silently invalidates whatever the other is holding, both see intermittent 401s, and each “fixes” it by refreshing again. It looks like random expiry and is unpleasant to debug. Two ways to avoid it, both fine:
  • One owner. A single process refreshes, writes the new access token to shared storage, and the others re-read it on 401 instead of refreshing themselves.
  • One grant each. Give every instance its own authorization. Separate grants never interfere, and a user authorizing twice is cheap.
With that settled, the call itself:
Server apps & services registrations also send their secret, either as Authorization: Basic or as a client_secret parameter. Refresh proactively — when the access token is a day or so from expiry, or on a 401 — rather than on a fixed schedule. There is no benefit to refreshing more often than you need to.

Two behaviours that surprise people

Refresh is non-rotating: the same refresh token comes back every time. The response repeats the refresh token you sent rather than issuing a new one, and its 180-day window slides forward.Nothing breaks if you store the value from every response — it will be identical. But you do not need to, and you should not treat a repeated value as a bug or as evidence the refresh failed. Check the new access_token instead.

Scope on refresh

You can send scope with a refresh, but it is rarely useful:
  • A narrower scope than the original grant is ignored — you get the original scope back.
  • A wider scope is rejected with invalid_scope.
So a grant’s scope can never shrink, and never grows. If you need different permissions, send the user through authorization again.

Revoking

Call POST /oauth2/revoke when a user disconnects or signs out. It is the polite thing to do, and it takes one request:
Revoking either token revokes both. The access token and refresh token are two halves of one grant, so passing either ends the whole thing. There is no way to drop the access token but keep refreshing.You can only revoke your own tokens. Revocation is scoped to the authenticating client, so a token issued to a different app is left untouched.Revoking affects one sign-in, not the whole account. Each authorization produces its own grant, so a user signed in on a phone and a tablet holds two. Revoking one leaves the other working — which is what makes a per-device “disconnect” feature possible, and why revoking on logout doesn’t sign the user out everywhere.
A 200 from revoke does not mean anything was revoked. The endpoint always returns 200 — for a token that never existed, one already revoked, one belonging to another app, and one it genuinely just destroyed. RFC 7009 requires exactly this, so that the endpoint cannot be used to probe which tokens are valid.The consequence is that there is no success signal to check. Do not treat the 200 as confirmation, and do not build a “verify it is gone” step on top of it. Revoke the token you hold, discard your copy, and move on. If you need to know whether a token still works, the only honest test is using it.

Handling a 401

A 401 on a normal API call means the access token is no longer good. In order of likelihood:
  1. It expired — refresh, then retry the request once.
  2. Another process sharing your grant refreshed and invalidated your token — do not refresh. See below.
  3. The user revoked your app from Connected Apps settings — refreshing fails too, and you need a fresh authorization.
For the ordinary case, refresh first and only fall back to re-authorizing if the refresh itself fails. Sending a user through consent because of a routine expiry is a bad experience and unnecessary.
Case 2 is the one where refreshing makes things worse, and the 401 looks identical to case 1 — so you cannot tell them apart from the response. If you are seeing intermittent 401s that clear on retry and come back later, assume case 2 and apply one of the two strategies under Refresh safely above.
A 401 with user_token_required is a different problem. That is not an expired token — it means the request arrived with no token at all, on an endpoint that requires one. Check that you are actually attaching the Authorization header on that code path rather than refreshing. Only public catalog data is exempt on V2.

Storing tokens

Both tokens are 43 characters, prefixed simkl_at_ and simkl_rt_. Treat both as secrets — a refresh token is worth more than an access token, since it mints new ones for six months. The prefixes are deliberate: automated secret scanners can recognise a leaked Simkl token on sight, and each token carries a checksum so a scanner can distinguish a real one from a random string. That helps only if the token was leaked somewhere a scanner looks, so it is a backstop, not a strategy.

Where to put them

Use the platform’s own secret store. Every one of these is encrypted at rest and access-controlled by the OS, which a file you write yourself is not:
Never put a refresh token in the browser. localStorage and sessionStorage are readable by any JavaScript on the page, so one XSS or one compromised dependency hands over six months of access to that user’s account.A browser app should keep tokens on its own backend and expose a session cookie to the page instead. If it genuinely has no backend, hold the access token in memory only and let the user sign in again after a reload — worse UX, but a page refresh is cheaper than a stolen grant.
Size the column for growth, not for today. Tokens are 43 characters now. VARCHAR(255) costs nothing and saves a migration if that ever changes; do not pick CHAR(43).Store the refresh token’s expiry alongside it. It slides forward on every use, so the value you persisted last week is not the current one — recompute it from each refresh response rather than assuming the original.
If a token does leak, revoke it rather than waiting it out. Revoking either half kills the whole grant immediately, and the user simply reconnects.

See also

OAuth flow

How to get the first pair of tokens.

Device / PIN flow

The same, for TVs and CLIs.

Scopes

What a token is allowed to do.

POST /oauth2/revoke

Endpoint reference.