payments
build telegram payments, stars invoices, shipping checks, pre-checkout validation, and paid media flows with yaebal.
the flow
telegram payments are a four-step protocol: send an invoice, optionally answer a shipping query, answer the pre-checkout query within 10 seconds, then handle the successful payment service event.
send a stars invoice
stars use currency: "XTR". for stars, the provider token is omitted or empty and the
prices array usually has one item.
await bot.api.call("sendInvoice", {
chat_id: ctx.chat!.id,
title: "pro plan",
description: "one month of access",
payload: "pro_monthly",
currency: "XTR",
prices: [{ label: "pro", amount: 250 }],
});answer pre-checkout
this is mandatory. if you do not answer, telegram cancels the payment. validate stock, plan id, user eligibility, and price-derived payloads here.
bot.on("pre_checkout_query", async (ctx) => {
const query = ctx.update.pre_checkout_query!;
const ok = await isOrderValid(query.invoice_payload, query.from.id);
await ctx.api.call("answerPreCheckoutQuery", {
pre_checkout_query_id: query.id,
ok,
error_message: ok ? undefined : "order is no longer available",
});
});shipping query
if the invoice needs shipping, telegram asks for available shipping options before checkout.
bot.on("shipping_query", async (ctx) => {
const query = ctx.update.shipping_query!;
await ctx.api.call("answerShippingQuery", {
shipping_query_id: query.id,
ok: true,
shipping_options: [
{
id: "standard",
title: "standard",
prices: [{ label: "shipping", amount: 0 }],
},
],
});
});successful payment
grant access only after the successful_payment service event arrives. the invoice
payload comes back unchanged, so use it as your internal order key.
bot.on("message", async (ctx) => {
const payment = ctx.message?.successful_payment;
if (!payment) return;
await grantAccess(payment.invoice_payload, ctx.from!.id);
await ctx.reply("payment received");
});production rules
- never trust only the client-side button press; provision after
successful_payment. - make invoice payloads unique enough to map back to your order.
- answer
pre_checkout_queryfast; do not run slow external workflows there. - store processed payment ids to avoid double provisioning after retries.
- test the flow with @yaebal/test before using a real provider.