Appearance
Payments
Not implemented
No part of this works yet — the gateway route is live, but no bot token can be issued, so no method can be called. See implementation status.
The currency model is undecided
This is the least settled page on the site, and the uncertainty is not about a ship date — it is about what the feature is.
The specification references Telegram-style Stars (XTR). GROM separately has its own internal token, called Take, already used inside the app. These two have not been reconciled, and reconciling them is tracked as an open blocker on the project, not as an implementation detail. Nobody has decided:
- which of the two a bot would charge in, or whether it is either;
- how fiat money would enter the system at all;
- what the fee, commission or payout model looks like;
- who settles a refund, out of what balance, and on what timeline.
No payment provider is connected. The method signatures below are written to the Telegram Bot API shape so that ported code compiles — they are provisional and the money-carrying fields are the ones most likely to change shape.
Do not plan revenue around this. If your bot's business model depends on charging users, that is not something GROM can support today, and there is no date at which it will.
What is stable, and what is not
It is worth separating two things, because they have very different confidence levels.
The flow is stable. The four-step choreography below — invoice, optional shipping query, pre-checkout query, successful payment — is the same on every platform that has copied this API, and it is a sensible shape regardless of what currency ends up flowing through it. You can design your order state machine against it.
The money is not. Currency codes, provider tokens, amounts, refunds, fees and payouts all depend on the unresolved question above. Write your integration so that the parts touching money sit behind one boundary in your code, and assume you will rewrite that boundary.
The payment flow
Four steps. Two of them are yours to answer, under a clock.
bot platform buyer
│ │ │
│ sendInvoice / createInvoiceLink │
│───────────────────────────────>│ invoice message / link │
│ │────────────────────────────>│
│ │ │
│ │ taps Pay, enters details
│ │<────────────────────────────│
│ │ │
│ shipping_query (only if is_flexible) │
│<───────────────────────────────│ │
│ answerShippingQuery ~10s │ │
│───────────────────────────────>│ delivery options │
│ │────────────────────────────>│
│ │ │
│ pre_checkout_query (always) │ │
│<───────────────────────────────│ │
│ answerPreCheckoutQuery ~10s │ │
│───────────────────────────────>│ │
│ │ charge attempted │
│ message with successful_payment │
│<───────────────────────────────│ │1. Send an invoice
sendInvoice puts an invoice message in a chat. createInvoiceLink instead returns a URL you can put on a website or a button.
The difference matters more than it looks. An invoice link is not bound to a chat or to a person. Anyone who has the link can open and pay it. If you generate one per order and assume only the intended buyer can pay it, you have built a bug — scope entitlements by whoever actually paid, which you learn from the successful-payment update, not by who you handed the link to.
python
from aiogram.types import LabeledPrice
await bot.send_invoice(
chat_id=chat_id,
title="Pro plan — 1 month",
description="Unlimited alerts and priority delivery for 30 days.",
payload="order_8241f0c3", # your key, not your data
currency="USD", # provisional — see the warning above
prices=[LabeledPrice(label="Pro plan", amount=999)], # 999 = 9.99
need_email=True,
)javascript
await bot.api.sendInvoice(chatId, {
title: 'Pro plan — 1 month',
description: 'Unlimited alerts and priority delivery for 30 days.',
payload: 'order_8241f0c3',
currency: 'USD',
prices: [{ label: 'Pro plan', amount: 999 }],
need_email: true,
})bash
curl -X POST "https://api.casinoplaneta.info/bot$TOKEN/sendInvoice" \
-H 'Content-Type: application/json' \
-d '{
"chat_id": 918273645,
"title": "Pro plan — 1 month",
"description": "Unlimited alerts and priority delivery for 30 days.",
"payload": "order_8241f0c3",
"currency": "USD",
"prices": [{"label": "Pro plan", "amount": 999}]
}'
# Today: HTTP 404 — the gateway does not exist.Two things about that call trip people up regardless of platform:
Amounts are integers in the smallest unit. 999 is nine dollars ninety-nine, not nine hundred and ninety-nine dollars. The total charged is the sum of the prices array, so a single line item is fine when you do not need an itemised breakdown. Negative amounts are how a discount line is expressed.
payload is a key, not a payload. It comes back to you at pre-checkout and on success, up to 128 bytes. Put an order id in it and look the rest up server-side. Encoding the price, the plan tier or an entitlement into it means trusting a value that made a round trip through a client — and the whole reason the pre-checkout step exists is so you do not have to.
2. Shipping query — only if is_flexible
Set is_flexible and the platform will ask you what delivery costs for the address the buyer entered. You answer with answerShippingQuery.
You have about ten seconds
Look the address up against a table you precomputed. Calling a carrier's rate API inline is the classic way to make this fail: the buyer sees the checkout stall, with no message explaining why.
Answering ok: false is a normal outcome, not an error — it is how you say you do not ship there. The error_message you provide is shown to the buyer verbatim, so write it for them.
If you set is_flexible and never handle the shipping_query update, every payment on that invoice stalls. That is worth an integration test.
3. Pre-checkout query — always
Every payment raises a pre_checkout_query, and you must answer it with answerPreCheckoutQuery. This is the last moment you can stop a charge, and again you have roughly ten seconds.
An unanswered pre-checkout query fails the payment
Not "delays" — fails. The window closes, the query id stops being answerable, and the buyer is told the payment did not go through with no explanation you control. A bot that is down, restarting, or holding this update behind a slow queue loses the sale.
If your handler does anything slow, do it before the invoice is sent, not here.
What belongs here is re-checking the things that can change between the invoice being sent and the money moving: is the item still in stock, is the seat still free, did the offer expire, has this order already been paid. Use the payload to find the order.
Answering ok: true is a commitment. After that, undoing it means a refund — so validate here rather than in your successful-payment handler.
python
@dp.pre_checkout_query()
async def on_pre_checkout(q: PreCheckoutQuery):
order = await orders.get(q.invoice_payload) # payload is the key
if order is None or order.is_paid:
await q.answer(ok=False, error_message="This order is no longer available.")
return
if not await inventory.reserve(order):
await q.answer(ok=False, error_message="The last item just sold out. Nothing was charged.")
return
await q.answer(ok=True)4. Successful payment
The charge produces a message carrying a successful_payment object. That object contains the charge identifier, and that is the only handle you ever get on the transaction — store it at that moment, alongside your order. Without it you cannot refund.
Treat this handler as idempotent. At-least-once update delivery applies to payments exactly as it does to messages, and "the user was charged once but my webhook fired twice" is a bad way to learn that.
Refunds
refundStarPayment takes the user id and the charge id from the successful-payment object.
What it actually refunds — Stars, Take, fiat, something else — is exactly the unresolved question at the top of this page. Whether refunds can be partial, how long they take, and whether any fee is retained are all unknown, because they have not been decided. This page does not state them rather than guess.
The telegram_ prefix on the charge id parameter is deliberate drop-in compatibility with existing Telegram bots. It may be renamed once the currency model is settled.
Errors and limits
Payment calls return the standard error envelope — see errors for the shape and limits for pacing. Two failure modes are specific to this flow and will not appear as API errors at all:
- The window lapsed. No error is returned to you, because you never made a call. The payment simply failed for the buyer.
- The provider declined. There is no provider yet, so there is nothing to say about what a decline looks like here.
What to do today
Read the flow, design your order state machine around it, and keep the money boundary thin.
Do not announce paid features. Do not build a pricing page. Do not commit to a launch that depends on charging users through GROM, because the platform cannot do it and the shape of how it eventually might is still an open argument. When that changes, status is where it will change first.