Skip to content

Telegram Mini App 接入 ​

这套接入适合谁 ​

你已经有一个 Telegram Mini App(Web App),想让用户在 App 内直接完成付款, 不用跳出到外部浏览器。

前置准备 ​

  • 已拿到 API KEY(见立即接入),并配置好收款地址。
  • Mini App 已配置好(BotFather 里填写 Web App URL)。
  • 你有一个服务端来放 API KEY 并接收回调。Mini App 是浏览器环境,绝不能持有 KEY。

Mini App 是前端,KEY 不能进去

Mini App 里跑的是网页代码,用户可以直接查看源码与网络请求。 下单必须由你的服务端执行,Mini App 只调你自己的后端。

安装 ​

在 Mini App 的页面里引入 Telegram WebApp SDK:

html
<script src="https://telegram.org/js/telegram-web-app.js"></script>

最小可跑示例 ​

1. 你的服务端下单 ​

js
// 与其它渠道一样:带 KEY 调开放接口
app.post('/api/miniapp/checkout', async (req, res) => {
  const { orderNo, amount } = req.body
  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,
      chainId: 2,
      tokenId: 1,
      amount: String(amount),      // 代币单位小数字符串
      subject: `MiniApp 订单 ${orderNo}`,
      notifyUrl: 'https://your-app.com/pay/notify',
    }),
  }).then((x) => x.json())

  if (r.code !== 0) return res.status(400).json(r)
  const o = r.data.order
  res.json({
    code: 0,
    orderNo: o.orderNo,
    payAddress: o.payAddress,
    payAmountDisplay: o.payAmountDisplay,
    payAmountMinimal: o.payAmountMinimal,
    chainKey: o.chainKey,
    identify: o.identify,
    cashierUrl: `${req.body.cashierBaseUrl}/pay/cashier/m/${o.merchantNo}`,   // 路径段,不是查询串
  })
})

2. Mini App 内展示 ​

js
const tg = window.Telegram.WebApp
tg.ready()
tg.expand()

export async function startCheckout(orderNo, amount) {
  const o = await fetch('/api/miniapp/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ orderNo, amount }),
  }).then((r) => r.json())

  if (o.code !== 0) {
    tg.showAlert('下单失败')
    return
  }

  // 方式 A:直接在 Mini App 里渲染收款信息(推荐,链上结算)
  tg.MainButton.setText(`应付 ${o.payAmountDisplay} USDT`)
  tg.MainButton.show()

  // 用 Telegram 的 Haptic 反馈一下,体验更好
  tg.HapticFeedback?.impactOccurred('light')

  // 轮询状态;后端再去查平台的查询接口
  const timer = setInterval(async () => {
    const s = await fetch(`/api/miniapp/status?orderNo=${o.orderNo}`).then((r) => r.json())
    if (s.status === 3 || s.status === 4) {
      clearInterval(timer)
      tg.MainButton.hide()
      tg.showPopup({ title: '支付成功', message: '正在为你发货' })
    }
  }, 3000)

  return o
}

也可以让用户点一下直接跳到平台收银台(用路径段带商户号):

js
tg.openLink(o.cashierUrl)   // {cashierBaseUrl}/pay/cashier/m/{merchantNo}

商户号是路径段,不是查询串

收银台地址的形状是 {cashierBaseUrl}/pay/cashier/m/{merchantNo}。 写成 ?m=... 会被部分钱包的深链按 = 截断,导致打不开。

3. 用 Telegram 的 Invoice(openInvoice) ​

如果你的 Bot 已经接了 Telegram 支付提供商,可以在 Mini App 里用 openInvoice 打开一张发票:

js
// 由你的服务端调用 Bot API 的 createInvoiceLink 生成发票链接
const { result: invoiceLink } = await fetch(
  `https://api.telegram.org/bot${process.env.BOT_TOKEN}/createInvoiceLink`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      title: `订单 ${orderNo}`,
      description: '商品',
      payload: orderNo,                              // 回执里会带回来
      provider_token: process.env.TG_PROVIDER_TOKEN, // 你接的支付提供商
      currency: 'USD',
      prices: [{ label: 'Total', amount: 2990 }],
    }),
  },
).then((r) => r.json())

// Mini App 里打开
tg.openInvoice(invoiceLink, (status) => {
  // status: 'paid' | 'cancelled' | 'failed' | 'pending'
  if (status === 'paid') {
    // 不要只凭这个就发货 —— 以平台的查询接口或回调为准
    tg.showPopup({ title: '已支付', message: '正在为你发货' })
  }
})

两条路别混

openInvoice 的结算走 Telegram 支付提供商; 而 Web3SDK 收的是链上稳定币,发货依据是平台的回调。 要收 USDT 就用方式 A(Mini App 内展示收款信息 / 打开平台收银台), openInvoice 只在你也接了 Telegram Payments 时才有意义。

回调处理 ​

回调由你的服务端接收,与 Mini App 无关:

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).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(sig))
}

app.post('/pay/notify', express.raw({ type: '*/*' }), (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)   // 幂等
  markPaid(p.merchantOrderNo, p.txHash)
  res.sendStatus(200)                                               // 2xx 停止重试
})
  • 幂等:回调会重试,用 merchantOrderNo 去重。
  • 快返回:约 10 秒超时。
  • 以查询为准:openInvoice 回调的 paid、以及买家的口头说法,都不足以发货; 以查询订单或平台回调为准。

常见问题 ​

问题说明
Mini App 怎么确认用户身份用 tg.initData 在服务端校验(Telegram 官方校验方式),不要只信前端传的 user.id
KEY 能放 Mini App 吗不能,页面源码可见
为什么发货要等回调钱包里按下确认不等于链上确认,看到"已确认"才安全
收银台链接打不开确认是 /pay/cashier/m/{merchantNo} 路径段,且 cashierBaseUrl 来自 getMyProfile
用哪个方式收 USDT方式 A(Mini App 内展示 / 打开平台收银台),不要用 openInvoice

相关 ​

面向全球中小商家的 Web3 支付基础设施