Suppose a subscriber's renewal payment fails at 10:00 UTC. Your bot removes them at 10:01. Two hours later, a payment retry succeeds, but the subscriber is still banned.
The payment retry succeeded, but the access workflow never restored the subscriber's ability to rejoin.
This walkthrough shows how to keep paid time separate from grace, set a fixed recovery deadline, and recover when removal conflicts with a later payment.
The scope is a custom bot managing access to a private Telegram community. The timeline is illustrative, and the Python function covers the access decision only. A working integration still needs billing, event processing, and Telegram delivery checks.
Give grace its own deadline
Keep payment status, entitlement, and observed membership separate. In particular, use different fields for paid time and temporary access:
| Application field | Meaning |
|---|---|
paid_through |
End of the period granted by confirmed payment |
grace_until |
End of a separately authorized recovery window |
renewal_enabled |
Whether future automatic renewal is enabled |
desired_membership |
Whether the access policy currently permits membership |
observed_membership |
Last known membership state in Telegram |
These are application fields, not a provider's universal schema. Preserve the payment and subscription references that explain each grant.
A failed renewal does not erase a period already purchased. Grace does not turn an unpaid period into a paid one.
Keep the evidence behind each deadline. Support should be able to distinguish purchased time, a recovery allowance, and a delayed removal without reconstructing them from one status flag.
Define one policy before writing handlers
Suppose Maya's paid period ends on December 1, 2026, at 10:00 UTC. Her renewal fails at that boundary.
The example community gives existing subscribers a 72-hour recovery window. This duration is an illustrative policy choice, not a Telegram default or a recommended duration for every community.
The policy is:
- Grace applies to an eligible renewal after a previously paid period, not to an unpaid first purchase.
- Its deadline is the previous paid-period end plus 72 hours.
- A repeated failure or notification does not restart the clock.
- Canceling renewal before the due date preserves paid time but does not create a recovery window for a renewal that was never due.
- If grace has already been promised, turning renewal off does not silently shorten that promise.
- Refunds, disputes, and moderation restrictions have their own explicit rules.
Record the grace decision against a stable renewal identifier, such as a provider invoice ID, with a uniqueness constraint and policy version. Schedule reconciliation at the renewal boundary even if no failure webhook has arrived: retrieve the billing state and evaluate the eligible renewal before removing access.
Do not calculate grace_until = webhook_received_at + 72 hours. Anchor it to the previous paid-period end. In this example, that same preauthorized window covers eligible renewals awaiting verification. Keep paid_through unchanged and distinguish unknown billing state from confirmed failure. Repeated events cannot restart the window. If verification remains unavailable, alert operators before the deadline; any outage exception needs separate authorization and a recorded end time. Never create new grace from an old invoice after a newer renewal has settled.
Two possible outcomes
| Time, UTC | Successful recovery | No recovery |
|---|---|---|
| Dec 1, 10:00 | Renewal fails; grace ends Dec 4, 10:00 | Same |
| Dec 2, 10:00 | Another failure; original deadline remains | Same |
| Dec 3, 09:00 | Payment settles the Dec 1–Jan 1 renewal period; grant is recorded once | Still unpaid |
| Dec 4, 10:00 | New paid period permits access; old removal task is obsolete | Temporary entitlement ends; removal is due |
This timeline uses calendar-month website billing with an unchanged billing anchor. Map the actual period covered by the settled invoice item to the access grant. Do not award a full new month from the retry time. Plan changes and prorations need their own mapping.
Separate collection retries from access grace
There are three independent retry mechanisms:
- Collection retry: the payment system tries to collect money again.
- Webhook retry: the sender attempts to deliver a notification again.
- Access-task retry: your worker attempts the Telegram operation again.
Only the first can result in a new payment. The other two must not charge the customer or grant duplicate paid time.
Stripe's retry documentation describes configurable collection behavior. Some failures require customer action or a new payment method. Do not promise another automatic attempt merely because the account is overdue.
Collection can continue after grace ends. Define the late-payment outcome before launch. Record the period a late payment settles, and restore current access only if that period or another valid grant covers the present time. A payment on January 5 for December 1–January 1 is historical settlement, not a reason to unban the subscriber. Compensation for interrupted access is a separate policy.
Explain that possibility to subscribers. If your offer instead promises no further collection after access ends, align the billing configuration with that promise.
Translate provider signals into access decisions
For an independent website subscription using Stripe, a compact mapping is:
| Signal | Application action |
|---|---|
invoice.payment_failed |
Identify the affected renewal; evaluate grace eligibility |
invoice.payment_action_required |
Direct the customer to complete the required payment action |
invoice.paid |
Validate the applicable invoice and subscription state; record the corresponding grant once |
| Subscription update or termination | Reconcile billing and cancellation policy; recompute entitlement |
Stripe's subscription webhook guide explains these events and statuses. past_due does not guarantee another collection attempt. An active subscription alone is not proof that every outstanding invoice was paid. Avoid treating either status as your complete access model.
Cancellation also needs interpretation: period-end cancellation differs from immediate termination. Preserve your paid-grant history rather than replacing it with one cancellation flag.
For digital purchases inside Telegram bots or Mini Apps, Telegram requires Stars, using XTR. An external provider integration is not a substitute for that in-app requirement.
For bot subscriptions, use subscription_expiration_date from a confirmed SuccessfulPayment to map the paid deadline. Recurring Stars periods currently use 30 days, so do not copy the calendar dates above. BotSubscriptionUpdated includes states such as failed and canceled; a failure signal does not grant paid time.
Native paid channel subscription links are a separate mechanism. This custom grace policy does not configure Telegram's native subscription behavior.
Make the access decision testable
This Python 3.10+ function evaluates approved deadlines for one subscription. The caller must validate payment evidence and authorize grace first; this function makes no provider or Telegram requests.
from datetime import datetime
def access_state(
*,
now: datetime,
paid_through: datetime | None,
grace_until: datetime | None,
blocked: bool = False,
) -> str:
for value in (now, paid_through, grace_until):
if value is not None and value.utcoffset() is None:
raise ValueError("Use timezone-aware timestamps")
if blocked:
return "blocked"
if paid_through is not None and now < paid_through:
return "paid"
if grace_until is not None and now < grace_until:
return "grace"
return "expired"
The caller supplies only grace that the policy has already authorized. For a subscriber with multiple subscriptions or other grants to the same community, evaluate all valid grants before removing access. An expired subscription must not override another current grant. Apply a community-wide moderation block separately.
At exactly grace_until, this function returns expired, unless paid time or a block produces a different result. Store timestamps as instants, use UTC in logs, and show the customer an unambiguous deadline with a timezone.
The policy deadline is not a guarantee of second-precise removal. Scheduler frequency, queues, and API failures introduce enforcement delay. Record that delay rather than quietly extending the entitlement date to match it.
Treat revocation as durable work
Track removal as pending, confirmed, or failed. Mark it confirmed only after checking the Telegram result.
When the deadline passes, recompute eligibility and write a removal task durably. Commit the decision and task together—for example, through a transactional outbox—so a crash cannot lose the action between them.
Use authenticated webhook input, durable event storage, and atomic uniqueness checks. Stripe documents duplicate events and unordered delivery. A late failure must not undo a later settled renewal. Deduplicating event IDs also does not replace deduplicating the business grant.
A removal task should carry a membership key, entitlement revision, reason, and decision time. The worker loads the latest state before acting and skips obsolete work.
That check reduces errors but does not close the whole race: payment can arrive between the database read and the Telegram request. Coordinate grant and removal operations for each user–community pair. After uncertain or concurrent results, reconcile actual membership against the latest entitlement. A database transaction cannot include Telegram in the same atomic commit.
banChatMember requires bot administrator rights. In supergroups and channels, re-entry requires unbanning, and the documented revoke_messages value is always true. Test its message-deletion behavior against your discussion archive. On recovery, unbanChatMember with only_if_banned=true avoids removing an existing member. Unbanning does not automatically rejoin someone.
Lift only a ban your billing workflow owns. A new payment must not reverse an independent moderation decision. Then offer re-entry through your controlled admission flow, checking current entitlement again.
If removal fails, retain the original deadline and mark enforcement as pending or failed. Retry transient failures with backoff; escalate missing permissions. After a timeout, check the result before assuming the action never happened.
Tell the subscriber what to do
A recovery notice should identify the failed renewal, the exact access deadline, and the required action. For Maya, it could say:
Your renewal payment was unsuccessful. Access remains available until December 4, 2026, at 10:00 UTC while you resolve it. Open billing settings to update your payment method or complete authentication. We will confirm when payment succeeds.
Do not promise a scheduled charge unless the billing system confirms one. Use the appropriate secure billing flow rather than collecting card details in chat.
Send notices for meaningful transitions: entering grace, approaching its deadline, confirmed recovery, and actual removal. Deduplicate them so repeated webhooks do not produce repeated warnings. Cancel obsolete reminders after recovery, and keep payment support reachable after community access ends.
Removal stops future membership under this workflow; it cannot recall copies of content already saved by a subscriber.
Test the failure paths
Use a payment test environment and a private test community with ordinary member accounts. These are proposed integration checks, not claimed deployment results.
| Scenario | Expected outcome |
|---|---|
| First purchase fails | No paid grant or renewal grace |
| Renewal fails before paid time ends | Existing paid access remains; grace uses the original period boundary |
| Same renewal fails again | One grace decision; unchanged deadline |
| Failure notification is delayed or verification is unavailable | Scheduled reconciliation evaluates the preauthorized window; no deadline is reset from delivery time, and unresolved verification is escalated |
| Paid renewal is followed by an older failure | Paid grant remains; no new grace or removal |
| Grace deadline is reached without recovery | Entitlement expires; a durable removal task is created |
| Payment arrives while removal is pending | Obsolete task is skipped, or the membership error is repaired |
| Payment settles after removal | Re-entry is offered only if a valid grant covers now and no independent moderation block applies; settling an ended period alone does not restore access |
| Another valid grant exists | No removal solely because this subscription expired |
| Bot loses removal permissions | Entitlement remains expired; failed enforcement is visible to operators |
Monitor expired-but-present memberships and entitled-but-banned accounts. For confirmed removals, measure enforcement lag from the applicable access deadline to confirmed removal, recording any authorized exception separately. Investigate task failures and distinguish voluntary departures from admission errors. Keep event and grant references in logs, excluding payment credentials and bot tokens.
Conclusion
An unsuccessful renewal starts a recovery workflow. Whether access continues depends on existing paid time and an explicit grace policy.
Keep the deadline fixed, record successful renewal once, and treat removal and re-entry as operations that can fail. That makes the system easier to explain to subscribers and easier to repair when billing and Telegram disagree.
Top comments (2)
The paid_through versus grace_until split is the crux. Most implementations I've seen collapse both into a single expiry field, which makes a failed renewal look identical to a lapsed subscription, and then the removal job can't tell whether it's doing its job or destroying an entitlement someone already paid for. Keeping "a failed renewal does not erase a period already purchased" as an invariant in code rather than a support policy is what makes recovery possible at all. The three-retry separation is worth its own section too: only one of collection, webhook, and access-task retries should ever be allowed to mint paid time, and the other two have to be idempotent no-ops from the entitlement side. That constraint alone prevents most duplicate-grant bugs.
Keeping paid_through and grace_until in separate fields stops a repeat failure from quietly extending access.