Skip to navigation

Refresh token error reasons

When a refresh token request fails, IDMe can tell your application why with a short, fixed code called error_reason. Your application can then react correctly. For example, it can send the member to sign in again instead of retrying a token that will never work.

error_reason is off by default and is turned on separately for each integration. To turn it on, contact your IDMe account team.

Why it exists

A failed refresh returns a generic invalid_grant error. The only explanation is an English sentence in error_description, which is written for people and can change. Applications that cannot tell the causes apart often retry the same dead refresh token. That adds failed calls and leaves members stuck instead of signed back in.

error_reason adds one field to the failed refresh response. The HTTP status, error, and error_description do not change.

How it works

yes no no retry Your app sendsthe refresh token IDMe checksthe token Valid? 200 OKsave the new tokens 400 invalid_grant+ error_reason Your app logs the reasonand signs the member in again
  1. Your application sends the refresh token to IDMe, as it does today.
  2. If the token is valid, IDMe returns new tokens. Successful responses do not change.
  3. If the token is not valid, IDMe returns the usual invalid_grant error and adds error_reason, a code that names the cause.
  4. Your application records the code and sends the member to sign in again. It does not retry the same refresh token.

Availability

error_reason is set per integration (OAuth client) and per environment. We recommend turning it on in sandbox first. Tell your account team which integration and environment you want. Turning it on or off needs no code change from you.

error_reason off (default)error_reason on
HTTP status of a failed refresh400400 (unchanged)
errorinvalid_grantinvalid_grant (unchanged)
error_descriptionHuman-readable sentenceSame sentence (unchanged)
error_reasonNot presentOne of the four values below, when one applies
Other grant types and endpointsUnchangedUnchanged

Response

error_reason appears only on failed POST /oauth/token requests with grant_type=refresh_token. The request does not change:

Request
curl -X POST https://api.id.me/oauth/token \
-d grant_type=refresh_token \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET \
-d refresh_token=$REFRESH_TOKEN

Public clients that use PKCE send no client_secret. Credentials can also be sent in an HTTP Basic Authorization header. error_reason works the same way in every case.

Response (HTTP 400)
{
"error": "invalid_grant",
"error_description": "The refresh token provided has already been used",
"error_reason": "refresh_token_revoked"
}

Values

The values are fixed codes for your code to compare. Write your handling so that a value you don’t recognize is treated as a failed refresh.

error_reasonWhat happenederror_description
refresh_token_invalidIDMe does not recognize the refresh token.The refresh token provided is invalid
refresh_token_revokedThe refresh token was already used, or it was revoked (for example, your application called /oauth/revoke). Each successful refresh returns a new refresh token and retires the old one.The refresh token provided has already been used
refresh_token_clientThe refresh token was issued to a different OAuth client.The refresh token provided was issued to another client
refresh_token_expiredThe refresh token is past its expiry. Each successful refresh starts a new refresh token lifetime.The refresh token provided has expired

IDMe checks these in the order listed and returns the first one that applies. For example, a used refresh token presented by a different client returns refresh_token_revoked.

When the field is absent

The response has no error_reason in these cases, even when the field is turned on:

  • The request has no refresh_token parameter. This returns invalid_request.
  • Client authentication fails, for example because of an unknown client_id or a wrong client_secret. This returns HTTP 401 invalid_client before IDMe looks at the refresh token.
  • The member’s account can no longer sign in, for example because it is suspended. This returns invalid_grant with no reason, so the response does not reveal the account’s status.
  • The request uses any other grant type, such as authorization_code.
  • The refresh succeeds.

Handling refresh failures

On any invalid_grant response to a refresh request, with or without error_reason, stop using that refresh token. It will not succeed. Discard it and send the member through sign-in to get a new token pair. Use error_reason to log the cause, show the right message, and find problems in your own token storage.

error_reasonRetry the same token?Recommended actionWorth investigating if frequent
refresh_token_revokedNoSign the member in again.Two processes refreshing the same token at once, or the new refresh token not being saved after a refresh.
refresh_token_expiredNoSign the member in again.Sessions left idle longer than the refresh token lifetime.
refresh_token_invalidNoSign the member in again.Tokens that are truncated, corrupted, or mixed up between sandbox and production.
refresh_token_clientNoSign the member in again.Tokens shared across client IDs, for example between web and mobile apps.
Absent, or a value you don’t recognizeNoSign the member in again.Nothing new to learn. This is the default behavior.
Example
const res = await fetch("https://api.id.me/oauth/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: storedRefreshToken,
}),
});
if (res.ok) {
const tokens = await res.json();
// Always save the new refresh token. The old one is now used up.
saveTokens(tokens.access_token, tokens.refresh_token);
} else {
const err = await res.json();
if (err.error === "invalid_grant") {
log("refresh failed", { reason: err.error_reason ?? "none" });
clearTokens();
redirectToSignIn();
} else {
// invalid_client, invalid_request, and so on: a setup problem, not a member problem
alertEngineering(err.error);
}
}
Best practices
  • Read error first, then error_reason.
  • Don’t require error_reason to be present. It can be absent (see When the field is absent).
  • Don’t parse error_description. Its text can change, and it can be translated into the member’s language.
  • Don’t retry a refresh token after invalid_grant.
  • Save the new refresh token after every successful refresh.
  • Ignore JSON fields you don’t recognize, so that new fields never break your integration.

Testing in sandbox

Ask your account team to turn on error_reason for your sandbox integration. Sign a test member in through your normal flow to get a token pair, then run these checks against https://api.idmelabs.com.

refresh() {
curl -s -X POST https://api.idmelabs.com/oauth/token \
-d grant_type=refresh_token \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET" \
-d refresh_token="$1"
}
1

Success, then refresh_token_revoked

Run refresh "$REFRESH_TOKEN". It returns new tokens. Run the same command again with the old token. It returns refresh_token_revoked.

2

refresh_token_invalid

Run refresh "not-a-real-token".

3

refresh_token_client

Send a refresh token issued to one of your integrations with the client_id and client_secret of another of your integrations.

4

refresh_token_expired

Keep a refresh token unused until it expires, then refresh with it.

5

No error_reason

Send a wrong client_secret (HTTP 401 invalid_client) or leave out refresh_token (invalid_request). Neither response has error_reason.

If a check returns invalid_grant without error_reason, the field may not be turned on yet. Wait a few minutes and try again, then contact your account team.

FAQ

No. Each value repeats what error_description already says, in a form your code can read. IDMe never returns a reason for account-status cases such as a suspended account.

No. The HTTP status, error, and error_description don’t change. The only change is one extra JSON field, and only after you ask for it.

No. It applies only to the refresh token grant on POST /oauth/token. Authorization code exchange, the authorization endpoint, and token revocation are unchanged.

No. All four mean the refresh token cannot be used again. Retrying only adds failed calls.