Send yourself WhatsApp alerts from a script (cron, CI, uptime check)

Get a WhatsApp message on your own phone when a cron job, CI run or health check fails. One approved template, one REST call, a Node script, a bash cron check and a GitHub Actions step, with no double alerts on retries.

November 7, 2026 · 7 min read

Email alerts get buried and Slack gets muted. A message on your own phone is harder to miss, and sending one from a script takes a single request. This guide sets up a small alert for a failed cron job, a failed CI run or a health check that stops answering.

SendFromChat is an independent product and is not affiliated with Meta or WhatsApp. It runs on Meta's official WhatsApp Cloud API; you keep your own WhatsApp Business account. Endpoints, fields and errors below come from our REST API reference, read on 7 Nov 2026.

What was tested: the Node script and the bash check were run on Node 24 and bash against a small local mock server, to check the request shape, the idempotency header and the exit codes. The GitHub Actions step was not run at all. Nothing here was sent through a live SendFromChat key or real WhatsApp traffic. Try it with your own key and your own number first.

The short version

  • Your alert goes out as a template with POST /messages. An alert can fire at any hour, and free-form text only delivers within 24 hours of the person's last message to you, so a template is the dependable route (why).
  • The phone that gets the alert is your own phone, which must be a different number from the business number you connected.
  • Send an Idempotency-Key built from the event, so a retried step or a repeated cron run does not send twice.
  • A 202 means accepted, not delivered. Check the status if you need to know.

What you need first

  1. A SendFromChat workspace with a number connected. A test key also needs a connected number to check against. See getting started.
  2. A card on your WhatsApp Business account at Meta before you can send templates. Meta bills it directly, separate from your SendFromChat plan. The 30-day trial itself needs no card.
  3. An API key from the dashboard's Developers page. Use a test key (sfc_test_…) while you build: it runs every check a real send would, then stops. Nothing is sent. Switch to a live key (sfc_live_…) when it is ready.
  4. Your own phone number in international format, for example +447700900123 (a made-up number; use yours).

Keep the key on your server or in your CI secrets. Never put it in browser code or commit it.

1. Create the alert template

Create one small template and reuse it for every job. In the dashboard, or by asking your assistant to create it, use text like this:

Alert from {{1}}: {{2}}. Check the logs and run it again if needed.

Name it job_alert, language en_US, and give each variable a sample value when you submit (for example {{1}} → "nightly-backup", {{2}} → "exit code 1"). Meta rejects templates without samples, or ones that start or end on a variable. The rules are in why WhatsApp templates get rejected.

Two things to expect:

  • Meta decides the category, not you. You can suggest one, and Meta may change it. Do not assume what it will be, or what it will cost. Meta's pricing page is the source for charges.
  • Review usually takes minutes, sometimes up to 24 hours. Wait for the status to say approved before you switch to a live key. A test key will tell you if the template is not approved yet (template_not_approved).

2. One script for any job (Node 18+)

Save this as alert.mjs. It needs no packages, only the built-in fetch.

// alert.mjs: send one WhatsApp alert to your own phone.
// Usage: node alert.mjs <job> <what> <runId>
const API = process.env.SENDFROMCHAT_API ?? "https://sendfromchat.com/api/v1";
const [job, what, runId] = process.argv.slice(2);
if (!job || !what || !runId) {
  console.error("usage: node alert.mjs <job> <what> <runId>");
  process.exit(2);
}

const body = {
  to: process.env.ALERT_TO, // your own phone, international format
  template: {
    name: "job_alert",
    language: "en_US",
    variables: [job, what],
  },
};

