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 intermittent401s, 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
401instead of refreshing themselves. - One grant each. Give every instance its own authorization. Separate grants never interfere, and a user authorizing twice is cheap.
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 sendscope 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.
Revoking
CallPOST /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.
Handling a 401
A401 on a normal API call means the access token is no longer good. In order of likelihood:
- It expired — refresh, then retry the request once.
- Another process sharing your grant refreshed and invalidated your token — do not refresh. See below.
- The user revoked your app from Connected Apps settings — refreshing fails too, and you need a fresh authorization.
Storing tokens
Both tokens are 43 characters, prefixedsimkl_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: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.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.