Free tool · no sign-up

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.

Endpoint

The URL and HTTP method.

Payload

What goes in the body.

Headers

Extra request headers, set as you'd expect.

Options

The flags that show up in real production scripts.

Error handling

Wrappers to make the call robust.

Apps Script snippet

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());
  }
}
The options that actually matter

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.
Payload shape pitfalls

The three things Apps Script gets wrong by default

  • JSON payload must be a string, not an object. Pass JSON.stringify(obj), not obj. If you pass an object and set Content-Type to application/json, Apps Script serialises as form-encoded instead and Slack throws invalid_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-urlencoded automatically.
  • 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.

Retry semantics

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.

When to use this vs the script generator

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.

FAQ

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.