payments
puregram covers telegram's payment flow end to end: LabeledPrice and ShippingOption for building the price breakdown and shipping choices, sendInvoice / createInvoiceLink for issuing invoices, and the shipping_query → pre_checkout_query → successful_payment handler sequence
import { LabeledPrice } from 'puregram'
await tg.api.sendInvoice({
chat_id,
title: 'premium subscription',
description: 'one month of access',
payload: 'sub_1m',
currency: 'USD',
prices: [LabeledPrice.of('subscription', 499)]
})LabeledPrice
LabeledPrice builds a single TelegramLabeledPrice — a labeled portion of the total price. amounts are in the currency's smallest unit (cents for USD, etc.)
import { LabeledPrice } from 'puregram'
// $4.99 product + $1.00 tax
const prices = [
LabeledPrice.of('item', 499),
LabeledPrice.of('tax', 100)
]factory
LabeledPrice.of(label, amount) — label is the human-visible description, amount is an integer in the currency's smallest unit
// telegram stars invoice — one item, currency "XTR", no provider token needed
await tg.api.sendInvoice({
chat_id,
title: 'extra lives',
description: '5 extra lives in the game',
payload: 'lives_5',
currency: 'XTR',
prices: [LabeledPrice.of('5 lives', 50)] // 50 stars
})telegram stars
for in-app star payments pass currency: 'XTR' and omit provider_token. the prices array must contain exactly one item. tips and shipping are not supported for star payments
ShippingOption
ShippingOption builds a TelegramShippingOption — an id, title, and price breakdown — passed to answerShippingQuery when the user requests shipping options
import { LabeledPrice, ShippingOption } from 'puregram'
const options = [
ShippingOption.of('std', 'standard (5–7 days)', [LabeledPrice.of('shipping', 500)]),
ShippingOption.of('exp', 'express (1–2 days)', [LabeledPrice.of('shipping', 1500)])
]factory
ShippingOption.of(id, title, prices) — id is your internal identifier, title is shown to the user, prices is an array of LabeledPrice portions
the payment flow
telegram's payment flow has four stages. all four update kinds are first-class in puregram:
sendInvoice / createInvoiceLink
↓
user fills address → shipping_query (if is_flexible)
↓
user confirms → pre_checkout_query (always)
↓
payment succeeds → successful_payment (service event on the message)sending an invoice
tg.api.sendInvoice sends a payment invoice directly into a chat. tg.api.createInvoiceLink produces a shareable link instead
await tg.api.sendInvoice({
chat_id,
title: 'premium subscription',
description: 'one month of access',
payload: 'sub_1m', // your internal reference — comes back in queries
currency: 'USD',
prices: [
LabeledPrice.of('subscription', 499),
LabeledPrice.of('service fee', 50)
],
provider_token: process.env.PAYMENT_TOKEN,
need_name: true,
need_email: true,
is_flexible: true // set to true when shipping is required
})key sendInvoice fields:
| field | notes |
|---|---|
payload | 1–128 bytes, not shown to the user — echoed back in all three query updates |
currency | ISO 4217 or 'XTR' for telegram stars |
prices | array of LabeledPrice.of(...) |
provider_token | omit or pass '' for star payments |
is_flexible | true triggers shipping_query before checkout |
need_name / need_email / need_phone_number / need_shipping_address | request user info |
handling shipping_query
when is_flexible: true, telegram sends a shipping_query update before the user can proceed. respond via update.answer(...) or tg.api.answerShippingQuery:
tg.on('shipping_query', async (query) => {
if (query.invoicePayload !== 'sub_1m') {
return query.answer({ ok: false, error_message: 'unknown product' })
}
await query.answer({
ok: true,
shipping_options: [
ShippingOption.of('std', 'standard', [LabeledPrice.of('shipping', 0)]),
ShippingOption.of('exp', 'express', [LabeledPrice.of('shipping', 500)])
]
})
})query.answer(params) is a shortcut for tg.api.answerShippingQuery — it fills shipping_query_id from the update automatically
handling pre_checkout_query
after the user confirms their order, telegram sends a pre_checkout_query. you must respond within 10 seconds — either confirm or abort:
tg.on('pre_checkout_query', async (query) => {
// validate stock, pricing, anything that can still fail
const valid = await validateOrder(query.invoicePayload)
if (!valid) {
return query.answer({ ok: false, error_message: 'item no longer available' })
}
await query.answer({ ok: true })
})query.answer(params) shortcut fills pre_checkout_query_id automatically
PreCheckoutQueryUpdate exposes:
query.id— unique query id (filled by the shortcut)query.from— the userquery.currency— ISO 4217 /'XTR'query.totalAmount— total in smallest currency unitsquery.invoicePayload— your payload string fromsendInvoicequery.shippingOptionId— chosen shipping option id, if anyquery.orderInfo— user-provided order info, if any
successful payment
when the payment clears, telegram delivers a successful_payment service event (a MessageUpdate with successful_payment set). handle it to provision what was purchased:
tg.on('successful_payment', (message) => {
const payment = message.raw.successful_payment
// payment.invoice_payload, payment.currency, payment.total_amount, etc.
console.log('paid', payment?.total_amount, payment?.currency)
})payload-scoped filters
shippingPayload, preCheckoutPayload, and successfulPaymentPayload filters let you register handlers for a specific invoice_payload value without a manual equality check:
import { shippingPayload, preCheckoutPayload } from 'puregram'
tg.on(shippingPayload('sub_1m'), async (query) => { /* ... */ })
tg.on(preCheckoutPayload('sub_1m'), async (query) => { /* ... */ })import these from 'puregram' — they live in packages/puregram/src/filters/payments.ts
paid media
sendPaidMedia lets you gate photos and videos behind a star payment. the user pays star_count stars to unlock the media
await tg.api.sendPaidMedia({
chat_id,
star_count: 10,
media: [
{ type: 'photo', media: 'attach://photo.jpg' }
],
caption: 'exclusive content'
})when a user buys access, telegram fires a purchased_paid_media update with update.from (the buyer) and update.paidMediaPayload (the optional bot-defined payload you passed to sendPaidMedia)
tg.on('purchased_paid_media', (update) => {
console.log(update.from.id, 'purchased', update.paidMediaPayload)
})see also
- messages & media — sending files and media
- dispatch & filters — registering
shipping_query/pre_checkout_queryhandlers - service events —
successful_paymentas a service event - methods — full generated method list