const res = await fetch(`${API}/messages`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SENDFROMCHAT_KEY}`,
    "Content-Type": "application/json",
    // same run = same key, so a retried step never sends twice
    "Idempotency-Key": `alert-${job}-${runId}`,
  },
  body: JSON.stringify(body),
  signal: AbortSignal.timeout(10_000),
});
const json = await res.json().catch(() => ({}));

if (res.ok) {
  console.log(`alert ${json.status} (${res.status})`);
} else {
  console.error(`alert not sent: ${res.status} ${json.error?.type ?? "unknown"}`);
  process.exit(1);
}

Run it:

export SENDFROMCHAT_KEY=sfc_test_...
export ALERT_TO=+447700900123
node alert.mjs nightly-backup "exit code 1" 8841

With a test key it prints alert test (200): every check passed and nothing was sent. With a live key it prints alert accepted (202).

On failure it exits non-zero and prints the error type, such as template_not_approved, opted_out or no_number. Branch on type, not on the message text. The full table is in the errors section of the reference.

3. A cron health check in bash

For a service that should answer on a URL, a cron line can check it and alert when it does not. Save this as healthcheck.sh:

#!/usr/bin/env bash
# healthcheck.sh: run from cron every 5 minutes. Alerts once per hour while the check keeps failing.
set -u
URL="https://example.com/health"

if curl -fsS --max-time 10 "$URL" > /dev/null; then
  exit 0
fi

curl -sS --fail-with-body --max-time 10 -X POST "https://sendfromchat.com/api/v1/messages" \
  -H "Authorization: Bearer $SENDFROMCHAT_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: alert-health-$(date -u +%Y%m%d%H)" \
  -d "{\"to\":\"$ALERT_TO\",\"template\":{\"name\":\"job_alert\",\"language\":\"en_US\",\"variables\":[\"health check\",\"$URL is not answering\"]}}"

--fail-with-body makes curl exit non-zero on a 4xx or 5xx reply (needs curl 7.76 or newer) and still prints the error body, so a failed alert is visible in cron's output instead of exiting 0 silently.

And the crontab entry (cron does not read your shell profile, so set the variables on the line or in the crontab):

*/5 * * * * SENDFROMCHAT_KEY=sfc_live_... ALERT_TO=+447700900123 /home/you/healthcheck.sh

The Idempotency-Key ends with the date and hour. Per the reference, a retry with the same key and body returns the first response instead of sending again, so while the check keeps failing, repeated runs inside the same hour do not send twice. That only works because the body is identical each time. Do not put a timestamp in the template variables, or the same key with a different body returns idempotency_mismatch. WhatsApp already shows the time of the message.

4. A GitHub Actions step for failed runs

Add this as the last step of a job. if: failure() makes it run only when an earlier step failed. Put the key and your number in the repository's secrets.

      - name: WhatsApp alert on failure
        if: failure()
        env:
          SENDFROMCHAT_KEY: ${{ secrets.SENDFROMCHAT_KEY }}
          ALERT_TO: ${{ secrets.ALERT_TO }}
          WORKFLOW: ${{ github.workflow }}
          REF: ${{ github.ref_name }}
          RUN_ID: ${{ github.run_id }}
        run: node alert.mjs "$WORKFLOW" "failed on $REF" "$RUN_ID"

The script file has to be in the repository and checked out in that job. The run id in the idempotency key means a re-run of the same step does not send a second alert for the same run. The workflow name, branch name and run id are passed as environment values, not pasted into the shell line, so a branch name with odd characters cannot run commands. This step was not run: check the quoting in your own workflow.

5. Keep alerts rare enough to read

  • One alert per event, not per attempt. Build the idempotency key from the event (job and run, or job and hour), never from the time of the attempt.
  • Alert on the state change. A job that fails 50 times in a night should message you once an hour at most, as in the bash check, or once when it first fails.
  • Alerts count like any other message. Messages sent through the API count toward your plan's monthly allowance, and a brand-new number has a daily cap on template sends in its first days (details). That cap is a 429 warmup_limit.

6. Know whether it arrived

The 202 accepted response means the request was accepted. To know what happened to the message, read GET /messages/{id}, or subscribe to the message.status webhook. The status moves from sent to delivered to read, or ends in failed with a reason. The delivery-status guide shows both (track sent, delivered and read).

If an alert never shows up, start with the error codes guide and the failed reason on the message.

Try it

Create a test key on the Developers page, run node alert.mjs as above and confirm you get alert test (200). Once the template is approved and a live key is in place, run it once on purpose from a job that you break deliberately.

Do not have an account yet? There is a 30-day free trial, no card needed to start: sign up. For the same call in more languages, see send WhatsApp messages from Node.js and test the WhatsApp API without sending a real message.

Sources

  • SendFromChat REST API reference: POST /messages, test and live keys, Idempotency-Key, GET /messages/{id}, webhooks, errors and limits. Read 7 Nov 2026.
  • SendFromChat getting started and templates: connecting a number, card at Meta for templates, template review time. Read 7 Nov 2026.
  • Meta's own pages decide template categories and charges. We did not retrieve them for this guide.

Do this from your AI assistant

SendFromChat connects Claude, ChatGPT, Cursor or any MCP client to the WhatsApp Business API — templates, bulk sends, inbox, chatbots and sequences, just by asking. 30 days free, no card.

Start free