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 |