← Google Apps Script

Advanced · A4

APIs, properties and cache

Your scripts so far only talk to Google. With UrlFetchApp they can talk to any web API: weather, holidays, exchange rates, AI services. This chapter fetches and parses JSON, keeps keys secret, and saves calls with a cache.

Fetch data from the web

Code.gs
function bangkokWeather() {
  // Open-Meteo is free and needs no key.
  const url = 'https://api.open-meteo.com/v1/forecast'
    + '?latitude=13.75&longitude=100.50&current=temperature_2m,wind_speed_10m&timezone=Asia%2FBangkok';

  const response = UrlFetchApp.fetch(url);
  console.log(response.getResponseCode());          // 200 = OK

  const data = JSON.parse(response.getContentText());
  const now = data.current;
  console.log(`Bangkok: ${now.temperature_2m} °C, wind ${now.wind_speed_10m} km/h`);
}

Execution log

200
Bangkok: 31.4 °C, wind 9.2 km/h

Three steps, every time:

  1. Fetch: UrlFetchApp.fetch(url) sends the request and waits for the answer.
  2. Check: getResponseCode(). 200 means OK; 4xx means your request was wrong; 5xx means the server had a problem.
  3. Parse: JSON.parse(response.getContentText()) turns the text into objects and arrays.

Read the API's documentation first

Every API has its own URL format and response shape. Open the URL in your browser to see the JSON, then write code that walks through it. The first fetch asks for the "Connect to an external service" permission.

Next: JSON to Sheet →

Turn JSON into rows

Open-Meteo returns parallel arrays: daily.time[i] goes with daily.temperature_2m_max[i]. map zips them into rows:

Code.gs
// Seven-day forecast into a sheet: one fetch, one setValues.
function forecastToSheet() {
  const url = 'https://api.open-meteo.com/v1/forecast'
    + '?latitude=13.75&longitude=100.50&daily=temperature_2m_max,temperature_2m_min,precipitation_sum'
    + '&timezone=Asia%2FBangkok';
  const d = JSON.parse(UrlFetchApp.fetch(url).getContentText()).daily;

  const rows = d.time.map((day, i) => [day, d.temperature_2m_max[i], d.temperature_2m_min[i], d.precipitation_sum[i]]);
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('Weather') || ss.insertSheet('Weather');
  sheet.clearContents();
  const table = [['Date', 'Max °C', 'Min °C', 'Rain mm'], ...rows];
  sheet.getRange(1, 1, table.length, 4).setValues(table);
}
ABCD
1DateMax °CMin °CRain mm
22026-11-0333.125.02.4
32026-11-0432.624.70

Put forecastToSheet on a daily trigger (I2) and the sheet always holds the latest forecast. A chart on it updates by itself.

Next: POST and errors →

Sending data, handling failure

Code.gs
function postExample() {
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify({ title: 'Hello', body: 'From Apps Script', userId: 1 }),
    // headers: { Authorization: 'Bearer ' + getKey_() },  // only for APIs that need a key
    muteHttpExceptions: true,                             // don't throw on 4xx/5xx; let me check
  };
  const res = UrlFetchApp.fetch('https://jsonplaceholder.typicode.com/posts', options);

  if (res.getResponseCode() >= 400) {
    console.error(`Failed (${res.getResponseCode()}): ${res.getContentText().slice(0, 200)}`);
    return;
  }
  console.log(JSON.parse(res.getContentText()));
}

Execution log

{ title: 'Hello', body: 'From Apps Script', userId: 1, id: 101 }
OptionUse
method'get' (default), 'post', 'put', 'patch', 'delete'
payloadThe body: a string, or an object (sent as form fields)
contentType'application/json' when the payload is JSON.stringify(…)
headersExtra headers, e.g. Authorization
muteHttpExceptionsReturn error responses instead of throwing

Try again when the network fails

Code.gs
// Networks fail. Try a few times, waiting longer each time.
function fetchWithRetry(url, options = {}, tries = 3) {
  for (let attempt = 1; attempt <= tries; attempt++) {
    try {
      const res = UrlFetchApp.fetch(url, { ...options, muteHttpExceptions: true });
      const code = res.getResponseCode();
      if (code < 500 && code !== 429) return res;          // success or a real client error
      console.warn(`Attempt ${attempt}: HTTP ${code}`);
    } catch (err) {
      console.warn(`Attempt ${attempt}: ${err.message}`);
    }
    Utilities.sleep(1000 * 2 ** attempt);                    // 2 s, 4 s, 8 s
  }
  throw new Error(`Gave up on ${url} after ${tries} tries`);
}

A 429 ("too many requests") or 5xx is often temporary. Waiting longer between tries (exponential backoff) gives the server time to recover. A 4xx other than 429 means your request is wrong: retrying won't help.

Next: properties →

Keep secrets and settings out of the code

PropertiesService is a small key-value store that belongs to the project. It survives between runs.

