Pin a stable web app URL with versioned deployments

TL;DR — Keep one stable Apps Script web app URL while you ship new versions: create a versioned deployment for webhooks, avoid ‘edit latest’ drift, and stop mystery 404s after you redeploy.

Webhooks and Zapier "Custom Request" steps hate moving targets. If you Deploy → New deployment every time you tweak doPost, partners get a new /exec URL — or worse, you keep "Test deployments" and wonder why production still runs last month’s code. Pin one web app URL; ship versions behind it.

TL;DR

  • Create a versioned web app deployment once; reuse that deployment ID.
  • Prefer Manage deployments → Edit → Version: New version over minting a brand-new deployment.
  • "Test deployments" / HEAD are for you — not for customer webhooks.
  • Execute-as and access settings matter as much as the URL (see execute-as guides).
  • After a bad push, roll forward with another version — don’t thrash URLs.

Using the Apps Script editor a lot? NitroGAS drops free themes & snippets right into script.google.com — optional Co-Pilot when you want a boost.

Why webhooks 404 after a "redeploy"

Common bootstrapper path:

  1. Deploy as web app → copy /exec into Stripe / HubSpot / a Sheet cell
  2. Change code → New deployment again → new URL
  3. Old URL returns 404 or an ancient version
  4. Support thread: "your webhook is broken"

The fix isn’t more retries. It’s treating the deployment like a stable endpoint.

New deployment vs new version

Action URL Who should use it
New deployment (web app) New /exec First time only (or intentional second endpoint)
Edit deployment → New version Same /exec Everyday code ship
Test deployment Separate test URL You, locally / QA

In the Apps Script UI: Deploy → Manage deployments → pencil → Version → New version → Deploy.

From clasp (when you use it): deploy with the existing deployment id rather than creating another.

Minimal doPost that stays boring

function doPost(e) { try { var body = e && e.postData && e.postData.contents ? JSON.parse(e.postData.contents) : {}; var result = handleWebhook_(body, e); return ContentService .createTextOutput(JSON.stringify({ ok: true, result: result })) .setMimeType(ContentService.MimeType.JSON); } catch (err) { return ContentService .createTextOutput(JSON.stringify({ ok: false, error: err && err.message ? err.message : String(err) })) .setMimeType(ContentService.MimeType.JSON); } } function handleWebhook_(body, e) { // validate signature / shared secret here // enqueue work; return fast return { received: true }; }

Keep the HTTP handler thin. Queue sheet rows or CacheService work for the heavy lift so webhook timeouts don’t tempt you into "just redeploy again."

Checklist when you change execute-as or access

URL stability doesn’t help if auth flips:

  • Execute as: Me — your quotas / Drive; good for trusted inbound webhooks
  • Execute as: User accessing — each caller; often wrong for server-to-server
  • Who has access — "Anyone" vs "Anyone with Google account" vs domain

Document the pair (URL + execute-as + access) in the client runbook. Complements the queued execute-as and webhook guides.

How to verify after shipping a version

function smokeWebApp_() { // Paste your STABLE /exec URL — not a test deployment var url = PropertiesService.getScriptProperties().getProperty('WEBAPP_URL'); var resp = UrlFetchApp.fetch(url, { method: 'post', contentType: 'application/json', payload: JSON.stringify({ ping: true }), muteHttpExceptions: true }); Logger.log(resp.getResponseCode() + ' ' + resp.getContentText()); }

Store WEBAPP_URL in Script Properties so menus and docs don’t drift from the live endpoint.

Soft Co-Pilot note

NitroGAS Co-Pilot can help sketch doPost + property checks; the deployment discipline (one URL, many versions) is still on you. Free extension; Co-Pilot optional.

Closing checklist

  • One production /exec URL recorded (Script Property + runbook)
  • Code ships as new version on that deployment
  • Test deployment URL never given to vendors
  • Execute-as / access documented beside the URL
  • Smoke POST after each version deploy

Happy Coding!