Skip to content

Telegram Mini App ​

Show ADBNK ads inside your Telegram Mini App with the ADBNK Web SDK. Rewarded ads come first; banners, native ads and interstitials are also available.

Before you start ​

  • You have a Telegram bot with a Mini App.
  • Your Mini App page is hosted on your own HTTPS domain.
  • You can add scripts to the Mini App page.

1. Add a Telegram site ​

In the publisher console, open Sites → Add site and choose Telegram Mini App.

FieldWhat to enter
Mini App URLThe https URL of your Mini App page. The domain is taken from it.
Bot usernameFor example your_app_bot. We look up the Bot ID for you.
Mini App short nameOptional. Fill it in if your app opens as t.me/<bot>/<short_name>.

Never share your bot token

You don't need to give us a Bot ID or a bot token. If anything that looks like a bot token is pasted into the form, the console clears it right away.

After you submit, the console shows the Bot ID we found — check that it is your bot. The Telegram details can't be changed later; to use another bot, add a new site.

Verify the site in three steps ​

In the site list, click Verify on the site and complete all three steps:

  1. Domain — the same as for websites: a meta tag, a root file or a DNS TXT record.
  2. Bot description — in @BotFather send /setabouttext, pick your bot and add the code shown in the console to the default-language description (not only to one translation). Keep the code until the site is approved; you can remove it afterwards. If you have just changed the description, wait a few minutes before checking again.
  3. Open once in Telegram — first add the SDK to your Mini App (see Add the SDK). Then open the launch link from the console inside Telegram: https://t.me/<bot>?startapp=<code>, or https://t.me/<bot>/<short_name>?startapp=<code> if you use a short name. Come back and click Check again. This step also confirms that your SDK integration works.

When all three steps pass, our team reviews the site. Every Telegram site is reviewed manually.

2. Create zones ​

Telegram sites can use these zone types:

Zone typeHow to integrate
Rewarded video (recommended)Programmatic
Rewarded interstitial (recommended)Programmatic
Mobile interstitial, desktop interstitialProgrammatic
Banner, mobile banner, sticky bannerTag
Floating banner, mobile floating bannerTag
Native feedTag

A zone can serve ads after both the site and the zone are approved.

3. Add the SDK ​

Always load Telegram's telegram-web-app.js before the ADBNK SDK. Copy the snippet from Get code in the console; the examples below show its shape.

Tag integration (banner, native, floating) ​

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>

Programmatic integration (rewarded, interstitial) ​

For programmatic use, load sdk.js without async. window.adbnk.telegram exists as soon as sdk.js has run; the SDK has no command queue, so code that runs earlier cannot call it. If you must load sdk.js with async, call window.adbnk.telegram.init(...) from the script's onload handler.

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">Watch an ad for 10 coins</button>
<script>
  const ad = window.adbnk.telegram.init({
    zoneId: 'zone_xxxxxxxx',
    userId: 'u123',          // your user id, passed back in the server callback
    customData: 'level-5'    // optional, passed back unchanged (max 500 characters)
  });
  ad.load(); // optional preload

  document.getElementById('watch-ad').addEventListener('click', function () {
    ad.show()
      .then(function (result) {
        if (result.format === 'rewarded') {
          // result.verified === true; see "Rewarded result" below.
          // Update your UI only. Grant the reward from the server callback (step 4).
        } else {
          // result: { format: 'interstitial' } — the interstitial was shown and closed
        }
      })
      .catch(function (e) {
        // e.code is a public error code, see the table below (e.legacyCode is the lowercase name used before 3.3.2). e.verified is false when a reward could not be confirmed.
      });
  });
</script>

If you load sdk.js with async, create the ad object in 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>
Method / propertyDescription
adbnk.telegram.init({ zoneId, userId, customData })Creates an ad object for one rewarded or interstitial zone. userId and customData are optional. userId is cut to 255 characters. customData may be at most 500 characters; a longer value makes load() and show() reject with INTERNAL_ERROR (e.legacyCode is error, e.reason is custom_data_too_long) before anything is loaded.
ad.load()Optional preload. Returns a Promise. Calling it again while loading returns the same Promise.
ad.show()Shows the ad and returns a Promise. Call it from a user action, such as a button tap.
ad.state'idle', 'loading', 'ready', 'showing' or 'destroyed'.
ad.destroy()Releases the ad object. Pending Promises are rejected with destroyed.

