├── .gitignore
├── README.md
├── bot
├── commands
│ ├── __init__.py
│ ├── balance.py
│ ├── refund.py
│ └── start.py
├── handlers
│ └── payment.py
└── utils
│ ├── __init__.py
│ └── errors.py
├── config.py
├── files
├── example.png
└── stars.svg
├── locales
└── en
│ └── messages.ftl
├── main.py
└── requirements.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | build/
8 | develop-eggs/
9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 |
23 | # Virtual Environment
24 | venv/
25 | .venv/
26 | env/
27 |
28 | # IDE
29 | .vscode/
30 | .idea/
31 | *.swp
32 | *.swo
33 | *~
34 |
35 | # Logs
36 | *.log
37 |
38 | # OS
39 | .DS_Store
40 | Thumbs.db
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
Telegram Stars Payment Bot
5 |
6 |
7 | Process Telegram Stars payments, issue refunds, and monitor bot balance with localized error handling.
8 |
9 |
10 |
11 | Report Bug
12 | ·
13 | Request Feature
14 | ·
15 | Contact Author
16 |
17 |
18 |
19 | ---
20 |
21 | ## Features
22 |
23 | - Create Stars payment invoice via `/start`
24 | - Alternative: generate payment link (commented example in `payment.py`)
25 | - Perform refunds with `/refund `
26 | - View bot Stars balance using `/balance`
27 | - Clear, structured refund error messages (Telegram charge status aware)
28 |
29 | ## Screenshot / Example
30 |
31 |
32 |

