Skip to content

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,请新建站点。

完成三步站点验证 ​

在站点列表中点击该站点的 验证,依次完成:

  1. 域名验证:与网站相同,可用 meta 标签、根目录文件或 DNS TXT 记录。
  2. Bot 简介验证:在 @BotFather 中发送 /setabouttext,选择你的 bot,把后台显示的验证码写进默认语言的简介(不要只写进某个语言版本)。审核通过前请不要删除验证码,通过后可以删除。如果刚改过简介,请稍等几分钟再检测。
  3. 在 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。代码请从后台的 获取代码 复制,下面的示例只说明结构。

标签式(横幅、原生、浮动) ​

html
<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(...)。

html
<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 里创建广告对象:

html
<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:

字段类型说明
formatstring固定为 'rewarded'。
verifiedboolean固定为 true。没有它,show() 不会以激励结果 resolve。
zoneId、adId、requestIdstring | undefined广告位、广告和本次广告请求的 id。
userId、customDatastring | undefined你传给 init() 的值(按上面的长度限制处理后)。
transactionIdstring本次奖励的唯一 id,与服务端回调里的 transaction_id 相同。
rewardAmountnumber | undefined广告位上配置的奖励数量。
rewardTypestring | undefined广告位上配置的奖励类型。
timestampnumber | undefined奖励的 Unix 时间(秒)。
signature、keyIdstring用于服务端核验:连同 transactionId 发给你的服务端,由服务端调用 POST https://api.adbnk.co/ad/reward/verify,传入 transaction_id、signature、key_id(见在服务端核验奖励)。它们不是服务端回调的签名格式,不要用第 4 节的方法验证它们。
signedFieldsCompleteboolean为 false 表示上面有些字段是用你传入的值补上的,而不是我们服务端返回的。

format、verified、transactionId、signature、keyId、signedFieldsComplete 一定存在。signedFieldsComplete 为 true 时,其余字段都取自我们服务端。为 false 时,服务端没有返回的字段会尽量用你传入的值补上,补不上就是 undefined:rewardAmount、rewardType、timestamp 没有可补的值,服务端没返回时就是 undefined;zoneId、adId、requestId、userId、customData 只有在无值可补时才是 undefined。使用前请先判断是否为 undefined。

这个结果只用于更新界面(例如提示「奖励发放中」),不能作为发奖凭据。

错误码 ​

e.codee.legacyCode含义建议处理
NO_FILL(或 FREQUENCY_CAP_REACHED、CONTAINER_TOO_SMALL)no_fill暂时没有可展示的广告(或已达到你为该广告位设置的展示次数上限,或容器过小)。稍后再试。
USER_DISMISSEDclosed用户在获得奖励前关闭了广告。不发奖。
USER_DISMISSEDskipped用户跳过了广告。不发奖。
LOAD_IN_PROGRESSbusy这个广告对象正在加载或展示。等当前调用结束。
INVALID_ZONEwrong_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_READYnot_active小程序当前不在前台。用户回到前台后再调用 show()。
OBJECT_RELEASEDdestroyed广告对象已被销毁。用 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。
signatureEd25519 签名,base64url 编码,不带填充。

记录奖励后,请在 5 秒内返回任意 2xx 状态码;我们不跟随重定向。同一笔交易可能会被投递多次,每个 transaction_id 只发一次奖。

验证签名 ​

  1. 从 https://aapi.adbnk.co/ad/reward/keys 获取公钥(JSON Web Key Set)。请缓存,遇到不认识的 key_id 时重新获取。使用 kid 等于 key_id、kty 为 OKP 的那一项。
  2. 取请求里的全部 query 参数,去掉 signature、key_id,以及你配置的回调地址里本来就带的参数。不要写死参数清单:有 tg_user_id 时它也参与签名,以后也可能增加新参数。按参数名的字节序排序,每个值按 RFC 3986 百分号编码(空格编码为 %20),写成 name=value 并用 & 连接。空值也要写成 name=。
  3. 用 Ed25519 公钥验证 signature 是否是对该字符串 UTF-8 字节的签名,验证失败就拒绝这个请求。
js
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;
  }
}
php
// $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 的流量主政策。

按 MIT 许可发布的文档内容。