Bot Payments API & Telegram Stars

Bot Payments API Bot Payments Stars

Telegram offers a safe, simple and unified payment system for goods and services.

Due to Google/Apple policies, there is a distinction between:

  • Digital Goods & Services, which can be paid using Telegram Stars (XTR) only
  • Physical Goods, which can be paid using real money, and can request more details like a shipping address.

Both process are similar, so we will demonstrate how to do a Telegram Stars payment (simpler) and give you some info about the difference for Physical Goods.

Important notes for physical goods

Before starting, you need to talk to @BotFather, select one of the supported Payment providers (you need to open an account on the provider website), and complete the connection procedure linking your bot with your provider account.

It is recommended to start with the Stripe TEST MODE provider so you can test your bot with fake card numbers before going live.

Price amounts are expressed as integers with some digits at the end for the "decimal" part.
For example, in USD currency there are 2 digits for cents, so 12345 means $123.45 ; With Telegram Stars (XTR), there are no extra digits.
See "exp" in this table, to determine the number of decimal digits for each currency.

Sending an invoice

send invoice method

When your bot is ready to issue a payment for the user to complete, it will send an invoice:

await bot.SendInvoiceAsync(
    chatId: chatId,                         // same as userId for private chat
    title: "Product Title",
    description: "Product Detailed Description",
    payload: "InternalProductID",           // not sent nor shown to user
    providerToken: ""                       // empty string for XTR
    currency: "XTR",                        // 3-letters ISO 4217 currency
    prices: [("Price", 500)],               // only one price for XTR
    photoUrl: "https://cdn.pixabay.com/photo/2012/10/26/03/16/painting-63186_1280.jpg",
);

Alternatively, you can instead generate an URL for that payment with CreateInvoiceLinkAsync.

With Physical Goods, you can specify more parameters like:

  • the providerToken obtained by BotFather (something like "1234567:TEST:aBcDeFgHi")
  • several price lines detailing the total price
  • some suggested tips
  • the need for extra information about the user, including a shipping address
  • if the price is flexible depending on the shipping address/method

If your bot supports Inline Mode, you can also send invoices as inline results ("via YourBot").

Handling the ShippingQuery Update

Shipping Query Update Answer Shipping Query method

This update is received only for Physical Goods, if you specified a flexible price. Otherwise you can skip to the next section.

update.ShippingQuery would contain information like the current shipping address for the user, and can be received again if the user changes the address.

You should check if the address is supported, and reply using bot.AnswerShippingQueryAsync with an error message or a list of shipping options with associated price lines:

var shippingOptions = new List<ShippingOption>();
shippingOptions.Add(new() { Title = "DHL Express", Id = "dhl-express",
    Prices = [("Shipping", 1200)] });
shippingOptions.Add(new() { Title = "FedEx Fragile", Id = "fedex-fragile",
    Prices = [("Packaging", 500), ("Shipping", 1800)] });
await bot.AnswerShippingQueryAsync(shippingQuery.Id, shippingOptions);

Handling the PreCheckoutQuery Update

Pre Checkout Query Update Answer Pre Checkout Query method

This update is received when the user has entered their payment information and confirmed the final Pay button.

update.PreCheckoutQuery contains all the requested information for the order, so you can validate that all is fine before actual payment

You must reply within 10 seconds with:

if (confirm)
    await bot.AnswerPreCheckoutQueryAsync(preCheckoutQuery.Id); // success
else
    await bot.AnswerPreCheckoutQueryAsync(preCheckoutQuery.Id, "Can't process your order: <REASON>");

Handling the SuccessfulPayment Message

Successful Payment Message

If you confirmed the order in the step above, Telegram requests the payment with the payment provider.

If the payment is successfully processed, you will receive a private Message of type SuccessfulPayment from the user, and you must then proceed with delivering the goods or services to the user.

The message.SuccessfulPayment structure contains all the same previous information, plus two payment identifiers from Telegram & from the Payment Provider.

You should store these ChargeId strings for traceability of the transaction in case of dispute, or refund (possible with RefundStarPayment).

Full example code for Telegram Stars transaction

using Telegram.Bot;
using Telegram.Bot.Types;

var bot = new TelegramBotClient("YOUR_BOT_TOKEN");
bot.OnUpdate += OnUpdate;
Console.ReadKey();

async Task OnUpdate(Update update)
{
   switch (update)
   {
      case { Message.Text: "/start" }:
         await bot.SendInvoiceAsync(update.Message.Chat,
            "Unlock feature X", "Will give you access to feature X of this bot", "unlock_X", "",
            "XTR", [("Price", 200)], photoUrl: "https://cdn-icons-png.flaticon.com/512/891/891386.png");
         break;
      case { PreCheckoutQuery: { } preCheckoutQuery }:
         if (preCheckoutQuery is { InvoicePayload: "unlock_X", Currency: "XTR", TotalAmount: 200 })
            await bot.AnswerPreCheckoutQueryAsync(preCheckoutQuery.Id); // success
         else
            await bot.AnswerPreCheckoutQueryAsync(preCheckoutQuery.Id, "Invalid order");
         break;
      case { Message.SuccessfulPayment: { } successfulPayment }:
         System.IO.File.AppendAllText("payments.log", $"\n{DateTime.Now}: " +
            $"User {update.Message.From} paid for {successfulPayment.InvoicePayload}: " +
            $"{successfulPayment.TelegramPaymentChargeId} {successfulPayment.ProviderPaymentChargeId}");
         if (successfulPayment.InvoicePayload is "unlock_X")
            await bot.SendTextMessageAsync(update.Message.Chat, "Thank you! Feature X is unlocked");
         break;
   };
}