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
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¤t=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:
- Fetch:
UrlFetchApp.fetch(url)sends the request and waits for the answer. - Check:
getResponseCode(). 200 means OK; 4xx means your request was wrong; 5xx means the server had a problem. - 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.
Turn JSON into rows
Open-Meteo returns parallel arrays: daily.time[i] goes with daily.temperature_2m_max[i]. map zips them into rows:
// 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);
} | A | B | C | D | |
|---|---|---|---|---|
| 1 | Date | Max °C | Min °C | Rain mm |
| 2 | 2026-11-03 | 33.1 | 25.0 | 2.4 |
| 3 | 2026-11-04 | 32.6 | 24.7 | 0 |
Put forecastToSheet on a daily trigger (I2) and the sheet always holds the latest forecast. A chart on it updates by itself.
Sending data, handling failure
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 } | Option | Use |
|---|---|
method | 'get' (default), 'post', 'put', 'patch', 'delete' |
payload | The body: a string, or an object (sent as form fields) |
contentType | 'application/json' when the payload is JSON.stringify(…) |
headers | Extra headers, e.g. Authorization |
muteHttpExceptions | Return error responses instead of throwing |
Try again when the network fails
// 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.
Keep secrets and settings out of the code
PropertiesService is a small key-value store that belongs to the project. It survives between runs.
// 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';
} | Store | Shared by | Good for |
|---|---|---|
getScriptProperties() | Everyone who runs the project | API keys, IDs, settings |
getUserProperties() | Just the current user | Personal 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.
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:
// 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, ornullif 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.
Practice
1. Run counter Easy
Each run, add 1 to a number in Script Properties and log "Run number n".
Show a solution
// 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
// 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
/**
* 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;
}