33 |
34 |
35 | ## Quick Start
36 |
37 | ### 1. Clone & Install
38 |
39 | ```bash
40 | git clone https://github.com/bohd4nx/stars-payment.git
41 | cd stars-payment
42 | pip install -r requirements.txt
43 | ```
44 |
45 | ### 2. Configuration
46 |
47 | Edit `config.py`:
48 |
49 | ```python
50 | API_TOKEN = "your_bot_token_here" # Obtain at @BotFather
51 | ```
52 |
53 | ### 3. Run
54 |
55 | ```bash
56 | python main.py
57 | ```
58 |
59 | ## Commands
60 |
61 | | Command | Description |
62 | | ------------------------------------ | --------------------------------------- |
63 | | `/start` | Send a Stars payment invoice |
64 | | `/refund ` | Attempt refund of a prior Stars payment |
65 | | `/balance` | Show current Stars balance of the bot |
66 |
67 | ---
68 |
69 |
70 |
Made with ❤️ by @bohd4nx
71 |
Star ⭐ this repo if it helps your Telegram payments!
72 |
73 |
--------------------------------------------------------------------------------
/bot/commands/__init__.py:
--------------------------------------------------------------------------------
1 | from .balance import router as balance_router
2 | from .refund import router as refund_router
3 | from .start import router as start_router
4 |
5 | __all__ = ['start_router', 'refund_router', 'balance_router']
6 |
--------------------------------------------------------------------------------
/bot/commands/balance.py:
--------------------------------------------------------------------------------
1 | from aiogram import Router, Bot
2 | from aiogram.filters import Command
3 | from aiogram.types import Message
4 | from aiogram_i18n import I18nContext
5 |
6 | router = Router(name=__name__)
7 |
8 |
9 | @router.message(Command("balance"))
10 | async def process_balance(message: Message, bot: Bot, i18n: I18nContext) -> None:
11 | balance = await bot.get_my_star_balance()
12 | me = await bot.me()
13 | await message.reply(
14 | i18n.get("balance-info", username=me.username, amount=balance.amount)
15 | )
16 |
--------------------------------------------------------------------------------
/bot/commands/refund.py:
--------------------------------------------------------------------------------
1 | from aiogram import Router, Bot
2 | from aiogram.exceptions import TelegramAPIError
3 | from aiogram.filters import Command
4 | from aiogram.methods.refund_star_payment import RefundStarPayment
5 | from aiogram.types import Message
6 | from aiogram_i18n import I18nContext
7 |
8 | from bot.utils import get_error_message
9 |
10 | router = Router(name=__name__)
11 |
12 |
13 | @router.message(Command("refund"))
14 | async def process_refund(message: Message, bot: Bot, i18n: I18nContext) -> None:
15 | parts = message.text.split()
16 | user_id = None
17 | transaction_id = None
18 |
19 | if len(parts) != 3:
20 | await message.reply(i18n.get("refund-invalid"))
21 | return
22 |
23 | try:
24 | user_id, transaction_id = int(parts[1]), parts[2]
25 | result = await bot(RefundStarPayment(
26 | user_id=user_id,
27 | telegram_payment_charge_id=transaction_id
28 | ))
29 |
30 | if result:
31 | await message.reply(i18n.get("refund-success"))
32 | else:
33 | await message.reply(i18n.get("refund-error", error="Unknown error"))
34 |
35 | except ValueError:
36 | await message.reply(i18n.get("refund-invalid"))
37 | except TelegramAPIError as e:
38 | await message.reply(get_error_message(i18n, str(e), user_id, transaction_id))
39 |
--------------------------------------------------------------------------------
/bot/commands/start.py:
--------------------------------------------------------------------------------
1 | from aiogram import Router
2 | from aiogram.filters import Command
3 | from aiogram.types import Message
4 | from aiogram_i18n import I18nContext
5 |
6 | router = Router(name=__name__)
7 |
8 |
9 | @router.message(Command("start"))
10 | async def start_intro(message: Message, i18n: I18nContext) -> None:
11 | await message.reply(i18n.get("start-intro"))
12 |
--------------------------------------------------------------------------------
/bot/handlers/payment.py:
--------------------------------------------------------------------------------
1 | from aiogram import Router, F, html, Bot
2 | from aiogram.exceptions import TelegramAPIError
3 | from aiogram.types import Message, LabeledPrice, PreCheckoutQuery
4 | from aiogram_i18n import I18nContext
5 |
6 | router = Router(name=__name__)
7 |
8 |
9 | @router.message(F.text.regexp(r"^\d{1,6}$"))
10 | async def handle_amount(message: Message, bot: Bot, i18n: I18nContext) -> None:
11 | """
12 | Flow:
13 | 1. User sends a number 1–100000 (raw Stars units)
14 | 2. Validate range; build single LabeledPrice (Stars requires exactly one component)
15 | 3. Call send_invoice with currency 'XTR' and empty provider_token (rule for Stars)
16 | 4. Optionally (commented) build a payment link instead of direct invoice.
17 |
18 | Core send_invoice parameters (Stars context):
19 | chat_id: user chat id (invoice destination)
20 | title: localized product/invoice title (1–32 chars)
21 | description: localized description (1–255 chars)
22 | payload: internal tracking string (NOT shown to user) – embeds amount
23 | currency: must be 'XTR' for Telegram Stars
24 | prices: list[LabeledPrice] – MUST contain exactly ONE item for Stars
25 | provider_token: empty string for Stars; normal fiat providers supply real token
26 | start_parameter: deep-link tag; forwarded copies behavior differs if present
27 |
28 | Optional (reference only): business_connection_id, subscription_period, photo_* metadata.
29 | Ignored for Stars: tips, user data requirements, shipping, flexible pricing.
30 |
31 | Docs: sendInvoice API – https://core.telegram.org/bots/api#sendinvoice
32 | Stars Overview – https://telegram.org/blog/telegram-stars
33 | Stars Bot API – https://core.telegram.org/bots/payments-stars
34 | """
35 | try:
36 | amount = int(message.text)
37 | except ValueError:
38 | await message.reply(i18n.get("amount-invalid"))
39 | return
40 | if not (1 <= amount <= 100000):
41 | await message.reply(i18n.get("amount-invalid"))
42 | return
43 |
44 | try:
45 | await bot.send_invoice(
46 | chat_id=message.chat.id,
47 | title=i18n.get("invoice-title"),
48 | description=i18n.get("invoice-description"),
49 | provider_token="", # Empty for Stars
50 | currency="XTR", # Stars currency code
51 | prices=[LabeledPrice(label=i18n.get("invoice-label"), amount=amount)], # Exactly one item
52 | start_parameter='stars-payment',
53 | payload=f'stars-payment-{amount}' # Internal tracking
54 | # Optional parameters for Stars payments:
55 | # business_connection_id=None,
56 | # subscription_period=None, # Must be 2592000 (30 days) if used
57 | # photo_url=None,
58 | # photo_size=None,
59 | # photo_width=None,
60 | # photo_height=None,
61 | )
62 |
63 | # Alternative: create a payment link instead of direct invoice.
64 | # Uncomment block below to send a link instead.
65 | # try:
66 | # invoice_link = await bot.create_invoice_link(
67 | # title=i18n.get("invoice-title"),
68 | # description=i18n.get("invoice-description"),
69 | # payload=f'stars-payment-{amount}',
70 | # provider_token="", # Empty for Stars
71 | # currency="XTR",
72 | # prices=[LabeledPrice(label=i18n.get("invoice-label"), amount=amount)]
73 | # )
74 | # await message.answer(
75 | # i18n.get("payment-link", link=html.quote(invoice_link))
76 | # )
77 | # except TelegramAPIError:
78 | # pass
79 | except TelegramAPIError:
80 | await message.reply(i18n.get("invoice-error"))
81 |
82 |
83 | @router.pre_checkout_query()
84 | async def handle_pre_checkout(pre_checkout_query: PreCheckoutQuery, bot: Bot) -> None:
85 | await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True)
86 |
87 |
88 | @router.message(F.successful_payment)
89 | async def handle_successful_payment(message: Message, i18n: I18nContext) -> None:
90 | info = message.successful_payment
91 | await message.reply(
92 | i18n.get(
93 | "payment-success",
94 | amount=info.total_amount,
95 | transaction_id=html.quote(info.telegram_payment_charge_id)
96 | )
97 | )
98 |
--------------------------------------------------------------------------------
/bot/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .errors import get_error_message
2 |
3 | __all__ = ['get_error_message']
4 |
--------------------------------------------------------------------------------
/bot/utils/errors.py:
--------------------------------------------------------------------------------
1 | ERROR_KEYS = {
2 | "CHARGE_ALREADY_REFUNDED": "charge-already-refunded",
3 | "CHARGE_NOT_FOUND": "charge-not-found",
4 | "REFUND_FAILED": "refund-failed",
5 | "DEFAULT": "refund-default"
6 | }
7 |
8 |
9 | def _format_tx_id(tx_id: str) -> str:
10 | return f"{tx_id[:6]}...{tx_id[-6:]}" if len(tx_id) > 12 else tx_id
11 |
12 |
13 | def get_error_message(i18n, error_text: str, user_id: int, transaction_id: str) -> str:
14 | upper = error_text.upper() if error_text else ""
15 | error_code = next((code for code in ERROR_KEYS if code in upper), "DEFAULT")
16 | key = ERROR_KEYS.get(error_code, ERROR_KEYS["DEFAULT"])
17 | tx_short = _format_tx_id(transaction_id) if transaction_id else "-"
18 | return i18n.get(
19 | key,
20 | tx_short=tx_short,
21 | user_id=str(user_id),
22 | error=error_text
23 | )
24 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | API_TOKEN: str = '' # Your bot token from @BotFather
2 |
--------------------------------------------------------------------------------
/files/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bohd4nx/stars-payment/057cbcf6a018e157fcc26e57458295242cc9de11/files/example.png
--------------------------------------------------------------------------------
/files/stars.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/locales/en/messages.ftl:
--------------------------------------------------------------------------------
1 | start-intro =
2 | ⭐️ Telegram Stars Payment Demo Bot
3 |
4 | This bot demonstrates payment, refund, and balance features with Telegram Stars.
5 |
6 | How to pay: send a number (1 – 100000) as a message and you will receive an invoice for that many ⭐️.
7 |
8 | Refund: Use /refund <user_id> <transaction_id> after a successful payment.
9 | Balance: Use /balance to view current Stars balance of the bot.
10 |
11 | Send an amount now to generate an invoice.
12 |
13 | refund-invalid =
14 | ❌ Please use format: /refund '<user_id>' '<transaction_id>'
15 |
16 | ℹ️ Example: /refund 123456789 ABC123XYZ
17 |
18 | refund-default =
19 | ❌ Refund failed
20 |
21 | 🆔 Transaction: { $tx_short }
22 | 👤 User ID: { $user_id }
23 |
24 | 💭 Error details:
25 | { $error }
26 |
27 | refund-failed =
28 | ❌ Refund failed
29 |
30 | 🆔 Transaction: { $tx_short }
31 | 👤 User ID: { $user_id }
32 |
33 | ⚠️ The bot may have insufficient balance or a Telegram-side error occurred.
34 |
35 | charge-already-refunded =
36 | 💰 Refund already processed
37 |
38 | 🆔 Transaction: { $tx_short }
39 | 👤 User ID: { $user_id }
40 |
41 | ℹ️ This payment has already been refunded.
42 |
43 | charge-not-found =
44 | ❓ Transaction not found
45 |
46 | 🆔 Transaction: { $tx_short }
47 | 👤 User ID: { $user_id }
48 |
49 | ⚠️ The specified transaction does not exist.
50 |
51 | payment-success =
52 | 🎉 Payment successful!
53 |
54 | 💵 Amount: { $amount }⭐️
55 |
56 | 🆔 Transaction ID: { $transaction_id }
57 |
58 | refund-error =
59 | ❌ Failed to refund payment: { $error }
60 |
61 | amount-invalid =
62 | ❌ Invalid amount
63 |
64 | Please send a whole number between 1 and 100000.
65 |
66 | ℹ️ Example: 150
67 |
68 | balance-info =
69 | 💰 Bot (@{ $username }) balance: { $amount }⭐️
70 |
71 | payment-link = Payment link: { $link }
72 |
73 | refund-success = ✅ Payment has been successfully refunded!
74 |
75 | invoice-description = Payment for services via Stars.
76 |
77 | invoice-error = ❌ Failed to create payment invoice
78 |
79 | invoice-label = Stars Payment
80 |
81 | invoice-title = Stars Payment Example
82 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 |
4 | from aiogram import Bot, Dispatcher
5 | from aiogram.client.default import DefaultBotProperties
6 | from aiogram.enums import ParseMode
7 | from aiogram_i18n import I18nMiddleware
8 | from aiogram_i18n.cores.fluent_runtime_core import FluentRuntimeCore
9 |
10 | from bot.commands import start_router
11 | from bot.commands.balance import router as balance_router
12 | from bot.commands.refund import router as refund_router
13 | from bot.handlers.payment import router as payment_handlers_router
14 | from config import API_TOKEN
15 |
16 | logging.basicConfig(
17 | level=logging.ERROR,
18 | format='[%(asctime)s] - %(levelname)s: %(message)s',
19 | datefmt='%H:%M:%S'
20 | )
21 | dispatcher_logger = logging.getLogger('aiogram.dispatcher')
22 | dispatcher_logger.setLevel(logging.INFO)
23 |
24 | bot = Bot(
25 | token=API_TOKEN,
26 | default=DefaultBotProperties(
27 | parse_mode=ParseMode.HTML,
28 | link_preview_is_disabled=True
29 | )
30 | )
31 | dp = Dispatcher()
32 |
33 | i18n_middleware = I18nMiddleware(
34 | core=FluentRuntimeCore(path="locales/{locale}"),
35 | default_locale="en"
36 | )
37 |
38 | dp.include_router(start_router)
39 | dp.include_router(payment_handlers_router)
40 | dp.include_router(balance_router)
41 | dp.include_router(refund_router)
42 |
43 | dp.update.middleware(i18n_middleware)
44 | dp.message.middleware(i18n_middleware)
45 | dp.callback_query.middleware(i18n_middleware)
46 |
47 | i18n_middleware.setup(dispatcher=dp)
48 |
49 |
50 | async def main():
51 | await dp.start_polling(bot, skip_updates=True)
52 |
53 |
54 | if __name__ == "__main__":
55 | asyncio.run(main())
56 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bohd4nx/stars-payment/057cbcf6a018e157fcc26e57458295242cc9de11/requirements.txt
--------------------------------------------------------------------------------