Posting a Telegram story with MTProto
A Telegram story can only be posted from an account, through the MTProto method stories.sendStory — the Bot API has no such method at all. Before posting, clients call stories.canSendStory: posting as a user requires Premium, posting as a channel requires the post_stories right and enough boosts.
Updated:
How do you post a story through the Telegram API?
Three steps. First stories.canSendStory checks the right and returns how many story slots are free. Then stories.sendStory takes a vertical photo or video (up to 30 MB), the privacy rules and an optional period. The response carries an updateStoryID with the new story id, which you need for editing and deleting.
All of this lives at the MTProto layer, inside an account session. In Socialift that is the TelegramAccount client; it runs on Telethon and installs with pip install "socialift[telegram]".
A story can also be posted as a channel or supergroup, but then you must be an admin holding the post_stories right.
Bot API vs MTProto for stories
This is the first question and the most expensive one to answer the hard way. A bot token cannot post stories, and that is not a temporary restriction.
| Action | Bot API | MTProto (account) |
|---|---|---|
| Post a story | no — the method does not exist | stories.sendStory |
| See an incoming story | yes — it arrives as a message | yes |
| Reply to a story | yes | yes |
| Edit a story | no | stories.editStory |
| Delete a story | no | stories.deleteStories |
| List active stories | no | stories.getPeerStories |
A bot only receives a story and can answer it. That is why Socialift does not mirror the story methods on the Telegram (bot) client — they exist only on TelegramAccount.
Why a bot cannot post stories
A story is part of a user profile. Telegram built it for accounts and channels, not for bots: the Bot API reference has no method that sends a story, only an object for an incoming one and the ability to reply.
In practice this means that if stories are part of the content plan, the project needs an account session — with api_id, api_hash and a phone number. That is a heavier decision than a bot: the session file has to be stored encrypted and the platform limits respected.
Running both clients in one project is normal: posts and messages through the bot, stories through the account.
Check first: can_send_story
Telegram's documentation asks for exactly this: "Before posting a story, clients should invoke stories.canSendStory". You see the error before the upload, not after it.
import asyncio
from socialift import ApiError, TelegramAccount
async def main():
async with TelegramAccount.from_env() as tg:
targets = await tg.story_targets()
print("channels you can post as:", targets.chats)
try:
check = await tg.can_send_story("me")
except ApiError as exc:
print("cannot post right now:", exc)
return
print("free slots:", check.count)
asyncio.run(main())On success can_send_story() returns stories.CanSendStoryCount: count is the number of free story slots, that is the limit minus the currently active stories.
story_targets() (stories.getChatsToSend) lists the channels and supergroups you may post as. Being on that list is not a green light — boosts and limits are still checked by can_send_story().
The check is not a guarantee: if the state changes between can_send_story() and send_story() — a story posted from another device, say — the same error comes back from send_story().
Who may post, and the errors you get
When a condition is not met, Telegram returns a specific error code and the SDK maps it onto its own exception types.
| Situation | Telegram error | What to do |
|---|---|---|
| The user has no Premium | PREMIUM_ACCOUNT_REQUIRED | Personal stories need Premium; consider posting as a channel |
| No post_stories right in the channel | CHAT_ADMIN_REQUIRED | Grant admin rights including that one (PermissionError in the SDK) |
| The channel lacks boosts | BOOSTS_REQUIRED | Wait until the channel collects enough boosts |
| Active story limit | STORIES_TOO_MUCH | Wait for older ones to expire, or delete them |
| Weekly or monthly cap | STORY_SEND_FLOOD_WEEKLY_%d or _MONTHLY_%d | The wait in seconds arrives in RateLimitError.retry_after |
Duration: the period values
Telegram accepts exactly four period values and the server rejects anything else. The Socialift SDK checks this before the file is uploaded.
| Duration | period (seconds) | Who can use it |
|---|---|---|
| 6 hours | 21600 | Premium only |
| 12 hours | 43200 | Premium only |
| 24 hours | 86400 | the default, everyone |
| 48 hours | 172800 | Premium only |
If period is omitted the field is not sent at all and Telegram applies 24 hours. When it expires the story moves to the archive; with pinned=True it also stays on the profile.
Privacy: who sees the story
In MTProto this is a Vector<InputPrivacyRule>. The SDK reduces it to four shorthands and still accepts a ready-made rule list for the complicated cases.
- everyone
- The default, matching the app —
InputPrivacyValueAllowAll. Everyone sees the story. - contacts
- Contacts only —
InputPrivacyValueAllowContacts. - close_friends
- Close friends only —
InputPrivacyValueAllowCloseFriends. The list itself is managed throughcontacts.editCloseFriends. - selected
- Only the chosen users —
InputPrivacyValueAllowUsers. With this value theallowlist is mandatory. - deny
- Exceptions —
InputPrivacyValueDisallowUsers. Rules apply in the order given, sodenyis appended last: "contacts, but not these people".
Media and text: vertical, 30 MB and the caption trap
Telegram describes the media in one line: a vertical photo or a vertical video, max 30 MB. There are no documented numbers for aspect ratio or duration, so the SDK does not check them either — non-vertical media is cropped badly in the apps without raising an error.
The size cap is checked locally: when you pass a file path or bytes, anything over 30 MB raises ConfigurationError instead of being uploaded and rejected.
caption is the text under the story. Its maximum length comes from the server config (story_caption_length_limit_*) and is not fixed as a number in the docs. Formatting (entities) depends on the stories_entities config, meaning bold text and links in a story do not work on every account. Without parse_mode the text is sent plain.
noforwards=True blocks forwarding and saving — for content that should not travel further.
Posting a story with the Socialift SDK
send_story uploads the file, assembles the privacy rules and calls stories.sendStory. The response is an Updates object carrying the new story id.
import asyncio
from socialift import RateLimitError, TelegramAccount
async def main():
async with TelegramAccount.from_env() as tg:
check = await tg.can_send_story("me")
if not check.count:
print("active story limit reached")
return
try:
result = await tg.send_story(
"me",
"story.mp4",
caption="New collection",
period=43200,
privacy="close_friends",
deny=["@competitor"],
pinned=True,
noforwards=True,
)
except RateLimitError as exc:
print("weekly or monthly cap, wait:", exc.retry_after)
return
print(result)
asyncio.run(main())TelegramAccount.from_env() reads TELEGRAM_API_ID, TELEGRAM_API_HASH and TELEGRAM_SESSION. A bot token will not work here.
file takes a path, bytes, a file object or a ready InputFile. The type is inferred from the extension (.jpg and .png are photos, everything else is video); force it with photo=True or photo=False.
To post as a channel, pass it as the first argument: send_story("@my_channel", ...). Telegram then checks the post_stories right and boosts instead of Premium.
period=43200 (12 hours) is rejected on a non-Premium account — when in doubt omit period and let Telegram apply 24 hours.
Reading, editing and deleting
An edit sends only the fields you pass and leaves the rest untouched — which is why privacy defaults to None in edit_story.
import asyncio
from socialift import TelegramAccount
async def main():
async with TelegramAccount.from_env() as tg:
peer = await tg.stories("me")
for story in peer.stories:
print(story.id, story.expire_date)
await tg.edit_story("me", 42, caption="Corrected text")
await tg.edit_story("me", 42, privacy="contacts", deny=["@competitor"])
await tg.delete_story("me", [42, 43])
asyncio.run(main())stories(entity) returns the currently active stories only (stories.getPeerStories). Sometimes the response carries bare ids — fetch those separately with stories(entity, ids=[...]).
Expired stories are not in that list: they live in the archive (stories.getStoriesArchive) or on the profile (stories.getPinnedStories), reachable through raw().
Calling edit_story with nothing to change raises ConfigurationError — an empty edit is a meaningless request. Replacing file re-uploads the media in full, because per the documentation DocumentAttributes are welded to the media.
delete_story is irreversible: the active story, the pinned copy on the profile and the archived one all go.
Checklist before going live
- Make sure you are not trying to post with a bot token — the Bot API has no such method.
- Call can_send_story() before every post and check count.
- Keep the media vertical and under 30 MB; police the aspect ratio yourself, since Telegram never fixed one.
- Pass only 21600, 43200, 86400 or 172800 as period; without Premium, only 86400 works.
- With privacy="selected", the allow list is mandatory.
- Catch STORY_SEND_FLOOD_WEEKLY and MONTHLY as RateLimitError and honour retry_after.
- When posting as a channel, verify the post_stories right and the boost level in advance.
- Store the session file encrypted — it grants full access to the account.
- Formatting in story text depends on the server-side stories_entities config and does not work everywhere.
Frequently asked questions
Can a Telegram bot post stories?
No. The Bot API has no method for posting a story — a bot only sees an incoming story and can reply to it. Posting requires MTProto and an account session, which in Socialift is the TelegramAccount client.
Do I need Telegram Premium to post a story?
To post as a user, yes — otherwise you get PREMIUM_ACCOUNT_REQUIRED. To post as a channel or supergroup, Premium is irrelevant: you need the post_stories admin right and enough boosts on the channel.
How long does a story last, and can I change that?
The period parameter takes four values: 21600 (6 hours), 43200 (12), 86400 (24) and 172800 (48). On an account without Premium only 86400 works. Omit it and Telegram applies 24 hours; after that the story moves to the archive, and with pinned=True it also stays on the profile.
What are the media limits for a story?
A vertical photo or a vertical video, at most 30 MB. Telegram documents no exact numbers for aspect ratio or duration, so non-vertical media posts without an error but is cropped badly.
Can a published story be edited?
Yes, through stories.editStory: text, privacy, media and media_areas. Only the fields you pass change. Replacing the media re-uploads the file, because the documentation states that DocumentAttributes are bound to the media itself.
How do I limit who can see a story?
The privacy values are everyone, contacts, close_friends and selected (which makes the allow list mandatory). To exclude individual people use deny — it is appended as the last rule.