Telegram 小程序接入
用 ADBNK Web SDK 在你的 Telegram 小程序里展示广告。激励广告优先,也支持横幅、原生和插屏。
开始之前
- 你已经有一个 Telegram bot,并为它配置了小程序。
- 小程序页面部署在你自己的 HTTPS 域名上。
- 你可以在小程序页面里添加脚本。
1. 添加 Telegram 站点
在流量主后台打开 站点 → 添加站点,选择 Telegram 小程序。
| 字段 | 填写说明 |
|---|---|
| 小程序网址 | 小程序页面的 https 网址,系统会从中提取域名。 |
| Bot 用户名 | 例如 your_app_bot。平台会自动识别 Bot ID。 |
| 小程序短名 | 选填。如果你的应用以 t.me/<bot>/<短名> 的形式打开,请填写。 |
不要提供 bot token
你不需要提供 Bot ID,也不要提供 bot token。如果有人把看起来像 bot token 的内容粘贴进表单,后台会当场清空。
提交后,后台会显示平台识别到的 Bot ID,请核对是不是你的 bot。Telegram 相关信息建站后不能修改;如需更换 bot,请新建站点。
完成三步站点验证
在站点列表中点击该站点的 验证,依次完成:
- 域名验证:与网站相同,可用 meta 标签、根目录文件或 DNS TXT 记录。
- Bot 简介验证:在 @BotFather 中发送
/setabouttext,选择你的 bot,把后台显示的验证码写进默认语言的简介(不要只写进某个语言版本)。审核通过前请不要删除验证码,通过后可以删除。如果刚改过简介,请稍等几分钟再检测。 - 在 Telegram 里打开一次:先在小程序里接入 SDK(见接入 SDK),然后在 Telegram 里打开后台给出的启动链接:
https://t.me/<bot>?startapp=<验证码>,配置了短名时也可以用https://t.me/<bot>/<短名>?startapp=<验证码>。打开后回到后台点 重新检测。这一步同时可以确认 SDK 已正确接入。
三步全部通过后,由平台人工审核。所有 Telegram 站点都需要人工审核。
2. 创建广告位
Telegram 站点可以使用以下广告位类型:
| 广告位类型 | 接入方式 |
|---|---|
| 激励视频(推荐) | 编程式 |
| 激励插屏(推荐) | 编程式 |
| 移动插屏、桌面插屏 | 编程式 |
| 横幅、移动横幅、吸附横幅 | 标签式 |
| 浮动横幅、移动浮动横幅 | 标签式 |
| 原生信息流 | 标签式 |
站点和广告位都审核通过后,广告位才会展示广告。
3. 接入 SDK
请始终先加载 Telegram 的 telegram-web-app.js,再加载 ADBNK SDK。代码请从后台的 获取代码 复制,下面的示例只说明结构。
标签式(横幅、原生、浮动)
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<ins class="adsbyadbnk" data-zone-id="zone_xxxxxxxx"
style="display:block;width:320px;height:50px"></ins>
<script async src="https://js.adbnk.co/sdk.js"></script>编程式(激励、插屏)
编程式接入请用同步方式加载 sdk.js(不加 async)。sdk.js 执行完毕后 window.adbnk.telegram 立即可用;SDK 没有命令队列,在它之前运行的代码无法调用。如果一定要用 async 加载,请在该 <script> 的 onload 回调里再调用 window.adbnk.telegram.init(...)。
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<script src="https://js.adbnk.co/sdk.js"></script>
<button id="watch-ad">看广告领 10 金币</button>
<script>
const ad = window.adbnk.telegram.init({
zoneId: 'zone_xxxxxxxx',
userId: 'u123', // 你的用户 id,会在服务端回调里原样带回
customData: 'level-5' // 选填,原样带回(最多 500 个字符)
});
ad.load(); // 可选的预加载
document.getElementById('watch-ad').addEventListener('click', function () {
ad.show()
.then(function (result) {
if (result.format === 'rewarded') {
// result.verified === true,字段见下方「激励结果」
// 这里只更新界面;发奖请以服务端回调为准(第 4 节)
} else {
// result: { format: 'interstitial' },插屏已展示并关闭
}
})
.catch(function (e) {
// e.code 是公开错误码,含义见下表(e.legacyCode 是 3.3.2 之前使用的小写名称);奖励未能确认时 e.verified 为 false
});
});
</script>用 async 加载 sdk.js 时,在 onload 里创建广告对象:
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<script>
var ad;
function initAd() {
ad = window.adbnk.telegram.init({ zoneId: 'zone_xxxxxxxx', userId: 'u123' });
}
</script>
<script async src="https://js.adbnk.co/sdk.js" onload="initAd()"></script>| 方法 / 属性 | 说明 |
|---|---|
adbnk.telegram.init({ zoneId, userId, customData }) | 为一个激励或插屏广告位创建广告对象。userId 与 customData 选填。userId 超过 255 个字符的部分会被截掉。customData 最多 500 个字符;超长时 load() 和 show() 会在加载任何内容之前以 INTERNAL_ERROR 拒绝(e.legacyCode 为 error,e.reason 为 custom_data_too_long)。 |
ad.load() | 可选的预加载,返回 Promise;加载过程中重复调用返回同一个 Promise。 |
ad.show() | 展示广告,返回 Promise。请在用户操作(例如点击按钮)时调用。 |
ad.state | 'idle'、'loading'、'ready'、'showing' 或 'destroyed'。 |
ad.destroy() | 释放广告对象,未完成的 Promise 以 destroyed 拒绝。 |
激励结果
激励广告看完且奖励得到确认时,show() 以下面的对象 resolve:
| 字段 | 类型 | 说明 |
|---|---|---|
format | string | 固定为 'rewarded'。 |
verified | boolean | 固定为 true。没有它,show() 不会以激励结果 resolve。 |
zoneId、adId、requestId | string | undefined | 广告位、广告和本次广告请求的 id。 |
userId、customData | string | undefined | 你传给 init() 的值(按上面的长度限制处理后)。 |
transactionId | string | 本次奖励的唯一 id,与服务端回调里的 transaction_id 相同。 |
rewardAmount | number | undefined | 广告位上配置的奖励数量。 |
rewardType | string | undefined | 广告位上配置的奖励类型。 |
timestamp | number | undefined | 奖励的 Unix 时间(秒)。 |
signature、keyId | string | 用于服务端核验:连同 transactionId 发给你的服务端,由服务端调用 POST https://api.adbnk.co/ad/reward/verify,传入 transaction_id、signature、key_id(见在服务端核验奖励)。它们不是服务端回调的签名格式,不要用第 4 节的方法验证它们。 |
signedFieldsComplete | boolean | 为 false 表示上面有些字段是用你传入的值补上的,而不是我们服务端返回的。 |
format、verified、transactionId、signature、keyId、signedFieldsComplete 一定存在。signedFieldsComplete 为 true 时,其余字段都取自我们服务端。为 false 时,服务端没有返回的字段会尽量用你传入的值补上,补不上就是 undefined:rewardAmount、rewardType、timestamp 没有可补的值,服务端没返回时就是 undefined;zoneId、adId、requestId、userId、customData 只有在无值可补时才是 undefined。使用前请先判断是否为 undefined。
这个结果只用于更新界面(例如提示「奖励发放中」),不能作为发奖凭据。
错误码
e.code | e.legacyCode | 含义 | 建议处理 |
|---|---|---|---|
NO_FILL(或 FREQUENCY_CAP_REACHED、CONTAINER_TOO_SMALL) | no_fill | 暂时没有可展示的广告(或已达到你为该广告位设置的展示次数上限,或容器过小)。 | 稍后再试。 |
USER_DISMISSED | closed | 用户在获得奖励前关闭了广告。 | 不发奖。 |
USER_DISMISSED | skipped | 用户跳过了广告。 | 不发奖。 |
LOAD_IN_PROGRESS | busy | 这个广告对象正在加载或展示。 | 等当前调用结束。 |
INVALID_ZONE | wrong_format | 该广告位不是激励或插屏广告位。 | 其他格式请用标签式接入。 |
REWARD_UNVERIFIED(或 NOT_ELIGIBLE、NETWORK_ERROR、INTERNAL_ERROR) | reward_failed | 广告已看完,但浏览器端未能确认奖励。e.verified 为 false。e.reason 为 unverified,e.code 为 NOT_ELIGIBLE 时为 not_eligible;它只为兼容保留,请改用 e.code。 | 不要据此发放任何有价值的奖励。见未确认的奖励。 |
NOT_READY | not_active | 小程序当前不在前台。 | 用户回到前台后再调用 show()。 |
OBJECT_RELEASED | destroyed | 广告对象已被销毁。 | 用 init() 重新创建。 |
INTERNAL_ERROR(或其他错误码,如 NETWORK_ERROR、TIMEOUT、EXPIRED) | error | 发生了意外错误,例如网络问题。customData 超过 500 个字符时 e.reason 为 custom_data_too_long。 | 稍后再试,或缩短 customData。 |
e.code 一定是公开错误码之一,完整列表和重试建议见 JS SDK 页的错误码。e.legacyCode 是兼容字段,已弃用,将来会移除,请改用 e.code。只有 Telegram 路径(adbnk.telegram.init(...))的错误带有它,值是 3.3.2 之前使用的旧小写名称(如 no_fill);标签路径(adbnk:result、adbnk:reward_unverified)没有它。e.message 是固定的一句话,e.retryable 表示稍后重试是否值得。
未确认的奖励
发奖只以你的服务端为准
reward_failed 错误和 adbnk:reward_unverified 事件都不代表可信奖励。有价值的奖励只能按服务端签名回调(SSV,见第 4 节)发放,或在你的服务端用 POST https://api.adbnk.co/ad/reward/verify 核验后发放。
e.code 为 REWARD_UNVERIFIED 或 NETWORK_ERROR 时,我们这边可能已经记录了这次奖励,你的服务端回调仍可能到达。请等回调,不要在浏览器端直接发奖。同一条广告再展示一次不会再领一次奖;想给用户再来一次的机会,请展示一条新广告。
4. 用服务端回调发奖
Promise 的结果只用于更新界面,发奖一律以服务端签名回调(SSV)为准。在后台编辑激励广告位,填写服务端奖励回调地址。
奖励确认后,ADBNK 会向你的回调地址发送 HTTP GET 请求:
| 参数 | 说明 |
|---|---|
transaction_id | 本次奖励的唯一 id,请用作幂等键。 |
user_id | 你传给 init() 的 userId。 |
tg_user_id | 观看广告的 Telegram 用户 id。Telegram 小程序广告位在可用时会带上,并与其他参数一起签名。不要假设它一定存在;请按字符串保存。 |
reward_amount | 广告位上配置的奖励数量。 |
reward_item | 广告位上配置的奖励类型。 |
custom_data | 你传入的 customData,原样带回(可能为空)。 |
ad_unit | 广告位的数字 id。 |
ad_network | 固定为 adbnk。 |
timestamp | 请求发出时的 Unix 时间(秒)。 |
key_id | 用于验证 signature 的公钥 id。 |
signature | Ed25519 签名,base64url 编码,不带填充。 |
记录奖励后,请在 5 秒内返回任意 2xx 状态码;我们不跟随重定向。同一笔交易可能会被投递多次,每个 transaction_id 只发一次奖。
验证签名
- 从
https://aapi.adbnk.co/ad/reward/keys获取公钥(JSON Web Key Set)。请缓存,遇到不认识的key_id时重新获取。使用kid等于key_id、kty为OKP的那一项。 - 取请求里的全部 query 参数,去掉
signature、key_id,以及你配置的回调地址里本来就带的参数。不要写死参数清单:有tg_user_id时它也参与签名,以后也可能增加新参数。按参数名的字节序排序,每个值按 RFC 3986 百分号编码(空格编码为%20),写成name=value并用&连接。空值也要写成name=。 - 用 Ed25519 公钥验证
signature是否是对该字符串 UTF-8 字节的签名,验证失败就拒绝这个请求。
import crypto from 'node:crypto';
// RFC 3986:encodeURIComponent 不转义 ! ' ( ) *,需要补上
const enc = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
// query:回调请求解析后的 query 参数
// ownParams:你配置的回调地址里本来就带的参数名
export function verifyAdbnkSsv(query, jwks, ownParams = []) {
const jwk = (jwks.keys || []).find((k) => k.kid === query.key_id && k.kty === 'OKP' && k.crv === 'Ed25519');
if (!jwk || typeof query.signature !== 'string') return false;
const skip = new Set(['signature', 'key_id', ...ownParams]);
const names = Object.keys(query)
.filter((n) => !skip.has(n))
.sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
if (names.some((n) => typeof query[n] !== 'string')) return false;
const message = names.map((n) => `${enc(n)}=${enc(query[n])}`).join('&');
try {
const key = crypto.createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, format: 'jwk' });
return crypto.verify(null, Buffer.from(message, 'utf8'), key, Buffer.from(query.signature, 'base64url'));
} catch {
return false;
}
}// $query:回调的 query 参数(例如 $_GET)
// $ownParams:你配置的回调地址里本来就带的参数名
function adbnk_ssv_verify(array $query, array $jwks, array $ownParams = []): bool
{
$jwk = null;
foreach ($jwks['keys'] ?? [] as $k) {
if (($k['kid'] ?? '') === ($query['key_id'] ?? '') && ($k['kty'] ?? '') === 'OKP' && ($k['crv'] ?? '') === 'Ed25519') {
$jwk = $k;
}
}
if ($jwk === null || !is_string($query['signature'] ?? null)) {
return false;
}
$params = array_diff_key($query, array_flip(array_merge(['signature', 'key_id'], $ownParams)));
ksort($params, SORT_STRING);
$pairs = [];
foreach ($params as $name => $value) {
if (!is_string($value)) {
return false;
}
$pairs[] = rawurlencode((string)$name) . '=' . rawurlencode($value);
}
try {
$sig = sodium_base642bin((string)$query['signature'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
$pub = sodium_base642bin((string)$jwk['x'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
return sodium_crypto_sign_verify_detached($sig, implode('&', $pairs), $pub);
} catch (\Throwable $e) {
return false;
}
}激励广告的更多说明见 广告请求 API → 激励广告。
5. 体验建议
- 在自然的停顿点展示广告,例如关卡之间、任务完成之后。
- 让用户自己选择是否观看激励广告,并由点击触发。
- 不要在小程序一打开就弹出广告。
- 观看前告诉用户能得到什么,收到服务端回调后立即发放。
6. 常见问题
在普通浏览器里打开小程序看不到广告。 Telegram 站点的广告只在 Telegram 内打开小程序时展示。我们会核验 Telegram 启动数据的真实性。
完全没有广告。 请确认站点和广告位都已审核通过、telegram-web-app.js 在 sdk.js 之前加载、sdk.js 能正常加载(广告拦截插件可能拦截它)。有时只是暂时没有可展示的广告,请稍后再试。
第 3 步「在 Telegram 里打开一次」一直检测不到。 请确认小程序页面已接入 SDK,并且是在 Telegram 里(而不是浏览器里)打开后台给出的链接,然后点 重新检测。
第 2 步「Bot 简介验证」检测不到。 请用 /setabouttext 把验证码写进默认语言的简介。Telegram 可能需要几分钟才会显示新简介,请稍后再试。
部分用户一直看不到广告。 请让他们把 Telegram 更新到最新版本。
show() 被拒绝,错误码是 NOT_READY(e.legacyCode 为 not_active)。 小程序当时在后台。等用户回到前台后再调用 show()。
show() 返回 reward_failed,但我的服务端收到了回调。 浏览器端在确认奖励时超时就可能出现这种情况。以服务端回调为准,按回调发奖即可。
能修改站点的 bot 或域名吗? 不能,请新建一个站点。
7. 内容政策
你的小程序和 bot 需要遵守 Telegram 小程序服务条款(Telegram 官方条款原名为 Terms of Service for Mini Apps)以及 ADBNK 的流量主政策。