Skip to content

Telegram Bot integration ​

Who this is for ​

Sellers who run their business inside Telegram and use a bot to take orders. The buyer places an order in the chat, the bot shows payment details, and delivery happens automatically once payment is confirmed.

Before you start ​

  • You have an API KEY (see Get started) and a collection address configured.
  • A bot: create one with BotFather and note the Bot Token.
  • A publicly reachable HTTPS endpoint for the webhook, receiving both Telegram updates and platform callbacks.
  • A server to hold the API KEY (your bot logic usually already runs on one).

The KEY stays on the server

Your bot backend is a server, so the API KEY belongs there. But do not hand it to any client (Mini App, Web App, desktop client).

Install ​

Calling the Telegram Bot API needs no SDK — a plain HTTPS request will do:

bash
# set the webhook
curl "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \
  -d "url=https://your-bot.com/tg/webhook" -d "secret_token=$TG_WEBHOOK_SECRET"

Minimal working example ​

The recommended path is show payment details in the bot + pay on-chain: the platform settles, the bot communicates and delivers.

js
// Node/Express: Telegram webhook + Web3SDK order creation
import express from 'express'
const app = express()
app.use(express.json())

const TG = `https://api.telegram.org/bot${process.env.BOT_TOKEN}`

async function tg(method, body) {
  return fetch(`${TG}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  }).then((r) => r.json())
}

// 1) The buyer taps "buy" in the bot
app.post('/tg/webhook', async (req, res) => {
  res.sendStatus(200)   // answer Telegram first, before it re-delivers
  const msg = req.body.message
  if (!msg || !msg.text?.startsWith('/buy')) return

  const chatId = msg.chat.id
  const orderNo = `TG-${msg.from.id}-${Date.now()}`

  // 2) Your server creates the platform order (with the API KEY — never in a client)
  const r = await fetch(`${process.env.API_BASE}/api/pay/open/createOrder`, {
    method: 'POST',
    headers: { 'X-Pay-Api-Key': process.env.PAY_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      merchantOrderNo: orderNo,     // idempotency key
      chainId: 2,
      tokenId: 1,
      amount: '29.9',               // decimal string in token units
      subject: `Telegram order ${orderNo}`,
      notifyUrl: 'https://your-bot.com/pay/notify',
    }),
  }).then((x) => x.json())

  if (r.code !== 0) return tg('sendMessage', { chat_id: chatId, text: `Order failed: ${r.msg}` })
  const o = r.data.order

  // 3) Send the payment details (use the display value of payAmountMinimal)
  await tg('sendMessage', {
    chat_id: chatId,
    parse_mode: 'HTML',
    text:
      `Order <code>${orderNo}</code>\n` +
      `Amount due: <b>${o.payAmountDisplay}</b> USDT (${o.chainKey})\n` +
      `Address: <code>${o.payAddress}</code>\n\n` +
      `Transfer exactly this amount, then tap the button below.`,
    reply_markup: {
      inline_keyboard: [[
        { text: 'I have paid', callback_data: `paid:${o.orderNo}` },
        { text: 'Refresh status', callback_data: `check:${o.orderNo}` },
      ]],
    },
  })
})

// 4) Handle button callbacks (Telegram callback queries)
app.post('/tg/callback', async (req, res) => { /* the same webhook works too */ })

Answer callback queries with answerCallbackQuery:

js
async function onCallbackQuery(q) {
  const [action, orderNo] = (q.data || '').split(':')
  if (action === 'check' || action === 'paid') {
    // Verify the real state with the query API (never trust the buyer's word alone)
    const r = await fetch(
      `${process.env.API_BASE}/api/pay/open/queryOrder?orderNo=${orderNo}`,
      { headers: { 'X-Pay-Api-Key': process.env.PAY_API_KEY } },
    ).then((x) => x.json())

    const status = r.data?.status
    const text =
      status === 3 || status === 4 ? 'Payment confirmed. Preparing your order…' :
      status === 2 ? 'Transaction seen on-chain, waiting for confirmations…' :
      'We have not received this payment yet — please try again shortly.'

    await tg('answerCallbackQuery', { callback_query_id: q.id, text, show_alert: false })
  }
}

If you want to use the Telegram Invoice flow ​

The Bot API offers sendInvoice / pre_checkout_query / successful_payment, which fits the case where you have already connected a Telegram payment provider:

js
// Send an invoice (the amount unit depends on currency)
await tg('sendInvoice', {
  chat_id: chatId,
  title: `Order ${orderNo}`,
  description: 'USDT payment',
  payload: orderNo,                      // your merchant order number; comes back in the receipt
  provider_token: process.env.TG_PROVIDER_TOKEN,   // from the provider you connected
  currency: 'USD',
  prices: [{ label: 'Total', amount: 2990 }],      // integer in the smallest unit
})

// You must answer before settlement (within 10s), or the buyer sees a failure
async function onPreCheckoutQuery(q) {
  await tg('answerPreCheckoutQuery', { pre_checkout_query_id: q.id, ok: true })
}

// Settlement succeeded
async function onSuccessfulPayment(m) {
  const orderNo = m.successful_payment.invoice_payload
  await markPaid(orderNo, m.successful_payment.provider_payment_charge_id)
}

Do not mix the two paths

sendInvoice settles through a Telegram payment provider, not on-chain; Web3SDK collects on-chain, and delivery is driven by the platform callback. If you want to collect USDT, use the "payment details + on-chain payment" path above; sendInvoice only makes sense if you have also connected Telegram Payments. Pick one per order — never both.

Callback handling ​

The platform's payment callback lands on the same bot backend:

js
import crypto from 'node:crypto'

function verify(rawBody, ts, sig) {
  const want = crypto
    .createHmac('sha256', process.env.PAY_API_KEY)
    .update(ts).update(rawBody)          // timestamp + raw body
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(sig))
}

app.post('/pay/notify', express.raw({ type: '*/*' }), async (req, res) => {
  const raw = req.body.toString('utf8')
  if (!verify(raw, req.get('X-Pay-Timestamp'), req.get('X-Pay-Signature'))) {
    return res.sendStatus(401)
  }
  const p = JSON.parse(raw)
  if (isProcessed(p.merchantOrderNo)) return res.sendStatus(200)   // idempotent
  await markPaid(p.merchantOrderNo, p.txHash)
  await deliverGoods(p.merchantOrderNo)     // deliver
  res.sendStatus(200)                        // 2xx stops the retries
})
  • Idempotent: callbacks retry; deduplicate on merchantOrderNo so you never deliver twice.
  • Return fast: about a 10-second timeout; queue delivery or persist first and deliver asynchronously.
  • Query is authoritative: when a buyer says "I paid", verify with Query order rather than trusting the chat.

FAQ ​

QuestionAnswer
Can the bot hold the KEY?Yes — the bot backend is a server. But never hand it to a client
How do I stop forged "I paid" claims?The button only triggers a query; truth comes from the platform query API or a verified callback
The webhook receives no updatesCheck the setWebhook URL, the secret_token check, and whether the server is publicly reachable
Duplicate callbacksDeduplicate on merchantOrderNo and return 2xx to stop retries
Can the buyer just post an address in the chat?Not recommended. Use a bot DM plus a platform order so the address and amount come from the platform

Web3 payment infrastructure for global merchants