WhatsApp message when a GitHub Actions run fails

One step, one secret, one curl. A failed run messages you on WhatsApp with a link straight to the logs — and there is no Twilio account anywhere in it.

Every existing answer to this wraps Twilio: an account, a WhatsApp sender, a sandbox to join and a bill with a per-message rate. The marketplace actions for it are all the same shape.

This is a curl in a step. No action to trust, no account but this one, and the key is the only secret.

1. Make a key

In the Jabee dashboard, go to Management then Approvals and create an API key. It starts jak_. Copy it now; it is not shown again.

Whoever is going to be messaged needs a verified phone, which they get by tapping a link in the dashboard and sending one message. Until somebody has done that there is nobody to deliver to.

2. Put the key in the repository

Settings → Secrets and variables → Actions → New repository secret, named JABEE_KEY. If several repositories will wake the same people, an organisation secret is the same thing in one place.

3. Add the step

      - name: WhatsApp me if this failed
        if: failure()
        env:
          JABEE_KEY: ${{ secrets.JABEE_KEY }}
          TITLE: "${{ github.workflow }} failed on ${{ github.ref_name }}"
          BODY: "${{ github.repository }} — ${{ github.actor }} — ${{ github.sha }}"
          RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
          EID: "gha-${{ github.run_id }}-${{ github.run_attempt }}"
        run: |
          jq -n --arg t "$TITLE" --arg b "$BODY" --arg u "$RUN_URL" --arg e "$EID" \
             '{title: $t, body: $b, url: $u, external_id: $e}' \
          | curl -sS -X POST https://api.jabee.ai/api/v1/alerts \
              -H "Authorization: Bearer $JABEE_KEY" \
              -H "Content-Type: application/json" \
              --data @-

Why it is written that way rather than as one long curl -d:

jq and curl are both already on the GitHub-hosted runners. On a self-hosted runner, check.

Telling me the whole run failed, not one job

if: failure() is scoped to its own job: the step runs when something earlier in that job failed. A workflow of five parallel jobs needs a sixth that waits for them:

  notify:
    needs: [build, test, lint]
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - name: WhatsApp me
        env:
          JABEE_KEY: ${{ secrets.JABEE_KEY }}
          TITLE: "${{ github.workflow }} failed on ${{ github.ref_name }}"
          RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
        run: |
          jq -n --arg t "$TITLE" --arg u "$RUN_URL" \
             --arg e "gha-${{ github.run_id }}-${{ github.run_attempt }}" \
             '{title: $t, body: "A job in this run failed.", url: $u, external_id: $e}' \
          | curl -sS -X POST https://api.jabee.ai/api/v1/alerts \
              -H "Authorization: Bearer $JABEE_KEY" \
              -H "Content-Type: application/json" --data @-

One message for the run, however many jobs went red. needs plus if: failure() is the whole trick.

What arrives

CI failed on main
acme/api — mumen — 4f2a9c1

https://github.com/acme/api/actions/runs/9912034

The link is the point. A notification you have to go and look something up from is a notification you start ignoring.

Asking before a deploy, rather than telling after it

The same key sends an approval: a message with Approve and Skip buttons that your workflow waits on before shipping. That is the approvals API, and in a workflow it is the same curl plus a poll on the returned id.

One honest caveat: secrets and forks

Secrets are not available to workflows triggered by a pull request from a fork, so this step cannot send on those runs — it will fail quietly on an empty key. That is GitHub's rule and it is the right one; use pull_request_target only if you already understand why that is dangerous.

What it costs

$29 a month for 500 alerts and approvals together, then 3 cents each. The first three are free, so you can wire this up and watch a message arrive before paying for anything. No per-seat charge.

Worth a thought on a busy repository: a flaky test suite on every push is a lot of messages. Put the step on the branches you care about (if: failure() && github.ref == 'refs/heads/main') rather than on all of them.

Questions

How do I get a WhatsApp message when a GitHub Actions workflow fails?

Add a step with if: failure() that POSTs a title, a body and the run URL to https://api.jabee.ai/api/v1/alerts with your key on the Authorization header, exactly as above. No Twilio account, no phone number of your own and no WhatsApp Business account: the number and the template are Jabee's.

Why does my notify step not run when the workflow fails?

Because if: failure() only looks at the job it is in. If the failing job is a different one, add a separate job with needs listing the jobs you care about and if: failure() at the job level — which also gives you one message per run rather than one per failed job.

Will re-running a failed job send another WhatsApp message?

Yes, and deliberately: the external_id includes github.run_attempt, so a re-run is a new alert. A retried curl within one attempt is the same id and delivers nothing twice.

Can it ask me to approve a deploy instead of telling me it broke?

Yes. The same key and nearly the same call sends an approval with Approve and Skip buttons, and the workflow waits for the answer before deploying — the approvals API, against the same allowance.

Do I need a WhatsApp number or a Meta account?

No, neither, at any point. The message goes over Meta's official Cloud API on Jabee's registered number using Jabee's approved template, so you need an API key and nothing else. There is no number of yours involved, so there is nothing of yours to get banned.

Can it message my customers?

No, deliberately. It reaches your own people, once they have verified their phone. A shared number messaging strangers is what gets numbers banned. To reach customers you connect your own number, which is the inbox.

What happens if nobody ever answers the messages?

Five notifications in a row that a handset never replies to and we stop sending to it, with an email to the account owner saying so. Sending any message to the Jabee number starts it again. One number's standing with Meta is shared by every business on it, which is why the rule exists.