DEV Community

Alex Manner
Alex Manner

Posted on

Failed Payments, Grace Periods and Automatic Access Revocation in Telegram Communities

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:

  1. Collection retry: the payment system tries to collect money again.
  2. Webhook retry: the sender attempts to deliver a notification again.
  3. 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"
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
fintechrailsdigest profile image
Fintech Rails Digest •

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.

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

Keeping paid_through and grace_until in separate fields stops a repeat failure from quietly extending access.