Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Games & Payments

Bot API for HTML5 games (scores, high scores) and Telegram Payments (shipping and pre-checkout confirmations).


Bots

async client.start_bot(bot_user_id: i64, peer: impl Into<PeerRef>, start_param: impl Into<String>) → Result<(), InvocationError>
Send a /start command to a bot with a deep-link parameter, as if the user clicked a t.me/BotUsername?start=param link. The bot receives the parameter in Message.text as /start param.

Parameters

ParamTypeDescription
bot_user_idi64The user ID of the bot to start. Must match a bot account.
peerimpl Into<PeerRef>The chat or user where the /start message is sent. Usually the same as the bot’s own private chat (pass the bot’s user ID).
start_paramimpl Into<String>The deep-link payload. Max 64 characters, only A-Z a-z 0-9 _ - are permitted by Telegram.

Examples

#![allow(unused)]
fn main() {
// Basic: open a bot in a private chat with a deep-link parameter
// bot_user_id is the numeric ID of the bot (not the username)
client.start_bot(bot_user_id, bot_user_id, "welcome").await?;
}
#![allow(unused)]
fn main() {
// Affiliate / referral tracking: encode a referrer ID in the param
let ref_param = format!("ref_{}", referrer_user_id);
client.start_bot(bot_user_id, bot_user_id, ref_param).await?;
}
#![allow(unused)]
fn main() {
// Launch a specific game flow
client.start_bot(bot_user_id, bot_user_id, "play").await?;
}
#![allow(unused)]
fn main() {
// Open the bot inside a group chat (bot must be a member)
client.start_bot(bot_user_id, group_peer, "group_hello").await?;
}

Getting bot_user_id

bot_user_id is the numeric user ID of the bot, not its username string. You can resolve it from the username once and cache it:

#![allow(unused)]
fn main() {
use ferogram::PeerRef;

let bot = client.resolve("MyBotUsername").await?;
let bot_user_id: i64 = match bot {
    PeerRef::UserId(id) => id,
    other => panic!("expected a user, got {:?}", other),
};
}

Or if you already have the bot’s user object from an earlier API call:

#![allow(unused)]
fn main() {
let bot_user_id = user.id();
}

What the bot receives

The bot’s update handler sees a Message whose text is exactly /start <start_param>. For example, if you pass "ref_42":

/start ref_42

The bot can extract the param from message.text.strip_prefix("/start ").

Notes

  • start_param is limited to 64 characters. Attempting a longer value will be rejected by the Telegram server with START_PARAM_INVALID.
  • Allowed characters: A-Z, a-z, 0-9, _, -. Spaces and special characters are not permitted.
  • The call is idempotent from the server’s perspective. Sending the same /start param twice triggers two separate updates on the bot side.
  • For bots with allow_zero_hash disabled (the default), the bot’s access_hash must already be in the peer cache. Call client.resolve() at least once before start_bot to populate the cache, or enable ExperimentalFeatures::allow_zero_hash.

Games

async client.set_game_score(peer: impl Into<PeerRef>, msg_id: i32, user_id: i64, score: i32, force: bool, edit_message: bool) → Result<(), InvocationError>
Set a user's score in a game that was sent in a chat message.
ParamDescription
peerChat where the game message lives
msg_idID of the message containing the game
user_idUser whose score to update
scoreNew score value
forceIf true, allow setting a score lower than the current one
edit_messageIf true, the game message is edited to show the new score
#![allow(unused)]
fn main() {
client.set_game_score(peer.clone(), msg_id, user_id, 42_000, false, true).await?;
}
async client.get_game_high_scores(peer: impl Into<PeerRef>, msg_id: i32, user_id: i64) → Result<Vec<tl::types::HighScore>, InvocationError>
Retrieve the high-score table for a game message, anchored around user_id. Returns up to 5 entries centred on the specified user's position.
#![allow(unused)]
fn main() {
let scores = client.get_game_high_scores(peer.clone(), msg_id, user_id).await?;
for s in &scores {
    println!("#{}  -  user {}  -  {} pts", s.pos, s.user_id, s.score);
}
}

HighScore fields

FieldTypeDescription
posi32Rank position (1-based)
user_idi64User ID
scorei32Score value

Payments

async client.answer_shipping_query(query_id: i64, error: Option<String>, shipping_options: Option<Vec<tl::enums::ShippingOption>>) → Result<(), InvocationError>
Respond to a ShippingQuery update that a bot receives when a user provides their shipping address for an invoice with is_flexible = true.
  • Pass error = None and shipping_options = Some(...) to confirm available shipping options.
  • Pass error = Some("message") and shipping_options = None to reject the query with an error shown to the user.
#![allow(unused)]
fn main() {
// Accept with two shipping options
client.answer_shipping_query(
    query.query_id,
    None,
    Some(vec![
        tl::enums::ShippingOption::ShippingOption(tl::types::ShippingOption {
            id: "standard".into(),
            title: "Standard (5-7 days)".into(),
            prices: vec![
                tl::enums::LabeledPrice::LabeledPrice(tl::types::LabeledPrice {
                    label: "Shipping".into(),
                    amount: 500,  // in smallest currency unit
                }),
            ],
        }),
    ]),
).await?;

// Reject (address not serviceable)
client.answer_shipping_query(
    query.query_id,
    Some("We don't ship to this address.".into()),
    None,
).await?;
}
async client.answer_precheckout_query(query_id: i64, ok: bool, error_message: Option<String>) → Result<(), InvocationError>
Confirm or reject a pre-checkout query. You receive this just before Telegram finalises a payment - use it to verify stock availability, validate the order, etc.
  • ok: true - approve the payment; Telegram completes it.
  • ok: false - reject; pass error_message to explain why.

You must answer within 10 seconds or the payment times out.

#![allow(unused)]
fn main() {
// Approve
client.answer_precheckout_query(query.query_id, true, None).await?;

// Reject
client.answer_precheckout_query(
    query.query_id,
    false,
    Some("Item is out of stock.".into()),
).await?;
}

Update variants

Payments generate update variants that you handle in your update loop:

#![allow(unused)]
fn main() {
Update::ShippingQuery(q) => {
    // q.query_id, q.user_id, q.payload, q.shipping_address
    client.answer_shipping_query(q.query_id, None, Some(options)).await?;
}

Update::PreCheckoutQuery(q) => {
    // q.query_id, q.user_id, q.currency, q.total_amount, q.payload
    client.answer_precheckout_query(q.query_id, true, None).await?;
}
}