Code.gs
// Run ONCE by hand (then delete the key from this code) to store a secret.
function saveKey() {
  PropertiesService.getScriptProperties().setProperty('WEATHER_KEY', 'paste-your-key-here');
}

// The trailing _ makes this private: it can't be run from the menu or google.script.run.
function getKey_() {
  const key = PropertiesService.getScriptProperties().getProperty('WEATHER_KEY');
  if (!key) throw new Error('No API key. Set WEATHER_KEY in Project settings → Script properties.');
  return key;
}

// User properties: each person who runs the script gets their own copy.
function rememberRoom(room) {
  PropertiesService.getUserProperties().setProperty('myRoom', room);
}
function myRoom() {
  return PropertiesService.getUserProperties().getProperty('myRoom') || 'not set';
}
StoreShared byGood for
getScriptProperties()Everyone who runs the projectAPI keys, IDs, settings
getUserProperties()Just the current userPersonal preferences
getDocumentProperties()Everyone using this file (bound scripts)Settings for one sheet or doc

You can also set Script Properties without code: ⚙ Project settings → Script properties → Add script property. Values are always strings, so convert numbers and JSON when you read them back.

Anyone who can edit the project can read its properties

Properties keep keys out of code you share or screenshot. They don't hide keys from editors of the project. Give edit access only to people you trust with the key.

Next: cache →

Don't ask twice

APIs have rate limits and take time. If the answer won't change for a while, keep a copy in CacheService:

Code.gs
// Ask the API at most once every 30 minutes, however often this runs.
function getForecast() {
  const cache = CacheService.getScriptCache();
  const hit = cache.get('forecast');
  if (hit) {
    console.log('from cache');
    return JSON.parse(hit);
  }

  const url = 'https://api.open-meteo.com/v1/forecast?latitude=13.75&longitude=100.50&daily=temperature_2m_max&timezone=Asia%2FBangkok';
  const data = JSON.parse(UrlFetchApp.fetch(url).getContentText());
  cache.put('forecast', JSON.stringify(data), 30 * 60);   // seconds; max 6 hours
  console.log('from the API');
  return data;
}

Execution log

from the API      ← first run
from cache        ← every run in the next 30 minutes
  • cache.put(key, string, seconds): store for up to 21,600 seconds (6 hours). Values are strings of up to 100 KB.
  • cache.get(key): the string, or null if it expired or was dropped.
  • Cache is temporary: Google can empty it early. Always be ready to fetch again.

Caching matters most for custom functions and web apps, which may run hundreds of times a minute.

Next: practice →

Practice

1. Run counter Easy

Each run, add 1 to a number in Script Properties and log "Run number n".

Show a solution
Code.gs
// Counts how many times the script has run, across runs.
function countRuns() {
  const props = PropertiesService.getScriptProperties();
  const n = Number(props.getProperty('runs') || 0) + 1;
  props.setProperty('runs', String(n));    // properties store strings only
  console.log(`Run number ${n}`);
}

2. Thai holidays Medium

Fetch this year's Thai public holidays from https://date.nager.at/api/v3/PublicHolidays/YEAR/TH and write date, Thai name and English name to a new tab.

Show a solution
Code.gs
// Thai public holidays this year from the free Nager.Date API.
function thaiHolidays() {
  const year = new Date().getFullYear();
  const res = UrlFetchApp.fetch(`https://date.nager.at/api/v3/PublicHolidays/${year}/TH`);
  const list = JSON.parse(res.getContentText());
  const rows = list.map((h) => [h.date, h.localName, h.name]);
  const sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(`Holidays ${year}`);
  sheet.getRange(1, 1, 1, 3).setValues([['Date', 'Thai name', 'English name']]);
  sheet.getRange(2, 1, rows.length, 3).setValues(rows);
}

3. =THB_TO(amount, "USD") Challenge

A custom function that converts baht using a free exchange-rate API, caching the rates for 6 hours so a column of 100 formulas makes one call.

Show a solution
Code.gs
/**
 * Converts THB to another currency, with rates cached for 6 hours.
 * @param {number} baht Amount in THB.
 * @param {string} to Currency code, e.g. "USD".
 * @customfunction
 */
function THB_TO(baht, to) {
  const cache = CacheService.getScriptCache();
  let rates = cache.get('thb-rates');
  if (!rates) {
    rates = UrlFetchApp.fetch('https://open.er-api.com/v6/latest/THB').getContentText();
    cache.put('thb-rates', rates, 6 * 60 * 60);
  }
  const rate = JSON.parse(rates).rates[String(to).toUpperCase()];
  if (!rate) return 'Unknown currency';
  return Math.round(baht * rate * 100) / 100;
}

Next: quiz →

Check yourself

1. UrlFetchApp.fetch(url) returns…
2. What does muteHttpExceptions: true do?
3. Where should an API key go?
4. Which store gives each user their own value?
5. CacheService is a good place for…
6. A function named helper_ (with a trailing underscore) is…