Rewarded result ​

When a rewarded ad is completed and the reward is confirmed, show() resolves with:

FieldTypeDescription
formatstringAlways 'rewarded'.
verifiedbooleanAlways true. show() never resolves with a rewarded result without it.
zoneId, adId, requestIdstring | undefinedIds of the zone, the ad and the ad request.
userId, customDatastring | undefinedThe values you passed to init() (after the limits above).
transactionIdstringUnique id of this reward. It is the same as transaction_id in your server callback.
rewardAmountnumber | undefinedReward amount configured on the zone.
rewardTypestring | undefinedReward type configured on the zone.
timestampnumber | undefinedUnix time (seconds) of the reward.
signature, keyIdstringFor server-side verification: send them with transactionId to your server, which calls POST https://api.adbnk.co/ad/reward/verify with transaction_id, signature and key_id (see Check a reward on your server). They are not in the server-callback format; don't check them with the steps in section 4.
signedFieldsCompletebooleanfalse means some fields above were filled in from the values you passed rather than returned by our server.

format, verified, transactionId, signature, keyId and signedFieldsComplete are always present. When signedFieldsComplete is true, the other fields come from our server. When it is false, a field our server didn't return is filled in from what you passed if possible; otherwise it is undefined. rewardAmount, rewardType and timestamp can't be filled in, so they are undefined whenever our server didn't return them; zoneId, adId, requestId, userId and customData are undefined only when there is nothing to fill them in with. Check for undefined before using them.

Use this result to update your UI, for example to show "Reward on its way". It is not proof of a reward.

Error codes ​

e.codee.legacyCodeMeaningWhat to do
NO_FILL (or FREQUENCY_CAP_REACHED, CONTAINER_TOO_SMALL)no_fillNo ad is available right now (or the display limit you set for this placement was reached, or the container is too small).Try again later.
USER_DISMISSEDclosedThe user closed the ad before earning the reward.Don't grant the reward.
USER_DISMISSEDskippedThe user skipped the ad.Don't grant the reward.
LOAD_IN_PROGRESSbusyThis ad object is already loading or showing.Wait for the current call to finish.
INVALID_ZONEwrong_formatThe zone is not a rewarded or interstitial zone.Use the tag integration for other formats.
REWARD_UNVERIFIED (or NOT_ELIGIBLE, NETWORK_ERROR, INTERNAL_ERROR)reward_failedThe ad was watched, but the reward could not be confirmed in the browser. e.verified is false. e.reason is unverified, or not_eligible when e.code is NOT_ELIGIBLE; it is kept for compatibility, use e.code.Don't grant anything of value from this result. See Unconfirmed rewards.
NOT_READYnot_activeYour Mini App is not in the foreground.Call show() again when the user is back.
OBJECT_RELEASEDdestroyedThe ad object was destroyed.Create a new one with init().
INTERNAL_ERROR (or another code such as NETWORK_ERROR, TIMEOUT, EXPIRED)errorSomething unexpected happened, such as a network problem. e.reason is custom_data_too_long when customData is over 500 characters.Try again later, or shorten customData.

Every e.code is one of the public codes; the full list with retry advice is in Error codes on the JS SDK page. e.legacyCode is a compatibility field. It is deprecated and will be removed in a future version, so use e.code instead. Only errors from the Telegram path (adbnk.telegram.init(...)) carry it; its value is the lowercase name used before 3.3.2 (for example no_fill). Errors on the tag path (adbnk:result, adbnk:reward_unverified) do not have it. e.message is a fixed sentence and e.retryable says whether trying again later is worthwhile.

Unconfirmed rewards ​

Only your server decides rewards

Neither the reward_failed error nor the adbnk:reward_unverified event means a trusted reward. Grant rewards of value only from your server-side callback (SSV, section 4) or after checking the reward with POST https://api.adbnk.co/ad/reward/verify on your server.

