appscript.dev
Automation Beginner

Send SMS notifications with Twilio

Text Northwind alerts straight from your scripts — for production outages or VIP events.

Published Jul 19, 2025

Email and chat notifications are fine for the routine stuff, but some alerts cannot wait for someone to open their laptop. When Northwind’s production checkout goes down at 2am, or a VIP account signs a renewal, the on-call engineer needs a buzz in their pocket — and an SMS is the one channel that reliably gets through.

Twilio sends a text message over a simple HTTP request, which means Apps Script can do it with UrlFetchApp and no add-on libraries. This script wraps that request in a sendSms helper, so any of your other scripts can text a number in one line. Keep it for the genuinely urgent alerts — texts cost money and deserve to mean something.

What you’ll need

  • A Twilio account, plus a Twilio phone number that can send SMS to your destination countries.
  • Your Twilio Account SID and Auth Token, both on the Twilio console dashboard.
  • Three values saved in Script Properties — see Store API keys and secrets securely:
    • TWILIO_SID — your Account SID.
    • TWILIO_TOKEN — your Auth Token (a secret; never paste it into the code).
    • TWILIO_FROM — your Twilio number in E.164 format, e.g. +447700900123.
  • Destination numbers also in E.164 format (a +, the country code, then the number, with no spaces).

The script

// Twilio's REST endpoint. The {SID} placeholder is filled in at call time
// because the account SID is part of the URL path.
const TWILIO_BASE = 'https://api.twilio.com/2010-04-01/Accounts/';

/**
 * Sends a single SMS through Twilio.
 *
 * @param {string} to    The destination number in E.164 format (+447700900123).
 * @param {string} body  The message text.
 * @return {GoogleAppsScript.URL_Fetch.HTTPResponse} The Twilio API response.
 */
function sendSms(to, body) {
  const props = PropertiesService.getScriptProperties();
  const sid = props.getProperty('TWILIO_SID');
  const token = props.getProperty('TWILIO_TOKEN');
  const from = props.getProperty('TWILIO_FROM');

  // Bail out clearly if the credentials were never set, rather than
  // sending a malformed request and reading a cryptic 401.
  if (!sid || !token || !from) {
    throw new Error('Missing Twilio credentials in Script Properties.');
  }

  // Twilio uses HTTP Basic auth: the username is the SID, the password is
  // the auth token. base64Encode turns "sid:token" into the header value.
  const auth = Utilities.base64Encode(sid + ':' + token);

  // The request body is form-encoded (not JSON). Passing a plain object as
  // "payload" makes UrlFetchApp send it as application/x-www-form-urlencoded.
  const response = UrlFetchApp.fetch(TWILIO_BASE + sid + '/Messages.json', {
    method: 'post',
    headers: { Authorization: 'Basic ' + auth },
    payload: { From: from, To: to, Body: body },
    muteHttpExceptions: true,
  });

  // 201 Created means Twilio queued the message. Anything else is a failure
  // worth surfacing — an unverified number, no balance, a bad credential.
  const code = response.getResponseCode();
  if (code !== 201) {
    Logger.log('Twilio error ' + code + ': ' + response.getContentText());
  }
  return response;
}

How it works

  1. TWILIO_BASE holds the constant part of the REST URL. The account SID is part of the path, so it is appended at call time rather than baked into the constant.
  2. sendSms reads all three credentials from Script Properties and throws a clear error if any are missing — far easier to debug than a 401 from Twilio.
  3. Twilio authenticates with HTTP Basic auth, where the SID is the username and the auth token is the password. Utilities.base64Encode builds the Basic ... header value from sid:token.
  4. The request body is form-encoded, not JSON. Passing a plain object as payload tells UrlFetchApp to send it as application/x-www-form-urlencoded, which is exactly what Twilio’s Messages endpoint expects.
  5. A queued message returns HTTP 201 Created. Any other status — an unverified recipient on a trial account, no balance, a typo in a credential — is logged with Twilio’s own error body so you can see what went wrong.

Example run

Call the helper from another script at the point an alert fires:

sendSms('+447700900000', 'Northwind: production checkout is down — investigating.');

The destination handset receives a text from your Twilio number reading Northwind: production checkout is down — investigating. Twilio’s API returns a 201 response whose JSON body includes a message SID and a status of queued, which you can see in the execution log if you need to trace it.

Run it

sendSms is a helper, not a scheduled job — call it from your existing scripts wherever an urgent event happens:

  • From an uptime check, when a health endpoint fails twice in a row.
  • From a billing script, when a high-value renewal is signed.

To confirm your setup before wiring it in, run a one-off test from the editor:

function testSms() {
  sendSms('+447700900000', 'Northwind SMS test — ignore.');
}

Watch out for

  • Every number must be in E.164 format: a leading +, the country code, then the number, no spaces or dashes. Twilio rejects anything else.
  • Trial accounts can only text numbers you have verified in the Twilio console, and prepend a trial banner to every message. Upgrade before relying on this in production.
  • SMS costs money per message, and prices vary by destination country. Reserve it for genuinely urgent alerts; route routine updates to email or chat.
  • A single SMS segment is 160 characters (70 if it contains emoji or other non-GSM characters). Longer messages are split into multiple billed segments, so keep alerts terse.
  • The auth token is a full credential — anyone who has it can send messages on your account and run up your bill. Keep it in Script Properties and rotate it in the Twilio console if it ever leaks.
  • sendSms returns once Twilio has queued the message, not once it is delivered. To confirm delivery, configure a status-callback webhook on the Twilio side.

Related