Apps Script example · 7 min read

Refresh an OAuth2 Access Token and Store It: Copy-Paste Apps Script Pattern

Working refresh an oauth2 access token and store it example in Apps Script—copy-paste code, common mistakes, and when to get it built professionally.

OAuth2PropertiesServiceUrlFetchApp

Calling a third-party API that uses OAuth2 from Apps Script means dealing with access tokens that expire, typically after an hour, well before most automations are done needing them.

This script posts to a token endpoint with a stored refresh token, client ID, and client secret, receives a fresh access token in response, and stores both the token and its computed expiry timestamp in Script Properties.

Other functions in the same project can call a small getValidAccessToken helper that checks the stored expiry first and only triggers a real network refresh when the cached token has actually expired, saving unnecessary calls to the token endpoint.

This pattern works for any provider using the standard OAuth2 refresh-token grant, with only the endpoint URL and stored credentials changing between integrations.

Need this built? Hire a Google Apps Script developer →

Sheet / project setup

Script propertyExamplePurpose
OAUTH_TOKEN_URLhttps://oauth2.googleapis.com/tokenToken endpoint
OAUTH_CLIENT_ID....apps.googleusercontent.comClient id
OAUTH_CLIENT_SECRET****Client secret
OAUTH_REFRESH_TOKEN****Long-lived refresh token
OAUTH_ACCESS_TOKEN(written by script)Cached access token

What It Does

The refreshAccessToken function posts a standard refresh-token grant request to the provider's token endpoint using the stored client ID, client secret, and refresh token, then parses the JSON response for a new access token and its expires_in value.

It stores the new access token along with a computed absolute expiry timestamp in Script Properties, so a companion getValidAccessToken function can later decide whether the cached token is still good without making a network call every single time.

Prerequisites

You need to have already completed the provider's initial OAuth2 authorization flow at least once to obtain a long-lived refresh token, since this script only handles refreshing an access token, not the initial consent step.

Store CLIENT_ID, CLIENT_SECRET, and REFRESH_TOKEN in Script Properties ahead of time rather than hardcoding them directly in the script file, keeping credentials out of code that might be shared or viewed by collaborators.

Walkthrough

Set TOKEN_ENDPOINT to your provider's token URL, then run refreshAccessToken once manually and check the Script Properties editor under Project Settings to confirm ACCESS_TOKEN and TOKEN_EXPIRES_AT were both written.

Write a small test function that calls getValidAccessToken and logs the result, confirming it returns the same cached token on a second call made immediately after the first, without triggering another network request.

Wait until just past the token's expiry window, call getValidAccessToken again, and confirm it correctly detects the expiry and performs a fresh refresh rather than returning the stale cached value.

Edge Cases

If the provider's refresh token itself has expired or been revoked, the token endpoint returns an error response instead of a new access token, so the script needs to detect that failure case rather than assuming every response contains a valid access_token field.

Clock skew between the time Apps Script computes now and the provider's own clock is handled by subtracting a small safety buffer from the token's reported expires_in value, so the cached token is treated as expired a little before it actually is.

Testing

Manually delete the ACCESS_TOKEN and TOKEN_EXPIRES_AT properties, call getValidAccessToken, and confirm it correctly falls back to performing a full refresh when no cached token exists yet.

Temporarily corrupt the stored REFRESH_TOKEN value, call refreshAccessToken, and confirm the resulting error response from the provider is caught and logged clearly rather than causing a confusing downstream failure in whatever function needed the token.

Hardening

Wrap the token endpoint call in a LockService lock so two functions racing to refresh at nearly the same expiry moment do not both fire simultaneous refresh requests and potentially invalidate each other's freshly issued tokens depending on the provider's rotation policy.

Never log the full access token or refresh token value directly; log only a truncated prefix or a boolean success flag so Apps Script execution logs cannot leak live credentials.

Variations

For providers that rotate the refresh token itself on every use, update the stored REFRESH_TOKEN property from the token response as well, not just the access token, since reusing an old rotated-out refresh token will fail on the next call.

