Telegram Stars refunds and the 21-day window: what a bot that pays out for Stars must handle
If your bot sells something that cannot be taken back (crypto, a gift card, a payout of any kind) for Telegram Stars, a refund or chargeback can leave you with neither the Stars nor the goods. This post collects what the
If your bot sells something that cannot be taken back (crypto, a gift card, a payout of any kind) for Telegram Stars, a refund or chargeback can leave you with neither the Stars nor the goods. This post collects what the Bot API and Telegram's terms actually say about refunds, and the pattern we use in a production bot: hold the payout through the refund window, detect refunds by polling getStarTransactions, and let users post collateral to skip the hold.
What Telegram says
Stars are not available at once. Bot Platform Developer Terms, section 6.2.4:
For technical reasons, Stars in your balance may not become available for advertising credits or rewards for up to 21 days after their receipt.
Chargebacks hit the bot's balance. From the Bot API description of StarTransaction:
Note that if the buyer initiates a chargeback with the payment provider from whom they acquired Stars (e.g., Apple, Google) following this transaction, the refunded Stars will be deducted from the bot's balance. This is outside of Telegram's control.
The developer terms add that the seller "will be responsible for the funds returned" and that Telegram "expressly discourages the real-time sale of digital products" that cannot be reclaimed after a successful dispute or chargeback.
You must handle disputes. Payments in Telegram Stars requires bots to answer /paysupport and to be "prepared to correctly process disputes and chargebacks". A bot refunds a payment itself with refundStarPayment(user_id, telegram_payment_charge_id).
None of these pages gives a deadline for chargebacks. The 21 days is the availability delay, and it is the window most services use for their own hold.
The attack
- Buy Stars through the App Store or Google Play.
- Pay your bot's invoice; the bot sends crypto immediately.
- File a chargeback with the store. The Stars are deducted from the bot's balance.
The crypto is gone for good. This is not theoretical: it happened to us before we added a hold.
Pattern 1: hold the payout through the window
On successful_payment, store telegram_payment_charge_id together with your order. If the order is above a risk threshold, give it a hold_until = paid_at + 21 days and pay out from a scheduled job once that time has passed. Show the user the exact release date; a hold without a date looks like a scam.
Keep the threshold and the per-user risk rules on the server. If the quote endpoint reveals exactly which users or amounts are held, it tells an attacker how to stay just under the line.
Pattern 2: detect refunds by polling getStarTransactions
There is no webhook for a chargeback. Refunds show up in the bot's transaction list instead:
-
getStarTransactions(offset, limit)returns transactions "in chronological order", up to 100 per call. Keep the offset in persistent storage and walk forward from it. - A refund is an outgoing transaction:
receiveris set (the docs name "a user for a purchase refund" as an example), withreceiver.type = "user"andreceiver.transaction_type = "invoice_payment". -
receiver.invoice_payloadis optional, so do not rely on it alone. -
StarTransaction.id"coincides with the identifier of the original transaction for refund transactions" and, for incoming payments, "coincides with SuccessfulPayment.telegram_payment_charge_id". That gives a reliable link from a refund back to your order.
Match by payload first and fall back to the charge id. If neither gives exactly one order, do not guess: a fuzzy match can punish the wrong user. On a confirmed match, mark the order forfeited, record the debt on the account and block it.
Pattern 3: collateral instead of waiting
A hold protects you, but a 21-day wait is bad for users who sell regularly. The way around it is collateral: the user deposits crypto, and while its free amount covers the refund risk of a swap, that swap pays out at once.
Rules that kept it safe for us:
- Reserve, then pay. Reserve collateral for a swap after the payment is confirmed and before the payout, under a lock (we use Symfony Lock), so two parallel payouts cannot both spend the same collateral.
- Freeze per swap. Each reserved portion has its own unlock date, the end of that swap's refund window. One deposit can have several.
- Forfeit on refund. The refund detector from pattern 2 forfeits exactly the collateral reserved for the refunded swap.
- Cooldown after withdrawal. A short cooldown after a collateral withdrawal stops a deposit, swap, withdraw, refund loop.
- Cover held swaps first. When new collateral arrives, release swaps that are already waiting before new ones use it.
Checklist
- [ ]
telegram_payment_charge_idstored with every order - [ ] payouts above the threshold held, with the release date shown to the user
- [ ]
getStarTransactionspolled from a persisted offset - [ ] refunds matched by payload, then by charge id, with no fuzzy matching
- [ ] refund leads to forfeit, debt and block
- [ ] collateral reserved under a lock and frozen per swap
- [ ]
/paysupportanswered
Disclosure: we build StarGate, a Telegram bot and web app that swaps Stars for GRAM, USDT, ETH and BTC, and the patterns above come from running it. This post was drafted by an AI assistant from our code and notes, and its quotes were checked against the Telegram documentation linked above.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.