Free Apps Script UrlFetchApp.fetch helper
Build the call with the right options object shape, payload encoding, headers, and error-handling wrapper. Get a copy-paste Apps Script snippet without guessing which flag does what.
The URL and HTTP method.
What goes in the body.
Extra request headers, set as you'd expect.
The flags that show up in real production scripts.
Wrappers to make the call robust.
Paste into your Apps Script editor.
function callEndpoint() {
try {
const options = {
method: "post",
headers: {
"Content-Type": "application/json",
},
payload: JSON.stringify({
"text": "Hello from Apps Script"
}),
muteHttpExceptions: true,
};
let response, status, body;
const MAX_ATTEMPTS = 3;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
response = UrlFetchApp.fetch("https://hooks.slack.com/services/T.../B.../...", options);
status = response.getResponseCode();
body = response.getContentText();
if (status < 500 && status !== 429) break;
// Backoff: 1s, 2s, 4s.
Utilities.sleep(Math.pow(2, attempt - 1) * 1000);
}
// status + body are now set. Branch on status code or response shape.
if (status >= 200 && status < 300) {
Logger.log("OK: " + body);
} else {
Logger.log("HTTP " + status + ": " + body);
}
} catch (err) {
// UrlFetchApp can throw for DNS / network errors or when
// muteHttpExceptions is false and the endpoint returned 4xx/5xx.
Logger.log("UrlFetchApp threw: " + err.toString());
}
}
Three flags every webhook call should think about
muteHttpExceptions: true: the default behaviour throws on 4xx/5xx, which makes it impossible to read Slack's actual error response body. Almost every production script wants this on so you can branch on the status code in code rather than in a try/catch.followRedirects: leave on (default true). The one case for false is when you specifically want to inspect a redirect target before following it. For webhook-style calls, the endpoint should never redirect.validateHttpsCertificates: leave on. Disabling silently opens you to MITM. Only the rare local-dev case justifies it.
The three things Apps Script gets wrong by default
- JSON payload must be a string, not an object. Pass
JSON.stringify(obj), notobj. If you pass an object and set Content-Type to application/json, Apps Script serialises as form-encoded instead and Slack throwsinvalid_payload. Counterintuitive but consistent. - Form-encoded payload must be an object, not a string.Apps Script handles the URL-encoding when payload is an object literal. Don't set Content-Type yourself either. Apps Script sets
application/x-www-form-urlencodedautomatically. - Raw text payload needs Content-Type explicitly. Default for a string payload is also form-encoded; pass
headers: { 'Content-Type': 'text/plain' }alongside.
The helper above handles all three for you, pick the payload shape and the generated code is correct without you having to remember which serialisation surface Apps Script expects.
What the wrapper actually does
The retry wrapper does exponential backoff on 429 and 5xx status codes only: three total attempts at 0s, 1s, 2s. It doesn't retry on 4xx because those are configuration errors (wrong URL, malformed body, revoked auth) that won't fix themselves. It doesn't retry on 200-299 because those succeeded.
- 429 (rate-limited): Slack rate-limits incoming webhooks per app per workspace. The retry gives the burst time to clear before retrying.
- 500 / 502 / 503 / 504: usually transient server issues on the receiving end. The retry pattern catches the brief outage.
- Other 5xx: same logic; treat as transient and retry.
For idempotency at the protocol level (so retries don't produce duplicate Slack posts), the receiver has to dedupe, usually on a request ID you include in the payload. RouteForms uses the Google Forms response ID as the dedupe key at the database layer.
The boundary
The script generator at /tools/google-forms-to-slack-generator gives you an entire onFormSubmit handler in 90 seconds, wired to a Slack webhook, installer included, ready to paste. That's the right starting point if you want the canonical form-to-Slack pipeline.
This helper is for the cases the script generator doesn't cover: calling a non-Slack endpoint with auth headers, layering retries on top of an existing handler, sending a form-encoded payload to a legacy receiver, testing UrlFetchApp's behaviour against a custom server. Use the script generator for the main path; come here for the edge cases.
Frequently asked questions
What does this helper do?▾
It assembles a UrlFetchApp.fetch(url, options) call for you, with the right options object shape, the correct payload encoding for the body type, and optional try/catch and retry wrappers. You pick the method, payload shape, headers, and flags from the UI; we emit Apps Script code you paste into your project.
How is this different from the Apps Script generator?▾
Different scope. /tools/google-forms-to-slack-generator emits an entire onFormSubmit handler wired to a specific webhook URL, installer, trigger, message build, fetch, everything. This helper just builds the single fetch call, with full control over options and error handling. Use it when you're writing a custom Apps Script that the generator doesn't fit, calling a non-Slack endpoint, handling auth headers, layering retries on top of an existing handler.
What payload shapes are supported?▾
(1) JSON, body is JSON.stringify(...) with Content-Type: application/json. (2) Form-encoded, body is an object literal and Apps Script serialises it as application/x-www-form-urlencoded automatically (don't set Content-Type yourself). (3) Raw text, body is a plain string with Content-Type: text/plain. (4) None, for GET / DELETE or when the endpoint expects an empty body.
Why muteHttpExceptions: true?▾
By default, UrlFetchApp throws when the endpoint returns a 4xx or 5xx status, so a 404 from Slack throws an exception before you can read the response body. With muteHttpExceptions: true, the call returns the response object regardless, and you check response.getResponseCode() yourself. Critical for retry logic: you can't decide to retry on 429 if Apps Script threw on 429 instead of returning it.
What does the retry wrapper do?▾
Exponential backoff retry on 429 and 5xx. Three attempts total: immediate, then 1-second sleep, then 2 seconds. Stops early on any other status. Useful when Slack rate-limits during a burst of submissions, or when a transient 500 from your endpoint would otherwise lose the response. Doesn't retry on 4xx because those are configuration errors that won't fix themselves.
When should I disable followRedirects?▾
Very rarely, almost always leave it on. The case for disabling is if you specifically want to see and handle the 3xx Location header yourself (e.g. tracking which URL a service redirected you to). For webhook-style calls, the endpoint should never redirect, and any 3xx you see is a misconfiguration on the receiving side.
Should I disable validateHttpsCertificates?▾
No, almost never. The only legitimate case is testing against a local server with a self-signed cert during development, and even then it's risky. Disabling silently opens you to man-in-the-middle attacks for the duration. Don't ship a production script with this flag off.
What happens if my JSON payload is invalid?▾
The helper detects unparseable JSON and falls back to embedding the input as a string literal in the generated code, your script will compile, but it'll send a quoted string instead of the JSON object you intended. The fix: paste valid JSON in the payload field. The Apps Script editor will also surface parse errors if you don't notice.
Skip UrlFetchApp entirely
RouteForms's installer is the only fetch call you'll need, we handle the retries, dedupe, and Slack-side error decoding on our infrastructure. Free for 30 responses a month.
Keep reading
If you want a whole form-to-Slack script, not just one fetch call, the generator gives you the install path in 90 seconds.
If your fetch call throws something, the decoder maps the error to plain English.
Before you wire up the fetch, verify the URL works by sending a test from our infrastructure.
All the real options for delivering Google Form responses to Slack, ranked by fit.