tgpost
Send messages and files to Telegram channels, now or on a schedule.
Disclaimer
Provided AS IS, without warranty of any kind, express or implied. Use entirely at your own risk. The author accepts no liability for data loss, content published to the wrong channel or audience, business interruption or consequential damages.
You are responsible for verifying the target channel, for what you publish, for being authorised to post there, and for complying with the Telegram Terms of Service.
tgpost scheduleandtgpost daemonpublish unattended: a mistaken cron expression can post to a live channel repeatedly until someone stops it. Test against a private channel first.Not certified for regulated, forensic, safety-critical or high-assurance use. There is no delivery guarantee: the Bot API offers no idempotency key, so a send that times out may or may not have arrived.
The MIT LICENSE is the governing text and prevails wherever this summary differs from it. See DISCLAIMER.md for the full text.
Why this exists
Sending a file to a Telegram channel from the command line is well covered already. What is missing is doing it later, reliably.
The Telegram HTTP Bot API has no server-side scheduling: there is no
schedule_date parameter on any send method, so "post this at 9am on weekdays"
has to be solved on your side, and survive a reboot. tgpost is a sender with a
persistent job store attached, so a scheduled post still goes out after the
machine restarts.
If you only need to send something right now, telegram-send and apprise both
do that well.
Install
pip install tgpost # sending only
pip install tgpost[schedule] # sending and scheduling
Requires Python 3.11 or newer.
Getting started
Create a bot with @BotFather, then add it to your channel as an administrator with permission to post messages. A bot cannot post to a channel as an ordinary member.
export TGPOST_BOT_TOKEN="123456789:your-token-here" # Windows: set TGPOST_BOT_TOKEN=...
tgpost targets add release-notes -1001234567890 --description "Build announcements"
tgpost check
tgpost check verifies the token and reports, per target, whether the bot can
actually post. Those two permission failures are the most common cause of a
send failing later, so it is worth running first.
Finding a channel id
A public channel can be addressed as @channelusername. For a private channel,
post any message in it, then read the numeric id (it starts with -100):
tgpost targets add private-chan @temporary_username # if public, then:
tgpost check # prints the numeric id
Sending
tgpost send --to release-notes --text "Build 1.2.3 shipped"
tgpost send --to release-notes --file build.zip --caption "Nightly build"
tgpost send --to release-notes --file a.png --file b.png --album --kind photo
git log --oneline -10 | tgpost send --to release-notes --stdin
tgpost send --to release-notes --text "check this first" --dry-run
Text longer than Telegram's 4096-character limit is split across several messages at paragraph, line or word boundaries.
Scheduling
tgpost schedule --to release-notes --text "Standup" --cron "0 9 * * 1-5" --tz Europe/London
tgpost schedule --to alerts --text "Still alive" --every 30m
tgpost schedule --to release-notes --file report.pdf --at "2026-09-10T14:00"
tgpost schedule --to alerts --text "in two hours" --at 2h
tgpost jobs # what is scheduled, and when each next runs
tgpost history # what actually happened
tgpost cancel a41a4c18
Jobs are stored in SQLite and survive a restart. Something has to run them:
tgpost daemon # long-running: keeps firing jobs until stopped
tgpost run-due # fires everything due, then exits
Use daemon on a machine that stays up. Use run-due from Windows Task
Scheduler or cron if you would rather not keep a process alive:
schtasks /create /tn "tgpost" /tr "tgpost run-due" /sc minute /mo 5
A job whose time passed while nothing was running still fires when the scheduler comes back, as long as it is within the one-hour grace window. Missed repeats are coalesced, so a weekend of downtime sends once rather than fifty times.
Formatting
The default parse mode is HTML, because it only requires &, < and > to
be escaped. MarkdownV2 requires eighteen characters to be escaped, including
., - and !, which appear in ordinary prose, and a single missed escape is
a hard error rather than a cosmetic one.
tgpost send --to alerts --text "<b>Deploy finished</b>" # markup as written
tgpost send --to alerts --text "$RAW" --escape # escape untrusted text
tgpost send --to alerts --text "plain" --parse-mode none
Pass --escape for text you did not write yourself. Without it, the text is
sent as markup and a stray < will be rejected by Telegram.
Configuration
Config lives at %APPDATA%\tgpost\config.toml on Windows, or
~/.config/tgpost/config.toml elsewhere.
[defaults]
parse_mode = "html"
base_url = "https://api.telegram.org"
[targets.release-notes]
chat_id = "-1001234567890"
description = "Build announcements"
[targets.alerts]
chat_id = "@my_public_channel"
parse_mode = "none"
| Variable | Purpose |
|---|---|
TGPOST_BOT_TOKEN |
Bot token. Preferred over storing it in the config file. |
TGPOST_CONFIG |
Path to the config file. |
TGPOST_DB |
Path to the job database. |
The token is read from --token, then TGPOST_BOT_TOKEN, then the config file.
tgpost targets add never writes the token to disk, so a token supplied on the
command line or in the environment is not persisted by accident.
Limits
Set by the Bot API, not by this package:
| Limit | Value |
|---|---|
| Message text | 4096 characters (tgpost splits automatically) |
| Caption | 1024 characters |
| Photo upload | 10 MB |
| Any other file | 50 MB |
| Album | 2 to 10 items |
| Rate | About 1 message per second per chat, 30 per second overall |
tgpost paces sends to stay under the rate limits, and on a 429 waits exactly
the retry_after Telegram asks for rather than guessing.
To send files larger than 50 MB, run a local Bot API server, which raises the limit to 2000 MB, and point tgpost at it:
tgpost --base-url http://127.0.0.1:8081 send --to release-notes --file big.iso
Python API
from tgpost import TelegramClient
with TelegramClient("123456789:your-token") as client:
client.send_message("-1001234567890", "Build finished")
client.send_file("-1001234567890", "report.pdf", caption="Nightly report")
Errors derive from TgPostError: AuthError, ForbiddenError,
BadRequestError, RateLimitError, ServerError, NetworkError,
FileTooLargeError, ConfigError and SchedulerError. Retryable failures are
retried internally; what reaches you is a failure worth acting on.
License
MIT. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tgpost-0.1.0.tar.gz.
File metadata
- Download URL: tgpost-0.1.0.tar.gz
- Upload date:
- Size: 32.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
340e93ccab57b32d95882ac621773e4c17a5e1a27054123a90d8ecd57b880fa6
|
|
| MD5 |
d99d4252a484068b5fdeb78199767088
|
|
| BLAKE2b-256 |
193704cb762062c9ecea3fbc7bca76d44cc4e8d192223dcba2904de1219773be
|
File details
Details for the file tgpost-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tgpost-0.1.0-py3-none-any.whl
- Upload date:
- Size: 35.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00aa7b00464de5495c1abb29ea923c29c2307ff5032484669d4e28f12db82744
|
|
| MD5 |
c14d89d18e386de15ab6666a0ed1f03b
|
|
| BLAKE2b-256 |
82a253e6812a577e4e2dd0a802128f273486b03720c08979b2b9cd3a0cf86121
|