Jump to content

Refresh token is invalid for OAuth2.0


Recommended Posts

Hi there,

My OAuth2.0 code has been working fine, but today I have received this from my API: {'error': 'invalid_grant', 'error_description': 'the submitted refresh_token is invalid'}. The refresh token had been minted a few minutes before from a sign in via Sage to authorize my app, so I doubt that my refresh token has expired. Any ideas on what the problem could be?

Here is my code to make sure I've followed the documentation correctly;

url = 'https://oauth.accounting.sage.com/token'
payload = {'grant_type': 'refresh_token', 
			'refresh_token': refreshtoken,
			'client_id': client_id,
			'client_secret': client_secret
headers = {
			'Content-Type': 'application/x-www-form-urlencoded'
response = requests.request("POST", url, headers=headers, data=payload)

Thank you any help would be great

Link to comment
Share on other sites

Hi Norman, thank you for your question.

We're not aware of any issue with the exchange of refresh_tokens at present. How are you storing the access and refresh tokens?  Do you set a timer for the access_token period or are you exchanging refresh_tokens for every request?

Making a comparison of your code to code I use to exchange refresh tokens, I noticed that I am also setting the below in the request header.

'Accept': 'application/json'



Link to comment
Share on other sites

Hi @Steel, Mark,

I'm exchanging refresh_tokens for every request. I added the accept parameter to the header and it works, thank you so much. I don't believe that was in the documentation though for Authorization? It would be helpful to have that in the documentation, especially for newbies to the Accounting API, as I was following the documentation strictly. Or does the documentation just assume that you know to put that in the header? Thanks again.

Link to comment
Share on other sites

@Steel, Mark

I thought it was working, but I have left it for about 4 hours now and have ran my program again and the problem is still there. Refresh tokens are valid for 31 days aren't they? So why would it be saying that a token is invalid? Also my token is stored in a variable, that takes the refresh token directly from the JSON response

Link to comment
Share on other sites

Thanks Norman

Refresh tokens are valid for 31 days or until a new refresh token is requested. If you had a scenario where many requests were made consecutively you would be requesting a new access_token for each of those requests. How do you ensure you wait for the new access_token and refresh_token to be returned and the holding variable set before making the next request? 

It sounds as if you're trying to exchange an already super-seeded refresh_token which is being seen as invalid. There's Java Script in the Demo Data Postman collection, that uses a timer to understand when the current access_token is due to expire and then exchanges the refresh_token before it expires and makes the next request.

I've included the script below FYI. 


// Global variables
const moment = require ('moment');
var time = moment();
if (pm.environment.get("accessTokenExpires")< time.format())
      url:  'https://oauth.accounting.sage.com/token',
      method: 'POST',
      header: {
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded',
      body: {
          mode: 'urlencoded',
          urlencoded: [
            {key: "client_id", value: pm.environment.get('clientId'), disabled: false},
            {key: "client_secret", value: pm.environment.get('clientSecret'), disabled: false},
            {key: "grant_type", value: "refresh_token", disabled: false},
            {key: "refresh_token", value: pm.environment.get('refreshToken'), disabled: false}
    }, function (error, response) {
        pm.environment.set("accessToken", response.json().access_token);
        //set the Access token expiry time as the response time + 5 mins
        var tokenExpires = new Date;
        tokenExpires =  time.add(5, 'minutes').format();
        pm.environment.set("accessTokenExpires", tokenExpires);
        pm.environment.set("refreshToken", response.json().refresh_token);


  • Like 1
Link to comment
Share on other sites

  • 1 year later...

I had a similar problem. Looks like SAGE errors when you try to refresh token on every single request but seems fine if you do it on expiration. My PHP code in Laravel looks like this to retrieve the TOKEN which I store in DATABASE with all the necessary details.


public function getAccessToken()
        if (!$sageOauthAccessToken = $this->company->sageOauthAccessToken) {
            return null;

        if (Carbon::parse($sageOauthAccessToken->expires_in) > Carbon::now()) {
            return $this->company->sageOauthAccessToken->access_token;

        //refresh TOKEN
        $client = new Client();
        $response = $client->post('https://oauth.accounting.sage.com/token', [
            'headers' => [
                'Accept' => 'application/json',
                'Content-Type' => 'application/x-www-form-urlencoded',
            'form_params' => [
                'grant_type' => 'refresh_token',
                'refresh_token' => $sageOauthAccessToken->refresh_token,
                'client_id' => config('services.sage.client_id'),
                'client_secret' => config('services.sage.client_secret'),

        $statusCode = $response->getStatusCode();
        if ($statusCode == 200) {
            $responseData = json_decode($response->getBody(), true);

            //Update TOKEN
                    'company_id' => $this->company->id,
                    'access_token' => $responseData['access_token'],
                    'expires_in' => Carbon::now()->addSeconds($responseData['expires_in']),
                    'refresh_token' => $responseData['refresh_token'],
                    'refresh_token_expires_in' => Carbon::now()->addSeconds($responseData['refresh_token_expires_in']),
                    'scope' => $responseData['scope'],

            return $responseData['access_token'];

        } else {
            // Handle the error response
            return json_decode($response->getBody(), true);


Link to comment
Share on other sites

Also, another thing I've noticed. When you register your new APP and then do the AUTH to get tokens, you have to then wait few minutes before making any other API calls. Otherwise it keeps complaining that your ACCESS TOKEN is malformed/invalid. After waiting few minutes that error disappeared. Weird

Link to comment
Share on other sites

Hi Peter, thank you for your post.

The refresh_token is invalid for one of three reasons:

1 - It has expired

2 - The user has revoked access

3 - The refresh token has been used before

In the majority of cases, it is generally reason three that is the cause, especially with apps using async calls to refresh the token. The scenario would usually be:

The connected app sends a request to exchange the current request token for a new set of tokens in an async call, during the exchange another request from the user is made, you detect that the current access token is expired and then send a second request to exchange the refresh token that is still being processed in the original request.

What are you doing to ensure this cannot occur? For example, if the tokens are stored in a DB table do you lock the table until the first request has finished executing?

If, you're exchanging tokens on every request and using async calls it's very likely that our auth server is seeing a second request with a previously exchanged refresh_token.



Link to comment
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in

Sign In Now

  • Create New...