With an e.code of REWARD_UNVERIFIED or NETWORK_ERROR, the reward may still have been recorded on our side, and your server callback may still arrive. Wait for the callback instead of granting from the browser. Showing the same ad again does not claim the reward again; to give the user another chance, show a new ad.

4. Grant rewards with the server callback ​

The Promise result is for updating your UI. Always grant rewards from the server-side callback (SSV). Set the callback URL on the rewarded zone in the console (edit the zone → server-side reward callback).

When a reward is confirmed, ADBNK sends an HTTP GET request to your callback URL:

ParameterDescription
transaction_idUnique id of this reward. Use it as your idempotency key.
user_idThe userId you passed to init().
tg_user_idThe Telegram user id of the person who watched the ad. Sent for Telegram Mini App zones when it is available, and signed like the other parameters. Don't assume it is always present, and store it as a string.
reward_amountReward amount configured on the zone.
reward_itemReward type configured on the zone.
custom_dataThe customData you passed, unchanged (may be empty).
ad_unitNumeric id of the zone.
ad_networkAlways adbnk.
timestampUnix time (seconds) when the request was sent.
key_idId of the public key for signature.
signatureEd25519 signature, base64url without padding.

Respond with any 2xx status within 5 seconds after you record the reward; redirects are not followed. The same transaction may be delivered more than once, so grant each transaction_id only once.

Verify the signature ​

  1. Fetch the public keys from https://aapi.adbnk.co/ad/reward/keys (a JSON Web Key Set). Cache them and refetch when you see an unknown key_id. Use the key whose kid equals key_id and whose kty is OKP.
  2. Take every query parameter of the request except signature, key_id and the parameters that were already part of the callback URL you configured. Don't hard-code a list: tg_user_id is included when present, and new parameters may be added later. Sort the names in byte order, percent-encode each value per RFC 3986 (space is %20), and join them as name=value with &. Empty values are included as name=.
  3. Verify signature over the UTF-8 bytes of that message with the Ed25519 public key. Reject the request if it fails.
js
import crypto from 'node:crypto';

// RFC 3986: encodeURIComponent leaves ! ' ( ) * unescaped, so escape them too
const enc = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());

// query: the parsed query string of the callback request
// ownParams: names of the parameters already in the callback URL you configured
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: the callback's query parameters (for example $_GET)
// $ownParams: names of the parameters already in the callback URL you configured
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;
    }
}

For more on rewarded ads, see Ad Request API → Rewarded ads.

5. Best practices ​

  • Show ads at natural breaks, such as between levels or after a task is done.
  • Let users choose to watch rewarded ads, and start them from a tap.
  • Don't show an ad as soon as the Mini App opens.
  • Tell users what they get before they watch, and give it to them right after the server callback arrives.

6. FAQ ​

Ads don't show when I open my Mini App in a normal browser. Ads on Telegram sites are shown only when the Mini App is opened inside Telegram. We verify that Telegram launch data is genuine.

No ads show at all. Check that the site and the zone are approved, that telegram-web-app.js loads before sdk.js, and that sdk.js loads (ad blockers can block it). Sometimes there is simply no ad to show; try again later.

Step 3 "Open once in Telegram" isn't detected. Make sure the SDK is on your Mini App page, that you opened the link from the console inside Telegram (not in a browser), and then click Check again.

Step 2 "Bot description" isn't detected. Put the code in the default-language description with /setabouttext. Telegram may take a few minutes to show the new description; try again later.

Some users never see ads. Ask them to update Telegram to the latest version.

show() is rejected with NOT_READY (e.legacyCode is not_active). The Mini App was in the background. Call show() again after the user returns.

show() is rejected with reward_failed, but my server got the callback. That can happen when the browser times out while the reward is being confirmed. Your server callback is what counts; grant the reward from it.

Can I change the bot or the domain of a site? No. Add a new site instead.

7. Content policy ​

Your Mini App and bot must follow Telegram's Terms of Service for Mini Apps and the ADBNK publisher policies.

Documentation released under the MIT License.