| version | 1.3.20260916 |
|---|---|
| last-updated | 2026-09-16 |
| title | Ly's Telegram Notify |
| description | Go library and CLI for sending Telegram messages and files via the Bot API |
English | Español | Italiano | Português
Main repository: https://gitlab.com/lyoneel/tg-notify. If you are reading this on any other host, it is a mirror. Please open issues and merge requests on GitLab.
Send Telegram notifications with a single, simple command. Designed to stay out of your way: no config files, just environment variables or flags. One command delivers everything:
- text messages
- photos, videos, documents, and files
- files sent by URL
- resending an existing file already on Telegram
- photo/video albums (up to 10 items)
- GIF animations and stickers
- replying to a specific message
Simple to install, simple to use, simple to configure. The binary is self-contained with built-in retry handling for rate limits and transient failures.
This project has two agent skills in the public ly-agent-skills repository for driving Telegram notifications from an agent:
- tg-notify wraps this CLI: messages, files, albums, replies, chat-ID discovery, and identity checks.
- tg-notipy is the Python sibling. It talks to the Bot API directly from a self-contained stdlib-only script and does not use this CLI. Where features overlap the two are interchangeable; only the CLI adds shell completion and socks5/socks5h proxy support.
Send a message:
tg-notify "Deploy finished"Send a file (photo, PDF, or video; type is auto-detected):
tg-notify -f screenshot.png
tg-notify "Monthly report" -f report.pdf
tg-notify -f clip.mp4Send a GIF animation or sticker:
tg-notify -f logo.gif -t animation
tg-notify -f sticker.webp -t stickerSend a photo/video album:
tg-notify --album a.jpg b.jpg c.jpgSend a message from stdin (pipelined):
echo "Build failed" | tg-notifyReply to a specific message:
tg-notify --reply-to 42 "Acknowledged"Print your bot's identity (validates the token):
tg-notify --whoamiWhen a file flag (-f, --url, or --file-id) is present, a leading
text argument becomes the file's caption instead of a message.
Grab the binary for your platform from the
GitLab releases page
or the
GitHub releases page.
Asset names follow the tg-notify-<os>-<arch> pattern, e.g.
tg-notify-windows-amd64.exe, tg-notify-linux-amd64, or
tg-notify-darwin-arm64.
Windows
Download the .exe for your architecture, rename it to
tg-notify.exe, and move it into a directory already on PATH; or
keep it in its own folder and add that folder to PATH. In PowerShell:
$bin = "$env:USERPROFILE\bin"
New-Item -ItemType Directory -Force $bin | Out-Null
Move-Item .\tg-notify-windows-amd64.exe "$bin\tg-notify.exe"
[Environment]::SetEnvironmentVariable("Path",
[Environment]::GetEnvironmentVariable("Path", "User") + ";$bin",
"User")Alternatively, add the folder through Settings > System > About > Advanced system settings > Environment Variables.
Linux/macOS
Make the binary executable and either add its directory to PATH or
move it into a directory already on PATH:
chmod +x tg-notify-linux-amd64
mkdir -p ~/bin && mv tg-notify-linux-amd64 ~/bin/tg-notifyMake sure ~/bin (or whichever directory you choose) is on PATH.
go install gitlab.com/lyoneel/tg-notify/cmd/tg-notify@latestThe binary lands in $(go env GOPATH)/bin (make sure that directory
is on PATH).
git clone https://gitlab.com/lyoneel/tg-notify.git
cd tg-notify
make build # or: go build -o ./build/tg-notify ./cmd/tg-notifyThe Makefile provides build, run, test, and quality targets; make help
lists them. The version stamps itself: make build injects
git describe --tags --always --dirty, and plain go build or
go install binaries report their module version or VCS revision via
-v. Override with:
make build VERSION=v1.3.20260916Set these environment variables before running:
| Variable | Required | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN |
yes | Telegram bot token |
TELEGRAM_CHAT_ID |
yes | Chat ID to send messages to |
Both can be overridden per invocation with the --token and
--chat-id flags. Chat IDs are passed as strings, so large or negative
group IDs (e.g. -1001234567890) work. No config file: set these once
in your shell profile (~/.bashrc, ~/.zshrc) or pass them as flags
per invocation.
The same client that powers the CLI is importable. The package is
tgnotify at the repository root; note the package name keeps the
camel-case tgnotify while the import path ends in tg-notify, so a
plain import resolves to the package name:
go get gitlab.com/lyoneel/tg-notifyMinimal send (three lines of setup):
import "gitlab.com/lyoneel/tg-notify"
bot := tgnotify.New("123456:ABC-TOKEN")
id, err := bot.SendMessageOpts(context.Background(), "123456789", "deploy finished", nil)Every send method has three equivalent forms: the positional classic
(SendMessage(ctx, chatID, text, parseMode, replyTo, silent)), the
options struct (SendMessageOpts(ctx, chatID, text, *SendOptions)),
and functional options through a constructor
(SendMessageOpts(ctx, chatID, text, tgnotify.NewSendOptions( tgnotify.WithParseMode("HTML"), tgnotify.WithSilent()))). All three
share one send core, so they cannot drift. nil or a zero
SendOptions means the Telegram defaults; SendOptions also carries
Caption for the file and album methods
(SendFileOpts, SendFileByURLOpts, SendFileByIDOpts,
SendMediaGroupOpts).
Retries are on by default with the CLI values (60 transient retries,
2s base wait, 429 retry_after honored). Tune them per bot:
bot.SetRetryPolicy(tgnotify.RetryPolicy{
MaxRetries: 3,
BaseWait: 2 * time.Second,
Logger: log.Printf, // nil keeps retries silent
})The library enforces the Telegram payload limits client-side
(MaxMessageRunes 4096, MaxCaptionRunes 1024), never prints a bot
token in an error, and never writes to stderr unless you attach a
Logger. To configure from the environment instead of literals, use
tgnotify.FromEnv(); it reads TELEGRAM_BOT_TOKEN,
TELEGRAM_CHAT_ID, TELEGRAM_BASE_URL, and TELEGRAM_PROXY and
returns a ready bot plus the chat ID.
See the Example functions in the package documentation
(pkg.go.dev/gitlab.com/lyoneel/tg-notify) for runnable shapes.
tg-notify "message text" # positional text sends a message
tg-notify -m "message text" -p HTML # message with a flag and formatting
echo "text from stdin" | tg-notify # message read from stdin when no text is given
tg-notify -f path/to/file.png # send a local file (auto-detected type)
tg-notify "caption" -f path/to/file.png # local file with positional caption
tg-notify -u https://example.com/a.pdf -t document
tg-notify -F AgAC... -t photo # resend an existing file
tg-notify --album a.jpg b.jpg c.jpg # send a photo/video album (2-10 items)
tg-notify --reply-to 42 "done" # reply to message 42
tg-notify --json "hello" # machine-readable JSON output
tg-notify -S -f report.pdf # send without a phone notification
tg-notify -D "hello" # preview the request without sending
tg-notify -P socks5://127.0.0.1:1080 "x" # route through a proxy
tg-notify -U http://localhost:8081 -f big.mp4 # self-hosted Bot API
tg-notify -A bash # print a bash completion script
tg-notify --whoami # print the bot's identity
tg-notify -d # print the latest chat ID
tg-notify -d -o 123 # print chat ID from updates after ID 123
tg-notify -v # print the versionVia make in this repo: make run ARGS="\"caption\" -f path/to/file.png".
| Flag | Short | Description | Required |
|---|---|---|---|
--message |
-m |
Message text (1-4096 chars) | No; read from stdin if absent |
--file |
-f |
Local file path to upload | One of -f, --url, --file-id |
--url |
-u |
Remote file URL to send | One of -f, --url, --file-id |
--file-id |
-F |
Telegram file_id to resend |
One of -f, --url, --file-id |
--album |
-a |
Photo/video files or URLs to send as one album (2-10) | No |
--type |
-t |
Override auto-detection: photo, document, audio, video, voice, animation, sticker |
Required with --file-id |
--caption |
-c |
Caption text for files (0-1024 chars) | No |
--parse-mode |
-p |
MarkdownV2, HTML, or empty (default: plain text) |
No |
--reply-to |
-r |
Message ID to reply to | No |
--json |
-j |
Print machine-readable JSON instead of text | No |
--no-trim |
Preserve whitespace in a piped message | No | |
--whoami |
-w |
Print the bot's identity and exit | Mode switch |
--token |
-T |
Bot token (overrides TELEGRAM_BOT_TOKEN) |
No |
--chat-id |
-C |
Chat ID (overrides TELEGRAM_CHAT_ID) |
No |
--no-retry |
-n |
Disable auto-retry on 429 rate limits and transient errors | No |
--retries |
-R |
Max retries on transient errors (default 60, 0 disables) | No |
--base-wait |
-B |
First backoff wait on transient errors (default 2s) | No |
--discover-chat-id |
-d |
Print the chat ID from the latest bot update | Mode switch |
--offset |
-o |
Past update ID to skip in --discover-chat-id |
No |
--silent |
-S |
Deliver without a phone notification | No |
--proxy |
-P |
Proxy URL (http, https, socks5, socks5h) overriding env proxies |
No |
--base-url |
-U |
Bot API base URL for a self-hosted server | No |
--dry-run |
-D |
Print the resolved request without sending | No |
--completion |
-A |
Print a completion script (bash, zsh, fish) and exit |
Mode switch |
--version |
-v |
Print the version and exit | Mode switch |
--help |
-h |
Print usage and exit | Mode switch |
Mode selection: -f, --url, or --file-id present sends a file,
and a positional text argument becomes the file caption; --album
sends a photo/video album; without a file or album flag,
--discover-chat-id prints the chat ID, --whoami prints the bot
identity, and positional text or --message is sent as a message. Flags
parse anywhere on the command line, including after the positional text.
File type is auto-detected from the extension and MIME type; unknown
types are sent as document. URL sends default to document when
--type is absent. Local uploads are checked client-side against the
Telegram limits: 10 MB for photos, 50 MB for everything else. When
--base-url targets a self-hosted Bot API server, the limit rises to
2000 MB for every type.
Point --base-url (or TELEGRAM_BASE_URL) at a self-hosted Bot API
server to route requests through your own gateway instead of
api.telegram.org. This bypasses the official HTTP gateway's upload
cap, raising the client-side limit to 2000 MB per file. The server is
the telegram-bot-api binary from tdlib/telegram-bot-api; it still
requires a bot token and an internet route to Telegram.
--proxy (or TELEGRAM_PROXY) routes every request through an HTTP,
HTTPS, SOCKS5, or SOCKS5h proxy, overriding any HTTP_PROXY/
HTTPS_PROXY/NO_PROXY settings. On startup, an optional ./.env
file is loaded before the environment is read, so file-supplied values
fill in any unset variable; real environment variables and flags always
win over the file.
Note: this CLI treats socks5:// and socks5h:// identically (the
Go SOCKS5 dialer always sends hostnames to the proxy), so the
curl-style local-vs-remote DNS distinction does not apply.
Pass --json to print a single machine-readable line instead of the
human summary, for scripting:
tg-notify --json "hello"
# {"ok":true,"message_id":123}The same flag works with file and album sends (albums emit a
message_ids array) and with --discover-chat-id (which emits
{"chat_id": …}). Errors still print to stderr and exit non-zero.
- On success the CLI prints
Sent (message_id: <id>)and exits 0. - On failure it prints
Failed: <reason>to stderr and exits 1; bot tokens are masked out of any Bot API URL in error output. - On a 429 rate limit it waits
retry_afterseconds (default 5) and retries exactly once, unless--no-retryis set. - On a transient failure (network timeout, connection error, or HTTP
5xx) it retries up to
--retriestimes (default 60) with exponential backoff starting at--base-wait(default 2s, doubling to a 60s per-wait cap, jittered), unless--no-retryis set. Missing local files are not transient and always fail immediately. - With no message, no positional text, and stdin attached to a terminal,
the CLI prints usage and exits 1 instead of waiting for piped input;
pipe input to send it (
echo "text" | tg-notify).
The CLI handles two kinds of failure automatically, each with its own schedule.
Rate limits (429). When Telegram replies with HTTP 429, the CLI
sleeps for the retry_after interval Telegram specifies (default 5
seconds) and retries exactly once. There is no retry cap to tune here:
the single retry is all it gets.
Transient errors. Network timeouts, connection errors, and HTTP
5xx responses are retried up to --retries times (default 60) with
exponential backoff. The first retry waits --base-wait (default 2s);
each subsequent attempt doubles that wait up to a 60s per-wait cap,
with ±25% jitter so many parallel clients do not retry in lockstep.
For example, with the defaults the waits run roughly 2s, 4s, 8s,
16s, 32s, then 60s from then on.
Both mechanisms are disabled by --no-retry, which makes every
failure return immediately. Setting --retries 0 disables only the
transient-error retries; 429 rate-limit retries still happen once
unless --no-retry is also set. --retries must be zero or more and
--base-wait greater than zero; invalid values are rejected with an
error.
Retrying a timed-out request can rarely duplicate a message, because a request that already reached Telegram but timed out on the response may be sent twice. Connection failures that never reached Telegram cannot cause duplicates.