One-Time Invite Links at Scale: The Leak Nobody Checks
One-Time Invite Links at Scale: The Leak Nobody Checks
Every paid Telegram channel depends on the same assumption: that a link issued to a paying subscriber admits that subscriber and nobody else.
That assumption is weaker than it looks, and the reason is a single word in Telegram's documentation that almost everyone misreads.
What member_limit actually means
From the Bot API documentation for createChatInviteLink:
member_limit — "The maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999"
Simultaneously. Not "in total."
A link with member_limit=1 does not mean one person may ever use it. It means one person who joined via that link may be in the channel at any given moment.
The consequence: when that member leaves — or when you remove them for a lapsed payment — the slot frees up and the link works again.
For a paid channel, that is a live leak. Your removal process is what re-arms it.
Consider the ordinary lifecycle: subscriber pays, gets a member_limit=1 link, joins. Three months later they lapse and you remove them. The link they were sent — still sitting in their chat history, possibly forwarded, possibly posted somewhere — becomes usable again. Anyone holding it can join. Free.
The fix
Revoke the link when you remove the member. Not when they join, not when they lapse — at removal, because removal is what re-opens the slot.
banChatMember(chat_id, user_id)
unbanChatMember(chat_id, user_id, only_if_banned=True)
revokeChatInviteLink(chat_id, invite_link)
That third line is the one that gets forgotten, and it is the one that closes the hole.
This requires storing which link was issued to which subscriber. If you are not keeping that mapping, you cannot revoke precisely, and you have no way to answer "how did this person get in?"
Never revoke the primary link as part of this. From the documentation: "If the primary link is revoked, a new link is automatically generated" — which invalidates every published copy of it, everywhere.
Belt and braces: expiry
expire_date takes a Unix timestamp after which the link stops working.
Set it. A link that is meant to be used within an hour should expire within a day. Combined with revocation you get two independent protections, and expiry keeps working even if your revocation job fails.
A reasonable pattern: issue with member_limit=1 and expire_date 24 hours out. A subscriber who doesn't join within a day asks for a new link — mildly annoying, and far better than an immortal link circulating.
The stronger pattern: join requests
creates_join_request=True makes users request to join, with each request approved or declined by you via approveChatJoinRequest / declineChatJoinRequest.
This inverts the security model. Instead of a link that grants access, you have a link that requests it — and you check payment status at the moment of approval. A leaked link then yields nothing but declined requests.
The constraint, straight from the docs: "If True, member_limit can't be specified."
They are mutually exclusive. You choose one:
| Approach | Strength | Weakness |
|---|---|---|
member_limit | Instant join, no friction | Slot frees on removal; needs revocation discipline |
creates_join_request | Verify at approval; leaks are worthless | Adds a step; you must process requests promptly |
For a paid channel of any size, join requests are the more robust design. The cost is that approval must be automatic and fast — a subscriber who pays and then waits twenty minutes is a support ticket.
Native subscription links
If you use Telegram's own Star subscriptions via createChatSubscriptionInviteLink, access and payment are bound together by Telegram — renewal and removal are handled at the platform level rather than by your revocation logic. That sidesteps most of this article.
The tradeoffs are real, though: subscription_period must be exactly 2,592,000 seconds (30 days), subscription_price is capped at 10,000 Stars, and — a detail with sharp edges — editChatSubscriptionInviteLink can only change the link's name, not its price. See tiered access for what that constraint does to pricing.
Audit your channel
Four questions. If you can't answer them, you have a leak.
1. How many active invite links exist? Most channels have accumulated far more than anyone remembers — links made for a campaign, a test, a one-off. Every one is a door.
2. Do you revoke on removal? If not, every member you have ever removed left behind a working link.
3. Do your links expire? Unexpired links from a year ago still work.
4. Do you know who was issued which link? Without that mapping you cannot revoke precisely or trace an unauthorised join.
Signs you are already leaking
- Member count higher than paid subscriber count
- Members you cannot match to any payment
- Joins clustering shortly after removals
- Your channel link appearing somewhere you didn't put it
That last one is worth a periodic search. Paid channel links get shared, and the first you hear of it is usually the member count.
The summary
member_limit is a concurrency cap, not a use counter. Removing a member re-arms their link.
Either revoke on removal with rigorous per-subscriber link tracking, or use join requests and check entitlement at approval. Both work. Doing neither means your paid channel is a public one with extra steps.
Parameter definitions quoted verbatim from Telegram's Bot API documentation, verified September 2026.