Skip to content

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

ts
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.)

ts
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

ts
// 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

ts
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

ts
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:

fieldnotes
payload1–128 bytes, not shown to the user — echoed back in all three query updates
currencyISO 4217 or 'XTR' for telegram stars
pricesarray of LabeledPrice.of(...)
provider_tokenomit or pass '' for star payments
is_flexibletrue triggers shipping_query before checkout
need_name / need_email / need_phone_number / need_shipping_addressrequest 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:

ts
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:

ts
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 user
  • query.currency — ISO 4217 / 'XTR'
  • query.totalAmount — total in smallest currency units
  • query.invoicePayload — your payload string from sendInvoice
  • query.shippingOptionId — chosen shipping option id, if any
  • query.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:

ts
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:

ts
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

sendPaidMedia lets you gate photos and videos behind a star payment. the user pays star_count stars to unlock the media

ts
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)

ts
tg.on('purchased_paid_media', (update) => {
  console.log(update.from.id, 'purchased', update.paidMediaPayload)
})

see also ​