telega/payments

Helpers for sending invoices and answering payment queries, on top of the raw Bot API methods in telega/api.

Telegram Stars

Telegram Stars (XTR) are the first-class case: digital goods and services must be sold in Stars, no payment provider is required, and the invoice has a single price.

import telega/payments

fn buy_handler(ctx, _command) {
  let assert Ok(_) =
    payments.stars_invoice(
      title: "Premium",
      description: "Premium access for a month",
      payload: "premium:1m",
      amount: 100,
    )
    |> payments.send(ctx)
  Ok(ctx)
}

payload is not shown to the user — it comes back in the pre-checkout query and in the successful payment message, so put your order identifier there.

Regular currencies

For physical goods, pass an ISO 4217 currency, a provider token from @BotFather, and a price breakdown in the smallest units of the currency:

payments.invoice(
  title: "Order #42",
  description: "2 pizzas",
  payload: "order:42",
  currency: "USD",
  provider_token: provider_token,
  prices: [
    payments.price("2x Pepperoni", 2490),
    payments.price("Delivery", 500),
  ],
)
|> payments.with_photo("https://example.com/pizza.jpg")
|> payments.require_email()
|> payments.send(ctx)

Other builder options: with_tips, with_flexible_shipping, with_provider_data, with_start_parameter, with_reply_markup, require_name, require_phone_number. create_link builds a shareable payment URL instead of sending a message.

Answering the pre-checkout query

After the user confirms the payment, Telegram sends a pre-checkout query that must be answered within 10 seconds, otherwise the payment fails:

import telega/router

router.new("shop")
|> router.on_pre_checkout_query(fn(ctx, query) {
  let assert Ok(_) = case in_stock(query.invoice_payload) {
    True -> payments.answer_pre_checkout_ok(ctx, query)
    False -> payments.answer_pre_checkout_error(ctx, query, "Out of stock")
  }
  Ok(ctx)
})

Shipping queries

For invoices created with with_flexible_shipping, Telegram asks the bot for shipping options once the user fills in an address:

router.on_shipping_query(router, fn(ctx, query) {
  let assert Ok(_) = case ships_to(query.shipping_address) {
    True ->
      payments.answer_shipping_ok(ctx, query, [
        payments.shipping_option(id: "dhl", title: "DHL", prices: [
          payments.price("Shipping", 500),
        ]),
      ])
    False -> payments.answer_shipping_error(ctx, query, "No delivery there")
  }
  Ok(ctx)
})

Waiting for the payment in a conversation

wait_successful_payment pauses the handler until the successful payment service message arrives, so the whole purchase reads top-to-bottom (see the conversation guide):

fn buy_handler(ctx, _command) {
  let assert Ok(_) =
    payments.stars_invoice(
      title: "Premium",
      description: "Premium access for a month",
      payload: "premium:1m",
      amount: 100,
    )
    |> payments.send(ctx)

  use ctx, payment <- payments.wait_successful_payment(
    ctx,
    or: None,
    timeout: Some(600_000),
  )

  // Store payment.telegram_payment_charge_id — it is needed for refunds
  reply.with_text(ctx, "Thanks! Your order: " <> payment.invoice_payload)
}

Refunds

For Telegram Stars, refund through the raw API method with the charge id from the successful payment:

import telega/api
import telega/model/types

api.refund_star_payment(
  ctx.config.api_client,
  parameters: types.RefundStarPaymentParameters(
    user_id: user_id,
    telegram_payment_charge_id: charge_id,
  ),
)

Types

Invoice under construction — build with stars_invoice or invoice, refine with the with_*/require_* functions, then send or create_link.

pub opaque type Invoice

Values

pub fn answer_pre_checkout_error(
  ctx ctx: bot.Context(session, error, dependencies),
  query query: types.PreCheckoutQuery,
  message message: String,
) -> Result(Bool, error.TelegaError)

Reject a pre-checkout query with a human-readable reason shown to the user.

pub fn answer_pre_checkout_ok(
  ctx ctx: bot.Context(session, error, dependencies),
  query query: types.PreCheckoutQuery,
) -> Result(Bool, error.TelegaError)

