SMSTunnel command-line tool
Send texts from your terminal and scripts, from your own number
One command, one text, from your own number
You write a text in your terminal or a script, SMSTunnel hands it to your linked Android phone, and your phone sends it from your own number, on your own SIM and plan. One small program for Windows, macOS and Linux, with nothing else to install. Your API key stays in your system's keychain.
What it does
- send
- Hands a text to your phone, which sends it from your number. One number per text, up to 1600 characters.
- code
- Sends a sign-in code (the text must contain the code), on its own daily allowance.
- status
- Whether a text went out, and when.
- received
- The texts your phone received, or new ones as they arrive, with a key allowed to read them.
- phones, usage
- Your phones and which are online; your plan and what is left today.
- login, logout
- Saves your key in your system's keychain after checking it, or removes it.
- --json
- JSON on every command, and exit codes your scripts can check.
1. Install
Windows: Scoop
scoop bucket add smstunnel https://github.com/narcisbodea/smstunnel-cli
scoop install smstunnel/smstunnel macOS and Linux: Homebrew
brew tap narcisbodea/smstunnel-cli https://github.com/narcisbodea/smstunnel-cli
brew install --cask smstunnel Ubuntu and other Linux (x64): snap
sudo snap install smstunnel Go 1.24 or newer
go install github.com/narcisbodea/smstunnel-cli/cmd/smstunnel@latest Coming soon: winget, Chocolatey, the AUR and npm (the Node.js version, with the same commands).
Or download it
Version 1.0.0, from the GitHub release:
- macOS .tar.gz
- Apple silicon Intel
Check a download against checksums.txt. From an archive, put
smstunnel (smstunnel.exe on Windows) somewhere on your
PATH. On a Mac, Homebrew is the easy way: the program isn't notarized by Apple yet, so a copy
you download needs Open Anyway in System Settings → Privacy & Security the first time.
sudo apt install ./smstunnel-cli_1.0.0_amd64.deb sudo dnf install ./smstunnel-cli-1.0.0-1.x86_64.rpm
Check it with smstunnel --version. The packages install the program as
smstunnel (/usr/bin/smstunnel on Linux). The
desktop app is a separate program and installs beside it.
2. 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 Command line, name it (for example "My laptop") 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 My laptop can't send texts anymore.
Get the steps with your key in them
It opens Add an app with Command line picked. Once the key is made, the dashboard shows every step and command with the key already filled in, ready to copy.
To read the texts your phone receives, also tick Can also read received texts, and turn on syncing of received texts in the SMSTunnel app. It's off by default.
3. Log in on your computer
smstunnel login Paste the key when asked (it stays hidden) and press Enter. The CLI checks the key with SMSTunnel first and saves it only if it works:
- macOS
- Your Keychain, item "smstunnel".
- Windows
- Windows Credential Manager, item "smstunnel" under Web Credentials.
- Linux
- Your keyring through the Secret Service (secret-tool, package libsecret-tools or libsecret), when it is installed and a desktop session is running.
- Anything else
- ~/.config/smstunnel/config.json (%APPDATA%\smstunnel on Windows), readable only by you. The CLI says so with a warning.
The key never goes on the command line. To pipe it in from a password manager:
pass show smstunnel | smstunnel login SMSTUNNEL_API_KEY, when set, is used instead of the saved key (handy in CI).
smstunnel logout removes the saved key from this computer; the key itself keeps working until
you stop or delete the app in your dashboard.
4. Send a text
smstunnel send +14155550123 "Your order is ready for pickup" Handed to your phone, which sends it from your number.
Message id: 6ae9952f7f9ac207c547af90. Check it with: smstunnel status 6ae9952f7f9ac207c547af90 - --wait
- Waits until your phone sent it (or it failed), up to --timeout seconds (120 by default).
- --phone <id or name>
- Which of your phones sends it (smstunnel phones lists them). Without it, your default phone.
- --country <code>
- The country code for a number written without + or 00: 07700 900123 with --country +44 becomes +447700900123. Without it, SMSTunnel uses the country saved for your phone.
- --idempotency-key <key>
- Makes the command safe to run again: within 24 hours, the same command with the same key gets the first answer back (the same message id) instead of sending a second text. One key per text, such as order-1042-ready.
- No message, or -
- The text is read from standard input.
smstunnel send 07700900123 --country +44 --phone "Store phone" --wait "See you at 10" Handed to your phone (message id 6ae9952f7f9ac207c547af90). Waiting up to 120 s for it to be sent… (Ctrl+C stops waiting)
Sent by your phone at 2026-11-03 10:02. Message id: 6ae9952f7f9ac207c547af90 echo "Backup finished" | smstunnel send +14155550123 If none of your phones is online, the text is queued and goes out when your phone is back online (within 3 days). A text your phone handed to the SIM without getting the confirmation is reported as "not confirmed": it was most likely sent, so don't send it again. How country codes are chosen
One number per text: smstunnel send "+14155550123,+14155550124" … is refused with "One number
per text." Texts can have up to 1600 characters; longer than one SMS part (160 plain Latin characters, or 70 with
characters such as ș or emoji), your phone splits them into parts.
Send a sign-in code
smstunnel code +14155550123 "Your sign-in code is 482913. It expires in 5 minutes."
You generate and check the code yourself; the text must contain it, as 4 to 10 digits on their own
(482913) or two groups of 3 joined by a dash (482-913), in up to 320
characters. Codes have their own daily allowance. code takes the same options as
send.
Check a text
smstunnel status 6ae9952f7f9ac207c547af90 Sent by your phone at 2026-11-03 10:02.
To: +14155550123
Message id: 6ae9952f7f9ac207c547af90 Received texts
smstunnel received --since 2h
smstunnel received --watch 2026-11-03 10:02 +14155550199 (Store phone) No problem, see you!
This lists the texts your phone received after you turned on Sync received texts in the SMSTunnel
app (it is off by default; the phone's existing inbox is never read). The key must be allowed to read them: in your
dashboard, Apps → Allow reading received texts, or tick Can also read received texts
when you add the app. Options: --since (a date such as 2026-11-03, or
30m, 2h, 1d), --from <number>,
--phone <id or name>, --limit (up to 100), and
--watch to keep printing new texts as they arrive (every --interval
seconds, 10 by default). To have them sent to your own server instead, use Apps → Forward received
texts (how it works).
Phones and usage
smstunnel phones
smstunnel usage ID NAME STATUS LAST SEEN
6ae9940bf30ce9d1447cc179 Store phone online now
6ae9940bf30ce9d1447cc17a Spare phone offline 4 days ago
Plan: pro
Texts today: 2 used, unlimited left
Sign-in codes today: 1 used, unlimited left In scripts
Every command takes --json and prints JSON on stdout; errors then go to stderr as JSON too
({"error":{"message":…,"exitCode":…,"status":…}}).
if smstunnel send "$MY_NUMBER" "Backup of $(hostname) finished" --wait --timeout 60; then
echo "texted"
fi
A script that may run twice (a cron job, a CI retry) should give each text its own
--idempotency-key: running it again then can't send the text twice.
smstunnel send "$CUSTOMER" "Order $ORDER is ready for pickup" --idempotency-key "order-$ORDER-ready" Exit codes
- 0
- Done. A text "not confirmed by your phone" also ends with 0: it was most likely sent, never send it again.
- 1
- Something unexpected.
- 2
- Invalid arguments, or SMSTunnel refused the input (HTTP 400), or the --idempotency-key was already used for a different text (HTTP 422).
- 3
- No API key, or SMSTunnel refused it (HTTP 401).
- 4
- The key or your plan doesn't allow it (HTTP 403), for example a key that may not read received texts.
- 5
- Not found (HTTP 404): an unknown message id or phone.
- 6
- Too many requests (HTTP 429 with Retry-After: the CLI says how many seconds to wait), the plan's limit for the day or month is reached (429 without it), or a request with the same --idempotency-key is still being handled (HTTP 409).
- 7
- SMSTunnel could not be reached or had a problem (5xx). If a send broke after it left, the CLI says the text may have gone out: check before sending it again, or run the same command with the same --idempotency-key.
- 8
- The text failed (send --wait, code --wait).
- 9
- Still waiting for your phone when --wait gave up. The text goes out when your phone is back online: don't send it again.
- 130
- You stopped --wait with Ctrl+C (the text is still on its way). Ctrl+C ends received --watch with 0.
The CLI never resends a text by itself.
Environment
- SMSTUNNEL_API_KEY
- A key to use instead of the saved one.
- SMSTUNNEL_API_BASE
- The API address, like --api-base (default https://smstunnel.io/api/v1).
- SMSTUNNEL_KEY_STORE=file
- Save the key in the config file instead of the system keychain.
- SMSTUNNEL_CONFIG_DIR
- Where config.json lives.
The CLI talks only to https://smstunnel.io (or the --api-base you give
it), over HTTPS, with your key in the X-API-Key header, and checks SMSTunnel's certificate
against your system's certificate store. It doesn't use HTTPS_PROXY and sends no analytics.
Limits worth knowing
- 30 send requests a minute per key (the keys of one account together: 90), and 100 other requests a minute.
- Plans: how many texts and sign-in codes you can send a day depends on your plan; see Pricing and
smstunnel usage. - Length: up to 1600 characters (10 joined SMS); sign-in codes up to 320.
- Offline phone: the text waits and goes out when the phone is back online, within 3 days.
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 script that sends text messages with the SMSTunnel command-line tool.
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:
- Install the smstunnel command: Windows: Scoop (scoop bucket add smstunnel https://github.com/narcisbodea/smstunnel-cli, then scoop install smstunnel/smstunnel); macOS and Linux: Homebrew (brew tap narcisbodea/smstunnel-cli https://github.com/narcisbodea/smstunnel-cli, then brew install --cask smstunnel); Ubuntu and other Linux (x64): snap (sudo snap install smstunnel); Go 1.24 or newer (go install github.com/narcisbodea/smstunnel-cli/cmd/smstunnel@latest); or the .deb, .rpm or archive for my system from https://github.com/narcisbodea/smstunnel-cli/releases.
- The key comes from the SMSTUNNEL_API_KEY environment variable (in CI, a secret), or from smstunnel login, which reads it from standard input, checks it and saves it in the system keychain. It never goes on the command line.
- smstunnel send <number> <text> [--phone <id or name>] [--country +44] [--wait [--timeout 120]] [--idempotency-key <key>] [--json]. With no text, or -, it reads the text from standard input.
- smstunnel code <number> <text> is for sign-in codes only: the text must contain the code, 4 to 10 digits standing on their own (482913) or 123-456, at most 320 characters. Codes have their own daily allowance.
- smstunnel status <message id>, smstunnel phones, smstunnel usage, smstunnel received [--since 2h] [--from <number>] [--watch]. Reading received texts needs a key with "Can also read received texts" and "Sync received texts" turned on in the app.
- One number per command: to text several people, run it once per number. Numbers in international format (+14155550123); --country completes a number written without + or 00. Texts up to 1600 characters; 30 send requests a minute per key.
- Exit codes: 0 done (also "not confirmed by your phone": most likely sent, never send it again); 1 unexpected; 2 bad input (400) or an idempotency key reused for another text (422); 3 key missing or refused (401); 4 not allowed for this key or plan (403); 5 not found (404); 6 too many requests (429: wait the seconds it says), the plan's limit for the day or month reached (429 without Retry-After), or the same --idempotency-key still in progress (409); 7 SMSTunnel unreachable or a 5xx (the text may have gone out: rerun only with the same --idempotency-key, which never sends a second text; after a 5xx the same key gives the same answer for 24 hours, so check the dashboard's Messages first); 8 the text failed (--wait); 9 still waiting for the phone when --wait gave up (it goes out when the phone is back: don't resend). With --json, the result is JSON on stdout and errors are JSON on stderr: {"error":{"message","exitCode","status"}}.
- Give each text its own --idempotency-key (such as order-1042-ready): running the same command again within 24 hours gets the first answer back instead of a second text. The CLI never resends a text by itself.
- If my phone is offline, the text waits and goes out when the phone is back online, 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.
- Check the exit code of every command, and never print the key or pass it as an argument. 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.
Windows, macOS, Linux, Homebrew and Scoop are trademarks of their owners. SMSTunnel is a third-party integration and is not affiliated with, endorsed by, or sponsored by Microsoft, Apple, Homebrew, Scoop or any Linux distribution.