Telegram Send Message API in Python: Send & Read Chats
The official Telegram Bot API is built for bots: a bot can only write to people who have already started a chat with it, it cannot look people up by phone number, and it cannot read the history of an arbitrary conversation. If your use case is a support inbox, CRM sync, notifications to users you already know, or community management, you usually need to act as a normal account instead. The Telegram Messaging API does that over plain REST: you log in once as a user account (not a bot), then every request carries a sessionId.
This guide is a practical Python walkthrough: send text and media, validate recipients, poll incoming messages, fetch chat history, and edit or delete what you sent. Use it only with accounts you own or are authorized to operate, and follow Telegram's Terms of Service.
What you need before you start
- An API key from your Dashboard. New accounts get free credits, so you can run every example below without paying.
- A connected session, which is the
sessionIdof a logged-in Telegram account. Logging in is a one-time flow with a login code (and a 2FA password if the account has one); it is covered step by step in the Telegram session login guide.
If you only need to know whether a number or username exists on Telegram, and do not need to send anything, the session-free Telegram number checker is the cheaper tool.
How requests are shaped
Every operation lives at https://api.opendata-api.com/api/project/telegram-messaging/<operation>. Your API key goes in key, and the session goes in sessionId. For GET and DELETE operations everything else is a query parameter. For POST and PUT operations, the operation's JSON payload is sent as a string inside a body field. Here is the first call in curl, so the shape is clear:
curl -X POST 'https://api.opendata-api.com/api/project/telegram-messaging/send-message?key=YOUR_API_KEY&sessionId=YOUR_SESSION_ID' \
-H 'Content-Type: application/json' \
-d '{"body":"{\"to\":\"me\",\"text\":\"Hello from the API\"}"}'
Sending to me posts into the account's own Saved Messages, which makes it a safe first test because nobody else receives anything.
Every reply is wrapped twice, as {"code": 200, "message": "success", "data": {"code": 200, "data": {...}}}: the outer layer comes from the platform and the inner one from the messaging service, which always answers with HTTP 200 and reports a failure in its own code, title and detail. One small helper unwraps both layers, raises on any failure and is reused in every step below:
import json
import requests
API_KEY = "YOUR_API_KEY"
SESSION_ID = "YOUR_SESSION_ID"
BASE = "https://api.opendata-api.com/api/project/telegram-messaging"
SUCCESS_CODES = {200, 201, 202, 204}
def call(method, operation, payload=None, **params):
"""Call one operation and return the innermost payload (dict, list or None). JSON payloads go in a string `body` field."""
params["key"] = API_KEY
kwargs = {"params": params, "timeout": 60}
if payload is not None:
kwargs["json"] = {"body": json.dumps(payload)}
resp = requests.request(method, f"{BASE}/{operation}", **kwargs)
if not resp.ok:
raise RuntimeError(f"{operation} failed: HTTP {resp.status_code} {resp.text}")
body = resp.json()
# Peel both envelopes: {"code": 200, "data": {"code": 201, "data": {...}}}
while isinstance(body, dict) and "code" in body:
if body["code"] not in SUCCESS_CODES:
raise RuntimeError(f"{operation} failed: {body}")
body = body.get("data")
return body
me = call("GET", "get-account", sessionId=SESSION_ID)
print(me)
get-account returns the numeric user id, phone number, username, first and last name, and a bot flag of the logged-in account. Calling it first is a cheap way to confirm exactly which account a session is connected as before you send anything.
Step 1: Send a text message
The send-message operation takes the recipient as to and the message as text. The to field accepts an @username or a bare username, a +phone in full international format, or me for Saved Messages. A bare numeric user ID is not supported unless the account already knows that peer.
call("POST", "send-message", {"to": "@some_username", "text": "Your order has shipped."}, sessionId=SESSION_ID)
call("POST", "send-message", {"to": "+15551234567", "text": "Hi, this is Anna from support."}, sessionId=SESSION_ID)
The reply confirms the send with to, text and sent. The session must have completed login, otherwise the call fails and call() raises. If a send raises an error, check the session status with get-session before retrying.
Step 2: Validate recipients before sending
Sending to a handle that does not exist wastes a call and teaches you nothing. The check-exists operation resolves a peer and reports whether it exists, together with the resolved id, username and display name. When the peer cannot be resolved it returns exists=false with a reason.
def peer_exists(peer):
info = call("GET", "check-exists", sessionId=SESSION_ID, peer=peer)
return bool(info.get("exists")), info
ok, info = peer_exists("+15551234567") # requests encodes the plus sign for you
print(ok, info)
If you build the query string by hand, write the plus sign as %2B, otherwise it is read as a space. Run the check once per recipient, cache the answer, and only send to peers that exist.
Step 3: Send media
send-media uploads a file from base64 content. Set type to image, video, audio, voice or document, pass the file as data, and optionally add caption, fileName (the document name) and mimeType to override the inferred type.
import base64
with open("invoice.pdf", "rb") as fh:
encoded = base64.b64encode(fh.read()).decode("ascii")
call("POST", "send-media", {
"to": "@some_username",
"type": "document",
"data": encoded,
"fileName": "invoice.pdf",
"caption": "Invoice for October",
}, sessionId=SESSION_ID)
Large files make large request bodies, so upload from a stable connection and keep a generous timeout.
Step 4: Read incoming messages
list-inbox returns messages received in real time while the session was connected, newest first, excluding messages the account sent itself. Each item carries the message id, chatJid, sender, type, text and timestamp. Use limit to cap the page (default 50). Because the inbox is a rolling window, polling and deduplicating by message id is the natural pattern:
import time
seen = set()
def poll_once():
items = call("GET", "list-inbox", sessionId=SESSION_ID, limit=50) or []
for msg in reversed(items): # oldest first
if msg["id"] in seen:
continue
seen.add(msg["id"])
print(msg["chatJid"], msg.get("text"))
call("POST", "mark-read", {"peer": msg["chatJid"]}, sessionId=SESSION_ID)
while True:
poll_once()
time.sleep(5)
Two limits to design around: the inbox holds the most recent 500 messages per session, and those records are cleared when the service restarts. Treat the inbox as a live feed, not as storage; write what you need to your own database as you read it. The chatJid values you see here (such as user:<id>, chat:<id> or channel:<id>) are valid peers for history, mark-read and group operations.
Step 5: Fetch chat history
For anything older than the live feed, use chat-history. It fetches recent messages for a peer directly from Telegram, not only the ones received during this session, newest first. Each message includes its numeric id, chatJid, sender, a fromMe flag, type, text and timestamp. The default limit is 20.
history = call("GET", "chat-history", sessionId=SESSION_ID, peer="@some_username", limit=50)
The peer can be an @username, a +phone, me, or a typed user:<id> / chat:<id> / channel:<id> peer taken from the inbox or chat list. To discover chats, call list-chats; note that it is built from messages observed since the session connected.
Step 6: Edit, react to and delete messages
Once you have a numeric messageId from the history or the inbox, you can act on it:
call("POST", "edit-message", {"peer": "@some_username", "messageId": 123, "text": "Corrected text"}, sessionId=SESSION_ID)
call("POST", "react-message", {"peer": "@some_username", "messageId": 123, "emoji": "π"}, sessionId=SESSION_ID)
call("POST", "revoke-message", {"peer": "@some_username", "messageId": 123}, sessionId=SESSION_ID)
Only the account's own messages can be edited. revoke-message deletes a message for everyone, and an empty-string emoji on react-message clears your reaction.
Beyond one-to-one chats
The same session can manage communities: list-groups shows the groups and channels the account belongs to, create-group starts a basic group from a list of participants, group-invite exports an invite link, and add-participants or update-admins handle membership and roles where the account has admin rights.
Error handling and best practices
- Read the inner
code, not just the HTTP status. The messaging service answers with HTTP 200 even when it rejects a request, and reports the outcome in the innercodewith atitleanddetail;call()raises on it. A platform-level401means the API key is missing or invalid, and403means insufficient credits or an inactive account. - Do not blindly retry sends. The send operations take no idempotency key, so after a timeout, read the recent history first to see whether the message went out before you send it again.
- Handle a lost session. If calls start failing, check
get-session. A saved account that is merely offline is reconnected by that call. If the stored login is gone, for example afterdisconnect-sessionwithlogout=true, create a new session as described in the login guide. - Pace your traffic. Add delays between sends, validate recipients first and avoid identical text to many people.
- Protect the
sessionId. It authorizes everything the account can do. Keep it in an environment variable or secret store, never in source control. - Disconnect cleanly.
disconnect-sessiondrops the live connection and keeps the stored login by default; addlogout=trueto log out of Telegram remotely and delete the stored login.
Compliance
Message people who expect to hear from you: customers, opted-in subscribers, transactional recipients. Unsolicited bulk messaging violates Telegram's terms and can get an account limited, and local privacy and anti-spam laws apply on top. Phone numbers are personal data, so process only the ones you have a lawful basis to use. For a WhatsApp equivalent of this workflow, see how to send WhatsApp messages via API, and for verifying contacts across channels before you message them, read the lead-verification workflow.
FAQ
Can I send Telegram messages from Python without a bot token?
Yes. The Telegram Messaging API logs in as a normal user account using a phone login code (or a QR scan), and from then on you call send-message with that account's sessionId. No bot token or bot setup is involved.
How do I send a message to a phone number or username?
Pass it as the to field of send-message: an @username or bare username, a +phone in full international format, or me to write to Saved Messages. A bare numeric user ID only works if the account already knows that peer.
How do I read incoming Telegram messages with this API?
Use list-inbox for a live feed of messages received while the session was connected, and chat-history to fetch older messages for a specific peer straight from Telegram. The inbox keeps the latest 500 messages per session and is cleared on restart, so store what you need.
What is the difference between the Telegram Bot API and this API? A bot is a separate identity with restricted abilities, such as not being able to start conversations. This API acts as a regular user account, so you can message usernames and phone numbers, read chat history and manage groups, subject to what that account is allowed to do on Telegram.
Does the session survive a restart?
The login does. After connecting, the session id (read it from get-session) is a stable ID derived from the account, and you can reconnect with create-session in restore mode without a new login code. The inbox and chat list records, however, are cleared on restart.
Is it allowed to automate a Telegram account? Automation is only appropriate for accounts you own or are authorized to operate, and it must follow Telegram's Terms of Service and the laws that apply to you. This is general guidance, not legal advice, so review the platform rules for your specific use case.
Try it
Explore the full operation list on the Telegram Messaging page, and read the send-message, send-media, list-inbox and chat-history references. See pricing for per-request costs.