Master UrlFetchApp for any REST API
A practical pattern for GET, POST, headers, and payloads — the foundation under every Northwind integration.
Published Jun 25, 2025
Almost every interesting Apps Script automation eventually has to talk to a
service Google does not provide a built-in library for — a payment processor, a
CRM, a weather feed, an AI model. UrlFetchApp is the one tool that makes that
possible: it sends HTTP requests straight from your script and hands back the
response, so any REST API in the world becomes reachable.
The API surface is small but the details matter — query strings, headers, JSON payloads, status codes. This reference collects the patterns Northwind reuses across every integration: a clean GET, an authenticated POST, and the quota facts you need to size a job. Each one is a function you can paste in and build on.
GET with query params
A GET request reads data. The only fiddly part is the query string: every key and value must be URL-encoded so spaces and symbols do not break the request.
/**
* Performs a GET request and returns the parsed JSON response.
* Throws if the API replies with an error status.
*
* @param {string} url - The endpoint, without a query string.
* @param {Object} params - Key/value pairs to send as query params.
* @return {Object} The parsed JSON body.
*/
function getJson(url, params = {}) {
// 1. Encode each key and value, then join into a query string.
const qs = Object.entries(params)
.map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v))
.join('&');
// 2. Append the query string only if there are params.
const fullUrl = qs ? url + '?' + qs : url;
// 3. muteHttpExceptions lets us read 4xx/5xx bodies instead of
// Apps Script throwing before we can see the error.
const res = UrlFetchApp.fetch(fullUrl, { muteHttpExceptions: true });
// 4. Treat any status of 400 or above as a failure.
if (res.getResponseCode() >= 400) throw new Error(res.getContentText());
// 5. The body is text; parse it into a real object.
return JSON.parse(res.getContentText());
}
What comes back. Calling it like this:
const data = getJson('https://api.example.com/v1/cities', { country: 'GB' });
sends GET https://api.example.com/v1/cities?country=GB and parses a response
such as:
{ "cities": [{ "name": "Leeds" }, { "name": "Bristol" }] }
so data.cities[0].name is "Leeds". If the API returned a 404, the function
throws with the error body as the message.
POST with auth
A POST request sends data — creating a record, triggering an action. Most APIs
that accept a POST also require authentication, usually a bearer token in the
Authorization header.
/**
* Performs an authenticated POST with a JSON body and returns the
* parsed JSON response. Throws if the API replies with an error.
*
* @param {string} url - The endpoint to post to.
* @param {Object} body - The request body, sent as JSON.
* @param {string} token - The bearer token for the Authorization header.
* @return {Object} The parsed JSON body.
*/
function postJson(url, body, token) {
const res = UrlFetchApp.fetch(url, {
// 1. The HTTP method — UrlFetchApp defaults to GET, so set it.
method: 'post',
// 2. contentType tells the API the payload is JSON.
contentType: 'application/json',
// 3. The bearer token authenticates the request.
headers: { Authorization: 'Bearer ' + token },
// 4. payload must be a string — serialise the object first.
payload: JSON.stringify(body),
// 5. Read error responses instead of throwing on them.
muteHttpExceptions: true,
});
// 6. Any status of 400 or above is a failure.
if (res.getResponseCode() >= 400) throw new Error(res.getContentText());
return JSON.parse(res.getContentText());
}
What comes back. Calling it like this:
const created = postJson(
'https://api.example.com/v1/leads',
{ name: 'Ada Lovelace', email: '[email protected]' },
'sk_live_abc123'
);
sends POST https://api.example.com/v1/leads with the JSON body and the
Authorization: Bearer sk_live_abc123 header, and parses a response such as:
{ "id": "lead_5821", "status": "created" }
so created.id is "lead_5821". Never hard-code the token — keep it in
Script Properties; see
Store API keys and secrets securely.
Reading the response
Both patterns above lean on the same HTTPResponse object. It is worth knowing
what else it offers, because not every request returns JSON.
/**
* Demonstrates the parts of an HTTPResponse worth knowing.
*/
function inspectResponse() {
const res = UrlFetchApp.fetch('https://api.example.com/v1/ping', {
muteHttpExceptions: true,
});
// The HTTP status code as a number, e.g. 200, 404, 503.
const code = res.getResponseCode();
// The raw response body as a string. Parse it only if it is JSON.
const text = res.getContentText();
// Response headers as an object, useful for rate-limit info.
const headers = res.getAllHeaders();
Logger.log(code + ' — ' + text);
Logger.log('Rate limit remaining: ' + headers['x-ratelimit-remaining']);
}
What comes back. getResponseCode() returns a plain number you can branch
on; getContentText() returns the body verbatim — so a non-JSON endpoint that
replies with pong is read safely without a JSON.parse crash; and
getAllHeaders() exposes headers like x-ratelimit-remaining that many APIs
use to tell you how much quota you have left.
Watch out for
- The daily fetch quota. Consumer Google accounts get 20,000
UrlFetchAppcalls per day; Workspace accounts get 100,000. A job that loops over a large list can exhaust it — batch where the API allows it. - The 6-minute execution limit. A script that makes hundreds of slow requests in one run will time out. Process in chunks and continue on the next trigger.
- Always set
muteHttpExceptions: true. Without it, any 4xx or 5xx response throws before you can read the body — and the body is usually where the API explains what went wrong. payloadmust be a string. For JSON, callJSON.stringifyyourself; passing a raw object sends form-encoded data instead.- Response timeouts.
UrlFetchAppwaits up to about 60 seconds for a reply. A slow endpoint will throw a timeout you should catch. - Secrets never go in code. API keys and tokens belong in Script Properties, not in the script body where they end up in version history.
- For high-volume or rate-limited APIs, a plain loop is not enough — you need backoff and retry. See Build a rate-limit-aware API client.
Related
Handle streaming responses from an LLM API
Manage long Northwind AI outputs reliably — note: Apps Script UrlFetch is synchronous.
Updated Jan 3, 2026
Cache API responses to cut quota usage
Store and reuse Northwind API responses intelligently — sub-second hits, fewer bills.
Updated Dec 26, 2025
Build an API-key vault and rotation system
Manage Northwind credentials securely at scale — centralised storage, scheduled rotation.
Updated Dec 22, 2025
Build a rate-limit-aware API client
Back off and retry gracefully on 429s — Northwind's robust outbound HTTP pattern.
Updated Dec 14, 2025
Build a generic paginated-API fetcher
Handle cursors and pages for any large dataset — Northwind's standard pull pattern.
Updated Dec 6, 2025