Extend getValidAccessToken into a small library file shared across multiple Apps Script projects if several automations in your organization need to call the same third-party API with the same credentials.

refreshAccessToken.gs

refreshAccessToken posts a standard refresh-token grant to a provider's token endpoint and caches the new access token and its expiry, while getValidAccessToken reuses the cache until it expires.

// Refresh an OAuth2 access token and cache it in Script Properties
function refreshAccessToken() {
  var TOKEN_ENDPOINT = 'https://provider.example.com/oauth/token';
  var props = PropertiesService.getScriptProperties();
  var clientId = props.getProperty('CLIENT_ID');
  var clientSecret = props.getProperty('CLIENT_SECRET');
  var refreshToken = props.getProperty('REFRESH_TOKEN');

  var response = UrlFetchApp.fetch(TOKEN_ENDPOINT, {
    method: 'post',
    payload: {
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: clientId,
      client_secret: clientSecret
    },
    muteHttpExceptions: true
  });

  var result = JSON.parse(response.getContentText());
  if (!result.access_token) {
    throw new Error('Token refresh failed: ' + response.getContentText());
  }

  var expiresAt = Date.now() + (result.expires_in - 60) * 1000;
  props.setProperty('ACCESS_TOKEN', result.access_token);
  props.setProperty('TOKEN_EXPIRES_AT', String(expiresAt));
  return result.access_token;
}

function getValidAccessToken() {
  var props = PropertiesService.getScriptProperties();
  var expiresAt = Number(props.getProperty('TOKEN_EXPIRES_AT') || 0);
  if (Date.now() < expiresAt) {
    return props.getProperty('ACCESS_TOKEN');
  }
  return refreshAccessToken();
}
  1. Line 11: Passing payload as an object with grant_type set to refresh_token follows the standard OAuth2 refresh grant that most providers expect.
  2. Line 21: Checking for a valid access_token before doing anything else catches a revoked or expired refresh token instead of caching garbage.
  3. Line 25: Subtracting 60 seconds from the reported lifetime builds in a small safety buffer against clock skew between the script and the provider.
  4. Line 27: Storing the expiry as a string in Script Properties is necessary since PropertiesService only persists string values.
  5. Line 34: Comparing the current time against the cached expiry is what lets repeated calls reuse a still-valid token without another network round trip.
  6. Line 37: Falling through to a real refresh only when the cached token has actually expired keeps this helper cheap to call from anywhere.

Deploy this example

  1. 01

    Open Apps Script

    In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.

  2. 02

    Paste and save

    Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.

  3. 03

    Authorize once

    Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.

  4. 04

    Add the trigger

    Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.

Before you run: oauth2 token refresh

  • 1Initial OAuth2 authorization completed to obtain a refresh token
  • 2CLIENT_ID, CLIENT_SECRET, and REFRESH_TOKEN stored in Script Properties
  • 3TOKEN_ENDPOINT set to the provider's correct token URL
  • 4Function run once and Script Properties checked for new values
  • 5getValidAccessToken tested to confirm cached tokens are reused
  • 6Expired-token scenario tested to confirm a fresh refresh occurs

Frequently asked questions

refreshAccessToken always makes a network call to get a new token, while getValidAccessToken checks the cached expiry first and only calls refreshAccessToken when necessary.

Update the stored REFRESH_TOKEN property from the token response as well, not just the access token, since reusing an old rotated-out refresh token will fail.

It subtracts a small safety buffer from the reported expires_in value, treating the cached token as expired slightly before it actually is.

No, log only a truncated prefix or a success flag, since full tokens in Apps Script execution logs could leak live credentials.

Wrap the token endpoint call in a LockService lock so simultaneous refresh requests do not race each other, as described in the hardening section.

Yes, use a distinct set of Script Properties keys per provider and call the corresponding refresh function for whichever API you are about to use.

Related examples

Want this wired into your real workflow?

I adapt these patterns to your Sheet structure, APIs, and triggers — deployed in your Google account. Fixed-scope quotes from $500 · free 30-min consult · quote within 24 hours.