Confirm a pre-checkout query: the order can proceed. Must be sent within 10 seconds after the query arrives.

pub fn answer_shipping_error(
  ctx ctx: bot.Context(session, error, dependencies),
  query query: types.ShippingQuery,
  message message: String,
) -> Result(Bool, error.TelegaError)

Reject a shipping query with a human-readable reason shown to the user.

pub fn answer_shipping_ok(
  ctx ctx: bot.Context(session, error, dependencies),
  query query: types.ShippingQuery,
  options options: List(types.ShippingOption),
) -> Result(Bool, error.TelegaError)

Confirm delivery to the queried address with the available options.

pub fn create_link(
  invoice invoice: Invoice,
  ctx ctx: bot.Context(session, error, dependencies),
) -> Result(String, error.TelegaError)

Create a shareable payment link for the invoice.

pub fn invoice(
  title title: String,
  description description: String,
  payload payload: String,
  currency currency: String,
  provider_token provider_token: String,
  prices prices: List(types.LabeledPrice),
) -> Invoice

Invoice in a regular currency through a payment provider.

pub fn price(
  label label: String,
  amount amount: Int,
) -> types.LabeledPrice

A labeled portion of the invoice price. amount is in the smallest units of the currency (cents for USD, stars for XTR).

pub fn require_email(invoice invoice: Invoice) -> Invoice

Require the user’s email to complete the order.

pub fn require_name(invoice invoice: Invoice) -> Invoice

Require the user’s full name to complete the order.

pub fn require_phone_number(invoice invoice: Invoice) -> Invoice

Require the user’s phone number to complete the order.

pub fn send(
  invoice invoice: Invoice,
  ctx ctx: bot.Context(session, error, dependencies),
) -> Result(types.Message, error.TelegaError)

Send the invoice to the current chat.

pub fn shipping_option(
  id id: String,
  title title: String,
  prices prices: List(types.LabeledPrice),
) -> types.ShippingOption

A shipping option offered in response to a shipping query.

pub const stars_currency: String

Telegram Stars currency code.

pub fn stars_invoice(
  title title: String,
  description description: String,
  payload payload: String,
  amount amount: Int,
) -> Invoice

Invoice in Telegram Stars: no provider token, single price portion.

pub fn wait_successful_payment(
  ctx ctx: bot.Context(session, error, dependencies),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    types.SuccessfulPayment,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for a successful payment service message. Other (non-payment) messages keep the conversation waiting, or go to the or handler if one is given.

let assert Ok(_) = payments.stars_invoice(...) |> payments.send(ctx)
use ctx, payment <- payments.wait_successful_payment(ctx, or: None, timeout: None)
reply.with_text(ctx, "Thanks! Charge id: " <> payment.telegram_payment_charge_id)

See conversation

pub fn with_flexible_shipping(
  invoice invoice: Invoice,
) -> Invoice

Request a shipping address and make the final price depend on the chosen shipping option — the bot will receive shipping queries, handle them with router.on_shipping_query and answer_shipping_ok/answer_shipping_error.

pub fn with_photo(
  invoice invoice: Invoice,
  url url: String,
) -> Invoice

Product photo shown in the invoice.

pub fn with_provider_data(
  invoice invoice: Invoice,
  data data: String,
) -> Invoice

JSON-serialized data for the payment provider.

pub fn with_reply_markup(
  invoice invoice: Invoice,
  markup markup: types.InlineKeyboardMarkup,
) -> Invoice

Inline keyboard for the invoice message. The first button must be a Pay button, otherwise Telegram inserts one.

pub fn with_start_parameter(
  invoice invoice: Invoice,
  parameter parameter: String,
) -> Invoice

Deep-linking parameter to recreate the invoice via a /start link.

pub fn with_tips(
  invoice invoice: Invoice,
  max max: Int,
  suggested suggested: List(Int),
) -> Invoice

Allow tips up to max with suggested amounts (smallest currency units). Not supported for Telegram Stars.

Search Document