Placing orders through an SMM panel API is the easy part. The annoying part comes after: which orders finished, which ones went Partial, and which ones are still sitting in Pending an hour later? If you resell for clients, you end up refreshing the orders page all day.
In this tutorial we will build a small Python script that watches your orders and sends you a Telegram message every time an order changes status. It uses only the Python standard library (no pip install), keeps its state in SQLite, retries on network errors and checks up to 100 orders in a single API call.
Disclosure: I work on SafeSMM, so the examples use the SafeSMM endpoint (https://safesmm.net/api/v2). The script uses the common API v2 format, so it should work with most panels after you change the URL.
What we are building
python order_watcher.py add 23501 "Client A - 5k views"
python order_watcher.py once
And in Telegram you get:
Order 23501: In progress → Completed
Client A - 5k views
Start count: 3572
Charge: 0.27 USD
Features:
- Watch any number of order IDs, each with an optional note (client name, link, anything)
- One API request per 100 orders, using the multi-order
statuscall - A message only when the status changes, so no spam
- Orders that reach a final status (Completed, Partial, Canceled, Refunded) are no longer checked
- Retries with backoff (2s, 4s, 8s) when the network or the panel has a bad moment
- Works without Telegram too: it just prints the messages
The one API call that matters
Most panels let you ask for the status of many orders at once with the orders parameter (comma separated):
curl -X POST https://safesmm.net/api/v2 \
-d "key=YOUR_API_KEY" \
-d "action=status" \
-d "orders=23501,23502,99999"
The answer is one object per order ID:
{
"23501": {"charge": "0.27", "start_count": "3572", "status": "Completed", "remains": "0", "currency": "USD"},
"23502": {"charge": "0.40", "start_count": "120", "status": "Partial", "remains": "157", "currency": "USD"},
"99999": {"error": "Incorrect order ID"}
}
Two kinds of errors are worth handling separately:
-
The whole request fails, for example
{"error": "Invalid API key."}. Retrying will not help, so we alert once and stop. -
One order fails inside a good response (like
99999above). We alert for that order and stop watching it, and the other orders carry on normally.
If you are not sure what each status means for your client (for example why Partial orders get a refund of the remains), I wrote a plain-language guide: SMM panel order status meanings.
Setup
You need Python 3.8+ and three values:
- Panel API key: from your panel account or API page.
-
Telegram bot token: message
@BotFather, send/newbot, copy the token. -
Your chat ID: message
@userinfobotand copy the number. Then send any message to your new bot once, so it is allowed to write to you.
Put them in environment variables (Linux/macOS):
export PANEL_KEY="your_panel_api_key"
export TG_TOKEN="123456:ABC..."
export TG_CHAT_ID="123456789"
# optional
export PANEL_URL="https://safesmm.net/api/v2"
export POLL_SECONDS=300
On Windows PowerShell use $env:PANEL_KEY="..." instead of export.
Never hard-code the API key in the file. If you share the script or push it to GitHub, the key goes with it, and anyone with your key can spend your balance.
The full script
Save this as order_watcher.py:
#!/usr/bin/env python3
"""order_watcher.py - Telegram alerts when your SMM panel orders change status.
Works with any SMM panel that uses the standard API v2 format.
Only the Python standard library is used (no pip install needed).
Usage:
python order_watcher.py add 23501 "Client A - 5k views"
python order_watcher.py list
python order_watcher.py once # check one time (good for cron)
python order_watcher.py run # keep checking every POLL_SECONDS
"""
import html
import json
import os
import sqlite3
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
PANEL_URL = os.getenv("PANEL_URL", "https://safesmm.net/api/v2")
PANEL_KEY = os.getenv("PANEL_KEY", "")
TG_TOKEN = os.getenv("TG_TOKEN", "")
TG_CHAT_ID = os.getenv("TG_CHAT_ID", "")
POLL_SECONDS = int(os.getenv("POLL_SECONDS", "300"))
DB_PATH = os.getenv("DB_PATH", "orders.db")
FINAL = {"completed", "partial", "canceled", "cancelled", "refunded"}
BATCH = 100 # most panels accept up to 100 IDs per status call
class PanelError(Exception):
"""The panel answered, but with an error (bad key, bad order ID...)."""
# ---------- HTTP with retries ----------
def post_form(url, fields, tries=4, timeout=30):
data = urllib.parse.urlencode(fields).encode()
delay = 2
for attempt in range(1, tries + 1):
try:
req = urllib.request.Request(url, data=data,
headers={"User-Agent": "order-watcher/1.0"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
# 4xx (except 429) will not fix itself, so do not retry it
if e.code < 500 and e.code != 429:
raise
err = e
except (urllib.error.URLError, TimeoutError, ValueError) as e:
err = e
if attempt < tries:
time.sleep(delay)
delay *= 2 # 2s, 4s, 8s
raise ConnectionError(f"giving up after {tries} tries: {err}")
def panel(action, **params):
result = post_form(PANEL_URL, {"key": PANEL_KEY, "action": action, **params})
if isinstance(result, dict) and "error" in result:
raise PanelError(result["error"])
return result
def statuses(order_ids):
"""Return {order_id: info_dict} using the multi-order status call."""
out = {}
for i in range(0, len(order_ids), BATCH):
chunk = order_ids[i:i + BATCH]
result = panel("status", orders=",".join(str(o) for o in chunk))
for key, info in result.items():
out[int(key)] = info
return out
# ---------- Telegram ----------
def notify(text):
if not (TG_TOKEN and TG_CHAT_ID):
print(text) # no Telegram set up: just print it
return
url = f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage"
post_form(url, {"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "HTML"})
def format_change(order_id, note, old, info):
status = info.get("status", "?")
lines = [f"<b>Order {order_id}</b>: {html.escape(old or 'new')} → <b>{html.escape(status)}</b>"]
if note:
lines.append(html.escape(note))
if info.get("start_count") not in (None, ""):
lines.append(f"Start count: {info['start_count']}")
if info.get("remains") not in (None, "", "0"):
lines.append(f"Remains: {info['remains']}")
if info.get("charge") not in (None, ""):
lines.append(f"Charge: {info['charge']} {info.get('currency', '')}".strip())
return "\n".join(lines)
# ---------- storage ----------
def db():
con = sqlite3.connect(DB_PATH)
con.execute("""CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY, note TEXT DEFAULT '', status TEXT DEFAULT '',
done INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)""")
return con
def add(order_id, note=""):
with db() as con:
con.execute("INSERT OR IGNORE INTO orders (id, note) VALUES (?, ?)", (order_id, note))
print(f"watching order {order_id}")
def list_orders():
with db() as con:
for oid, note, status, done in con.execute(
"SELECT id, note, status, done FROM orders ORDER BY id"):
flag = "done" if done else "watching"
print(f"{oid:>10} {status or '-':<12} {flag:<9} {note}")
def check_once():
con = db()
rows = con.execute("SELECT id, note, status FROM orders WHERE done = 0").fetchall()
if not rows:
print("nothing to watch")
return 0
try:
current = statuses([r[0] for r in rows])
except PanelError as e:
notify(f"⚠️ Panel error: {html.escape(str(e))}")
return 0
changed = 0
for oid, note, old in rows:
info = current.get(oid, {})
if "error" in info:
notify(f"⚠️ Order {oid}: {html.escape(info['error'])}")
con.execute("UPDATE orders SET done = 1 WHERE id = ?", (oid,))
continue
new = info.get("status", "")
if new and new != old:
notify(format_change(oid, note, old, info))
changed += 1
done = 1 if new.lower() in FINAL else 0
con.execute("UPDATE orders SET status = ?, done = ?, updated_at = ? WHERE id = ?",
(new, done, int(time.time()), oid))
con.commit()
con.close()
return changed
def main(argv):
if len(argv) < 2:
print(__doc__)
return 1
cmd = argv[1]
if cmd == "add" and len(argv) >= 3:
add(int(argv[2]), " ".join(argv[3:]))
elif cmd == "list":
list_orders()
elif cmd in ("once", "run"):
if not PANEL_KEY:
print("Set PANEL_KEY first (your panel API key).")
return 1
while True:
try:
n = check_once()
print(time.strftime("%H:%M:%S"), f"{n} change(s)")
except ConnectionError as e:
print("network problem, will try again:", e)
if cmd == "once":
break
time.sleep(POLL_SECONDS)
else:
print(__doc__)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
How it works
post_form() sends a form POST and parses JSON. On a 5xx, a 429 (too many requests), a timeout or broken JSON it waits and tries again, doubling the wait each time. A normal 4xx is not retried, because it will not fix itself.
statuses() splits your order IDs into chunks of 100 and calls action=status with orders=.... Watching 250 orders costs 3 requests per check, not 250. Your panel will thank you.
check_once() compares each new status with the one saved in SQLite. If it changed, it sends a message. Final statuses are marked done = 1, so the next check skips them.
notify() calls the Telegram Bot API directly with sendMessage. No library needed. Notes are passed through html.escape(), because Telegram rejects messages with unescaped < or & in HTML mode.
Running it
Run it in a loop:
python order_watcher.py run
Or, better on a server, run one check every 5 minutes with cron:
crontab -e
# add this line:
*/5 * * * * cd /home/you/watcher && PANEL_KEY=... TG_TOKEN=... TG_CHAT_ID=... /usr/bin/python3 order_watcher.py once >> watcher.log 2>&1
On Windows, Task Scheduler can run python order_watcher.py once every 5 minutes in the same way.
Check what you are watching at any time:
$ python order_watcher.py list
23501 Completed done Client A - 5k views
23502 Partial done
23503 Pending watching Client B
Testing without spending money
I tested the script against a tiny local mock server that returns Pending, then In progress, then Completed, plus a fake 500 error on the first call to check the retry. Point PANEL_URL at your own mock and leave TG_TOKEN empty: every alert is printed in the terminal instead of being sent.
A quick real-world check that costs nothing: run it with a wrong key. You should see:
⚠️ Panel error: Invalid API key.
Ideas to extend it
- Add new orders automatically: when you place an order with
action=add, calladd()with the returned order ID. - Send a daily summary: completed today, partial today, total charge.
- Auto refill: for orders on services with a refill button, call
action=refillwhen your client reports a drop. - Send alerts to your clients' own Telegram chats by storing a chat ID next to each order.
More from this series
- How to Connect Your App to an SMM Panel API (PHP and Python Examples)
- Build a Telegram Bot to Manage SMM Panel Orders with Python
If you prefer a ready client, there is also a small package: pip install safesmm (PyPI). More examples are in the GitHub repo, and the full endpoint list is in the SafeSMM API docs.
Questions or a panel that answers in a different format? Leave a comment and I will help you adapt the script.
Top comments (0)