A privacy-first Telegram bot that tracks parcels from 3,000+ carriers and tells you the moment they move.
Features · How it works · Privacy · Self-hosting · Configuration · Architecture · Roadmap
Send Tracking a tracking number or just a photo of the label. The bot figures out the carrier, watches the parcel around the clock and messages you when something changes. Your data is encrypted at rest, nothing is logged, and delivered parcels delete themselves.
The code is fully open: read it, check what it stores, or run your own instance in a few minutes.
- 3,000+ carriers worldwide through the 17TRACK API, with the carrier detected automatically
- Manual carrier override for the rare cases where auto-detection picks the wrong one
- Status checks every 15 minutes (configurable), plus on-demand refresh
- Delivered and expired parcels are removed automatically after one final notification. So are parcels that get no tracking data for 30 days, and parcels whose tracking hasn't changed for 90 days (stuck in transit, for example).
- Type the tracking number. Spaces as printed on labels (
JJD 0003 9000) are handled. - Or send a photo of the QR code or barcode. It's decoded on the server, including tracking links inside QR codes. If a photo has several codes, you pick the right one.
- Give the parcel a name, or skip
- Choose how you want updates, per parcel: on every status change, or a status summary every N hours (1 to 168)
- Clean chat: all the questions and answers from the add flow are deleted when you're done. Only the confirmation stays.
- Persistent keyboard menu: Add package, My packages, Settings, Help
- Per-parcel screen with scan history, refresh, rename, notification settings, share, carrier and delete
- Share a parcel with a link. Whoever opens it gets the parcel on their own list, with their own notifications. The link works for anyone who has it until it expires, so a forwarded link works too.
- Silent at night: notifications between 22:00 and 07:00 arrive without sound, in each user's own time zone. Users pick a city in Settings, so daylight saving time is handled automatically.
- 11 languages: English (default), Deutsch, Español, Français, Polski, Português, Türkçe, Русский, Українська, 中文, 日本語
- Every message carries the bot's banner
sequenceDiagram
autonumber
actor U as User
participant B as Tracking bot
participant D as Encrypted DB
participant T as 17TRACK API
U->>B: Tracking number or label photo
B->>B: Decode QR / barcode (in memory)
B-->>U: Name? Notify how? Every how many hours?
B->>T: register(number)
B->>D: store (AES-256-GCM)
B-->>U: Package added (flow messages deleted)
loop every 15 minutes
B->>D: load parcels
B->>T: gettrackinfo(numbers, batches of 40)
T-->>B: latest status and events
alt new event, or N hours passed
B-->>U: Status update
end
alt delivered or expired
B-->>U: Final update
B->>D: delete parcel
B->>T: deletetrack (if nobody else tracks it)
end
end
Privacy isn't a feature added on top. The storage layer is built around it.
| Data | Stored as |
|---|---|
| Telegram chat ID | HMAC-SHA256 tag (lookup only). The real ID lives only inside the encrypted blob. |
| Tracking number | HMAC-SHA256 tag (lookup and de-duplication), plus inside the encrypted blob |
| Parcel name, carrier, status, scan history, timestamps, notification settings | AES-256-GCM encrypted blob, authenticated and bound to its owner |
| Settings: language, silent at night, time zone, account start date (for the new-account limit) | AES-256-GCM encrypted blob |
| Share links | Only an HMAC of the token is stored. The payload is encrypted, expires after 7 days and is deleted together with its package. The link to the package is an HMAC too, so the database doesn't show who shared what. |
| Label photos | Never stored. Decoded in memory and discarded. |
| Logs | None. Logging and warnings are disabled in code. The only possible output is a configuration error at startup or, if the bot crashes, a single line with the error type (for example fatal: ValueError). Neither contains user data or tracebacks. |
The encryption key (DATA_KEY) is kept outside the database. Deleted rows are overwritten inside the database file (PRAGMA secure_delete). That doesn't cover copies the operating system or an SSD may keep on its own, so run the bot on an encrypted disk if that matters to you. Database files are created readable only by the bot's user.
| Scenario | Protected? |
|---|---|
| Someone gets a copy of the database file or a backup | Yes. They see opaque hashes and ciphertext. What still shows: how many users and parcels there are, how many parcels each anonymous user has, whether two users track the same number, how many share links exist per parcel and when they expire, and how many parcels each anonymous user added today (the daily counters, which can't be linked to parcels and are deleted the next day). Parcel IDs are random, so the order in which parcels were added isn't stored. |
| A curious operator browses the database | Yes. Same as above. There's no admin panel and no "view users" feature. |
Full compromise of the running server (database and .env) |
No. The bot needs the key to poll parcels unattended, so an attacker with the key can decrypt. |
| 17TRACK | Partly. Receives tracking numbers, and nothing else, in order to track them. |
| Telegram | Partly. Chats with bots are not end-to-end encrypted, so Telegram's servers carry the messages. |
That last part is what any bot that sends notifications in the background has to deal with. If you don't want to trust an operator, host your own instance. That's what this repository is for.
- Label photos are limited to 10 MB and 24 megapixels, downscaled before decoding and processed off the main loop, two at a time, with a short cooldown per user.
- New packages are limited per user and per day for the whole bot (
DAILY_ADDS_PER_USER,DAILY_ADDS_TOTAL), so the shared 17TRACK quota can't be burned in minutes. - Accounts younger than 3 days can add only 3 packages a day, and all new accounts together can use at most half of the daily total. A handful of fresh accounts can't lock everyone else out.
- Each chat can send at most 8 messages per 10 seconds. Anything above that is ignored, so one spammer can't slow the bot down for everyone.
- Manual refreshes and carrier changes share a per-user cooldown. A carrier can't be changed for a number that someone else tracks too, because the change applies to everyone.
- Up to 3 active share links per parcel. Creating a new one replaces the oldest.
- The per-user daily counter is stored under its own anonymous hash, so it survives
/deletedatafor the rest of the day but can't be linked to anyone's parcels.
- Delete any parcel instantly. When no one tracks a number anymore, it's also removed from 17TRACK.
/deletedataor Settings > Delete my data erases everything about you immediately- Blocking the bot erases your data immediately. If the bot happens to be offline for more than a day at that moment, Telegram drops the event. Your parcels are then erased at the next attempt to message you. If you had no parcels, your encrypted settings stay until you use
/deletedata. - The bot only works in private chats. If someone adds it to a group, it leaves.
- Python 3.10+ (developed on 3.14)
- A Telegram bot token from @BotFather
- A 17TRACK API key from api.17track.net (Settings > Security > Access Key). The free plan includes 200 tracking numbers.
git clone https://github.com/Soyboy/tracking-telegram-bot.git ~/tracking-telegram-bot
cd ~/tracking-telegram-bot
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
cp .env.example .env
chmod 600 .envGenerate an encryption key and put it in .env together with your tokens:
python3 -c "import secrets,base64;print(base64.b64encode(secrets.token_bytes(32)).decode())"DATA_KEY=<generated key>
TELEGRAM_TOKEN=123456:ABC...
TRACK17_KEY=<your 17track key>Important
Back up DATA_KEY separately from the database. Without it the stored data can't be recovered. If it sits next to the database backup, the encryption protects nothing.
.venv/bin/python bot.pyOn startup the bot sets its own name, command list and English description in Telegram (turn this off with SET_PROFILE=0). To finish the profile, send logo.png to @BotFather with /setuserpic.
On first start the bot stores an encrypted check value. If it's later started with a different DATA_KEY, it refuses to start instead of silently showing everyone an empty list.
The bot is silent by design and keeps no logs. If it starts and answers /start, it works.
If something seems off (no notifications, for example), start it with DIAG=1. It then prints one line per problem to stderr, with the error type and the 17TRACK endpoint only, for example 17track gettrackinfo: HTTP 401 when the API key is wrong. It never prints tracking numbers, names, chat IDs or tokens. Turn it off again when you're done.
Note
Don't re-enable Python logging (the logging.disable(...) line in bot.py) instead.
Warning
With logging enabled, httpx logs every request URL at INFO level, and Telegram request URLs contain your bot token. Never enable logging in production or share such logs.
The bundled banner.png shows @trackshipmentbot. If you run your own bot, replace banner.png and logo.png with your own images. To use a different name, change BOT_NAME in bot.py and the word "Tracking" in locales/*.json.
For a server, run the bot as a system service under its own user. The code then belongs to root and can't be changed by the bot, and the only place it can write to is /var/lib/tracking-bot.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin tracking
sudo git clone https://github.com/Soyboy/tracking-telegram-bot.git /opt/tracking-telegram-bot
cd /opt/tracking-telegram-bot
sudo python3 -m venv .venv
sudo .venv/bin/pip install -r requirements.txt
sudo cp .env.example .env # then fill it in
sudo chown root:tracking .env
sudo chmod 640 .env
sudo cp tracking-bot.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now tracking-botThe unit stores the database in /var/lib/tracking-bot (created automatically, readable only by the tracking user) and overrides DB_PATH for that. It's sandboxed: read-only system and code, no access to home directories, a private /tmp and no devices, only network and local sockets, only the system calls a normal service needs, no capabilities, and a 512 MB memory cap. It also runs in its own user namespace, blocks privileged and resource-related system calls, and can't create memory that is both writable and executable. systemd-analyze security tracking-bot rates it at about 1.1 on systemd's 0 to 10 exposure scale (lower is better).
Note
These file-system protections need a system service. When systemd runs a unit as a regular user (systemctl --user), most of them are silently ignored.
All settings are environment variables, usually set in .env.
| Variable | Default | Description |
|---|---|---|
TELEGRAM_TOKEN |
required | Bot token from @BotFather |
TRACK17_KEY |
required | 17TRACK API access key |
DATA_KEY |
required | 32-byte base64 key for database encryption |
CHECK_INTERVAL_MIN |
15 |
How often all parcels are checked, in minutes |
MAX_PACKAGES_PER_USER |
10 |
Parcel limit per user, to protect the shared 17TRACK quota |
CHECK_COOLDOWN_SEC |
120 |
Cooldown between manual refreshes per user |
SHARE_DAYS |
7 |
How long share links stay valid |
DAILY_ADDS_PER_USER |
10 |
New packages a single user can add per day |
DAILY_ADDS_TOTAL |
50 |
New tracking numbers the whole bot can register per day |
STALE_DAYS |
30 |
Parcels with no tracking data for this many days are removed |
INACTIVE_DAYS |
90 |
Parcels whose tracking hasn't changed for this many days are removed |
NEW_ACCOUNT_DAYS |
3 |
How long an account counts as new |
NEW_ACCOUNT_DAILY_ADDS |
3 |
Daily package limit for new accounts |
BOT_TZ |
UTC |
Default time zone for silent at night (22:00 to 07:00), used until a user picks their own in Settings |
SET_PROFILE |
1 |
Set the bot's name, commands and English description in Telegram on startup. Use 0 to manage them yourself in BotFather. |
DIAG |
0 |
Troubleshooting only: 1 prints error types to stderr. See Diagnostics. |
SUPPORT_CONTACT |
empty | Optional contact shown in Help, e.g. @yourname |
DB_PATH |
data/tracking.db |
SQLite database file. Its folder is created on startup. |
BANNER_PATH |
banner.png |
Image attached to every message |
| Command | Description |
|---|---|
/start |
Main menu (also used by share links) |
/add <number> |
Track a parcel. You can also just send the number or a photo. |
/list |
Your parcels |
/refresh |
Check all your parcels now |
/settings |
Language, silent at night, time zone, delete data |
/language |
Change language |
/deletedata |
Delete all your data |
/help |
How it works |
Every user starts in English and can switch in Settings > Language. After switching, their command menu follows the chosen language too.
All UI text lives in locales/, one JSON file per language with identical keys. To add a language:
- Copy
locales/en.jsontolocales/<code>.jsonand translate the values. Keep{placeholders}, HTML tags and/commandsas they are. - Add
("<code>", "<flag> <native name>")toLANG_NAMESinbot.py. - Keep every
btn_*label unique within the file, because keyboard taps are matched by label.
Carrier detection is automatic for 3,000+ carriers. The manual override list (CARRIERS in bot.py) currently includes:
4PX, Asendia USA, Azerbaijan Post, BRT, Correos, DHL Express, DHL Netherlands, FedEx, GLS Europe, GLS Italy, Hermes Germany, iMile, InPost Italy, Japan Post, Liccardi Trasporti, Pitney Bowes, Poste Italiane, PostNord, SDA, SpeedPAK (Orange Connex), SunYou, TNT Italy, UPS, UPU, Winit
To add one, look up its numeric key in 17TRACK's carrier list and add a (key, "Name") tuple.
Stack: python-telegram-bot (async, job queue), httpx, SQLite, cryptography (AES-GCM), zxing-cpp and Pillow (QR and barcode decoding)
Design notes
- Single process, single file. The bot runs on long polling, so it needs no web server, webhooks or open ports.
- Batched polling. Parcels are fetched from 17TRACK in batches of 40. A number tracked by several users is registered and fetched only once.
- Change detection. Each parcel keeps an encrypted fingerprint of its latest status and event, so a notification goes out only when it changes, or when the user's N-hour interval is due.
- Keyboard-driven UI. Screens use a reply keyboard, and taps are mapped back to actions per screen. Previous menu screens are cleaned up so the chat stays readable.
- Self-healing. Numbers that 17TRACK has dropped are re-registered automatically, in batches of 40, within the daily limit. A failed re-registration doesn't stop the check. Users who block the bot or whose chat no longer exists are purged.
- Schema versioning. The database carries a schema version (
PRAGMA user_version), and upgrades run automatically on startup. - Responsive under load. Updates from different users are handled in parallel, while each chat is processed in order. A manual refresh during a scheduled check answers right away instead of waiting. Outgoing messages go through a rate limiter, so Telegram's flood limits don't drop notifications.
- No lost edits. Polling runs one at a time, and both polling and carrier changes are merged into the latest stored version of each parcel, so a rename or settings change made in the meantime isn't overwritten.
Database schema (everything sensitive is inside data)
packages (id = random, owner = HMAC(chat_id), number_tag = HMAC(number), data = AES-GCM blob)
users (owner = HMAC(chat_id), data = AES-GCM blob)
shares (token_tag = HMAC(token), data = AES-GCM blob, expires_at, ref = HMAC(owner, package))
meta (key, value) banner file_id, key check value and today's anonymous add counters
Project layout
.
├── bot.py the whole bot
├── locales/ UI translations (11 JSON files)
├── banner.png attached to every message
├── logo.png bot profile picture
├── tests/ unit tests
├── tracking-bot.service systemd service
├── requirements.txt
├── .env.example
└── LICENSE
- Shared 17TRACK quota. On a public instance all users share the operator's key: the free plan covers 200 numbers in total, and registrations aren't refunded when a number is deleted. The daily limits slow abuse down but don't remove it.
- Update latency. 17TRACK refreshes carrier data on its own schedule, so updates can arrive later than on the carrier's own website.
- Buttons after a restart. Menu state lives only in memory. After a restart, tapping an old button shows a fresh main menu.
- No end-to-end secrecy from the operator. See the threat model.
- Bring your own 17TRACK key: each user adds their own API key, so there's no shared quota (see
TODO.md) - 17TRACK webhooks as an alternative to polling
- More carriers in the manual override list
- Native-speaker review of all translations
.venv/bin/python -m unittest discover -s testsThe tests cover number parsing, 17TRACK response parsing, caption limits, encryption (including a check that nothing readable ends up in the database file), QR decoding, the polling logic (notifications, delivered and stale parcels, N-hour summaries), share links, daily limits and per-user silent hours. 17TRACK and Telegram are replaced by fakes. They don't need a Telegram token or a 17TRACK key.
Issues and pull requests are welcome. Before opening a PR:
- Keep the privacy guarantees intact. Don't add logging, analytics, or plaintext storage of user data.
- Add new UI strings to all files in
locales/, with the same keys in the same order. - Test the full add flow (text, photo, name, both notification modes) against a test bot.
Released under the MIT License. Copyright (c) 2026 Soyboy.