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