appscript.dev
Automation Intermediate

Build a WhatsApp notification sender

Push Northwind updates via the WhatsApp Business API — for client billing milestones.

Published Jul 23, 2025

Northwind’s clients do not read email the day it arrives, and a billing milestone buried in an inbox gets missed. Most of them, though, check WhatsApp constantly — so when an invoice is raised or a project hits a payment stage, Northwind wants that note to land in WhatsApp, where it will actually be seen.

This script wraps the WhatsApp Business API in one small sendWhatsApp helper. Give it a destination number and a message and it does the rest. It is the piece you call from any other automation — an invoice script, a project tracker — whenever a client needs a nudge.

What you’ll need

  • A WhatsApp Business Platform account with a registered phone number, set up through Meta’s developer console.
  • Two values saved in Script Properties — see Store API keys and secrets securely:
    • WA_PHONE_ID — the Phone Number ID of your WhatsApp sender.
    • WA_TOKEN — a permanent access token for the WhatsApp Business API.
  • Recipient numbers in international format without a + or spaces (e.g. 441632960111).

The script

// WhatsApp Business API version. Bump this when Meta deprecates it.
const WA_API_VERSION = 'v18.0';

/**
 * Sends a plain-text WhatsApp message through the WhatsApp Business API.
 *
 * @param {string} to       Recipient number, international format, no '+'.
 * @param {string} message  The message text to send.
 */
function sendWhatsApp(to, message) {
  const p = PropertiesService.getScriptProperties();
  const phoneId = p.getProperty('WA_PHONE_ID');
  const token = p.getProperty('WA_TOKEN');

  // Bail out early rather than firing a request that is bound to fail.
  if (!phoneId || !token) {
    throw new Error('WA_PHONE_ID and WA_TOKEN must be set in Script Properties.');
  }

  // The endpoint is scoped to your sender's Phone Number ID.
  const url = 'https://graph.facebook.com/' + WA_API_VERSION +
    '/' + phoneId + '/messages';

  const res = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + token },
    payload: JSON.stringify({
      messaging_product: 'whatsapp',
      to: to,
      type: 'text',
      text: { body: message },
    }),
    muteHttpExceptions: true,
  });

  // A non-2xx status means Meta rejected the message — surface the reason
  // instead of letting the caller assume it was delivered.
  const code = res.getResponseCode();
  if (code < 200 || code >= 300) {
    throw new Error('WhatsApp API error (' + code + '): ' + res.getContentText());
  }
}

/**
 * Example caller: notify a client that their invoice is ready.
 *
 * @param {string} clientPhone  Recipient number, international format.
 * @param {string} invoiceNo    The invoice reference.
 */
function notifyInvoiceRaised(clientPhone, invoiceNo) {
  sendWhatsApp(
    clientPhone,
    'Hi from Northwind — invoice ' + invoiceNo + ' is ready to view in your portal.'
  );
}

How it works

  1. sendWhatsApp reads WA_PHONE_ID and WA_TOKEN from Script Properties, so no credentials ever sit in the code.
  2. It guards on those two values. If either is missing it throws straight away, which is a clearer failure than a 400 from Meta about a malformed request.
  3. It builds the Graph API URL, scoped to your sender’s Phone Number ID, with the API version pulled out into the WA_API_VERSION constant for easy upgrades later.
  4. It POSTs a JSON body in the shape WhatsApp expects — messaging_product, the recipient to, a type of text, and the message under text.body.
  5. It checks the HTTP status. A non-2xx response throws, carrying Meta’s actual error text, so a delivery failure never looks like a success.
  6. notifyInvoiceRaised shows the intended use: a thin wrapper that builds one specific message and hands it to sendWhatsApp.

Example run

From another script, or straight from the editor:

sendWhatsApp('441632960111', 'Your Northwind project hit milestone 2 — payment is now due.');
InputValue
to441632960111
messageYour Northwind project hit milestone 2 — payment is now due.

The client’s phone shows a WhatsApp message from the Northwind business number:

Your Northwind project hit milestone 2 — payment is now due.

If the number is wrong or the token has expired, sendWhatsApp throws with the exact error Meta returned, so you can fix the cause rather than guess.

Run it

sendWhatsApp is a helper, not a scheduled job — you call it from whatever automation reaches the right moment:

  1. From the Apps Script editor, select notifyInvoiceRaised (or call sendWhatsApp directly) and click Run to send a test message to your own number.
  2. Approve the authorisation prompt the first time.
  3. In real use, call sendWhatsApp from your invoice or project-tracking script at the point a client needs to hear from you.

Watch out for

  • WhatsApp has a 24-hour window rule. Free-form text messages only reach a client who has messaged your business in the last 24 hours. Outside that window — which is most of the time for a billing nudge — you must send a pre-approved message template, not the plain text body shown here.
  • Message templates need Meta’s approval before use. Submit your billing and milestone templates in the WhatsApp Manager and switch the payload to type: "template" once they are approved.
  • Access tokens expire. Temporary tokens last hours; generate a permanent System User token for production and store it as WA_TOKEN.
  • The recipient number must be in international format with no +, no spaces, and no leading zeros. A malformed number fails or, worse, reaches someone else.
  • Meta deprecates Graph API versions on a schedule. The WA_API_VERSION constant is there so the upgrade is a one-line change when that happens.
  • WhatsApp messaging is billed per conversation. High volume adds up — check the current pricing tiers before wiring this into a busy workflow.

Related