SMSTunnel for GitHub Actions
A text from your workflows, from your own phone and number
Know when a deployment fails
Get a text when a deployment fails, when a release goes out, or when a nightly job needs you. The workflow hands the text to your SMSTunnel account and your own Android phone sends it from its own SIM, so you get it from a number you know, even where there is no internet. Delete the workflow's API key in your dashboard and it can't text anymore.
What it does
- A text from a step
- One step texts you, or each number on a list (at most 30), one text per number.
- Sign-in codes
- A code of 4 to 10 digits to one number, on its own daily allowance.
- Message IDs
- The IDs of the texts as an output, and a table in the job summary.
- Numbers stay private
- It never prints the numbers it texts, and hides the key and the numbers if anything else prints them.
- Clear failures
- The step fails with SMSTunnel's own explanation for each number. Too many requests: it waits and tries once more.
- Any runner
- GitHub-hosted and self-hosted runners.
1. Get your key
- 1. Install the SMSTunnel app from Google Play on your Android phone and sign in.
- 2. In your dashboard, open Apps → Add an app, pick GitHub Actions, name it (for example "GitHub Actions") and click Create the key.
- 3. Copy the key right away: it's shown only once. You can stop or delete the app there anytime, and GitHub Actions can't send texts anymore.
Get the steps with your key in them
It opens Add an app with GitHub Actions picked. Once the key is made, the dashboard shows every step and command with the key already filled in, ready to copy.
2. Save it in your repository
- 1. In your repository, open Settings → Secrets and
variables → Actions → New repository secret and save the key as
SMSTUNNEL_API_KEY. - 2. In a public repository, save the number too
(
ALERT_PHONEin the examples): workflow files and logs are public. The action never prints the numbers it texts, and hides the key and the numbers if anything else prints them.
3. Add the step
- name: Text me
uses: narcisbodea/smstunnel-action@v1
with:
api-key: ${{ secrets.SMSTUNNEL_API_KEY }}
to: ${{ secrets.ALERT_PHONE }}
message: "${{ github.repository }}: the deployment failed. ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" Inputs
- api-key (required)
- Your SMSTunnel API key. Pass it from a secret.
- to (required)
- The number in international format (+14155550123). Several numbers, separated by commas or new lines, get one text each (at most 30).
- message (required)
- The text, up to 1600 characters.
- device-id
- Send through this phone (its ID is under Phones in the dashboard). Empty: SMSTunnel picks one of yours.
- default-country-code
- For example +40: completes numbers written without + or 00.
- code
- true sends a sign-in code to one number: the text must contain the code, 4 to 10 digits standing on their own (482913), up to 320 characters. Codes count against a separate quota. Default false.
- server-url
- Only for a self-hosted SMSTunnel or a test server. Default https://smstunnel.io.
Output: message-ids, the IDs of the texts, comma-separated, in the order of the numbers
(empty for a number that got no text). The step also writes a table to the job summary: the last digits of each
number, the message ID, and what happened.
Examples
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: ./deploy.sh
- name: Text me if it failed
if: failure()
uses: narcisbodea/smstunnel-action@v1
with:
api-key: ${{ secrets.SMSTUNNEL_API_KEY }}
to: ${{ secrets.ALERT_PHONE }}
message: "Deploy of ${{ github.repository }} failed at ${{ github.sha }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" name: Release texts
on:
release:
types: [published]
jobs:
text:
runs-on: ubuntu-latest
steps:
- uses: narcisbodea/smstunnel-action@v1
with:
api-key: ${{ secrets.SMSTUNNEL_API_KEY }}
to: ${{ secrets.TEAM_PHONES }} # "+14155550123, +14155550124": one text each
message: "${{ github.event.repository.name }} ${{ github.event.release.tag_name }} is out: ${{ github.event.release.html_url }}" - id: sms
uses: narcisbodea/smstunnel-action@v1
with:
api-key: ${{ secrets.SMSTUNNEL_API_KEY }}
to: ${{ secrets.ALERT_PHONE }}
message: The nightly backup finished.
- name: Show the IDs
env:
IDS: ${{ steps.sms.outputs.message-ids }}
run: |
echo "SMSTunnel message IDs: $IDS" Pull requests from forks, and Dependabot
GitHub gives no secrets to workflows that run for pull requests from forks, or for Dependabot. There
api-key is empty, and the step fails with a message that says so. Skip the step in those runs:
if: github.actor != 'dependabot[bot]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
Don't switch to pull_request_target to get the secret into a fork's workflow: that runs
with your secrets next to code from a stranger's pull request.
When a text does not go out
The step fails, with SMSTunnel's own explanation for each number:
- 401
- The API key is wrong, was deleted, or its account is suspended. The other numbers aren't tried.
- 403
- The key may not do this (PERMISSION_REQUIRED), or the plan does not include it. The other numbers aren't tried.
- 429
- Too many requests: the step waits as long as SMSTunnel asks (up to a minute) and tries once more. Without a wait from SMSTunnel, the plan's limit for the day or month is reached, and the step fails after one more try.
- 400
- SMSTunnel refused a field: a number it cannot read, a code text without a code. The other numbers still get their text.
A failed text should not fail your deployment? Add continue-on-error: true to the step. If
your phone is offline, the text waits on your account and goes out when the phone is back, within 3 days (the
summary says queued).
Limits worth knowing
- Rate limit: SMSTunnel allows 30 sends a minute; the action sends one text at a time.
- Length: up to 1600 characters (10 joined SMS); codes up to 320.
- Numbers: use the international format (
+14155550123). For local numbers, setdefault-country-code: it always takes precedence over the phone's. - Runtime: the action runs on Node.js 24 (
node24), on GitHub-hosted and self-hosted runners. - Pinning:
@v1follows the 1.x releases; for a fixed version, use a full commit SHA instead.
Build it with an AI assistant
Copy this prompt into Claude, ChatGPT, Cursor or Copilot and replace the part in [brackets]. Don't paste your API key into the chat: the prompt doesn't need it.
Help me write a GitHub Actions workflow step that sends a text with SMSTunnel.
About SMSTunnel (https://smstunnel.io): my own apps and tools hand text messages (SMS) to my SMSTunnel account, and my own Android phone sends them from my own number, on my own SIM and plan. The phone runs the SMSTunnel app from Google Play (https://play.google.com/store/apps/details?id=io.smstunnel.app), signed in to my account.
What I want: [describe what should send a text, to whom, and when]
How it works:
- Use the action narcisbodea/smstunnel-action@v1 with these inputs: api-key: ${{ secrets.SMSTUNNEL_API_KEY }} (required); to: the number in international format (+14155550123), several numbers separated by commas or new lines get one text each, at most 30; message: the text, up to 1600 characters; device-id (optional): the phone to send from; default-country-code (optional, e.g. "+40"): completes numbers written without + or 00; code: "true" only for sign-in codes, to one number: the text must contain a code of 4 to 10 digits standing on its own (e.g. 482913 or 123-456), at most 320 characters.
- Output: message-ids (comma-separated). Behind it is POST https://smstunnel.io/api/v1/sms/send with the header X-API-Key, one recipient per call, 30 send requests a minute. The step fails on 401 (bad key), 403 (missing permission or plan; code such as PERMISSION_REQUIRED), 400 (refused number or text) and 5xx; on 429 it waits (Retry-After, at most a minute, else 15 seconds) and retries once; a 429 without Retry-After means the plan's limit for the day or month is reached, so that retry fails too. If the phone is offline, the text waits on the account and goes out when the phone is back, within 3 days.
- The API key comes from my SMSTunnel dashboard (https://dashboard.smstunnel.io): Apps → Add an app → Create the key. It is shown only once and can send texts and check whether they went out.
Rules for your answer:
- Keep the API key in an environment variable or the platform's secret store (for example SMSTUNNEL_API_KEY). Never put it in source code, in a repository, or in code that runs in a browser or a mobile app: call SMSTunnel from a server. I will not paste my key in this chat; use a placeholder.
- Validate phone numbers before sending (international format, like +14155550123) and skip or report the invalid ones.
- Handle errors: log or show what went wrong, retry only after 429, 5xx or network errors and only a few times, and never send the same text twice.
- Only send texts people expect from me: alerts to myself, reminders, sign-in codes, updates they asked for. No unsolicited texts.
- Keep the API key in a repository secret, never in the workflow file. In a public repository, keep the phone numbers in secrets too.
- Skip the step for pull requests from forks and for Dependabot (they get no secrets); never use pull_request_target for this.
- Decide with me whether a failed text should fail the job (continue-on-error). Responsible use
Built for alerts to yourself and your team, and texts people expect from you. One text goes to one number. Sending unsolicited messages from a consumer SIM generally breaches mobile carrier terms of service.
GitHub and GitHub Actions are trademarks of GitHub, Inc. SMSTunnel is a third-party integration and is not affiliated with, endorsed by, or sponsored by GitHub.