SubGram API

RU

SubGram API Documentation

Everything you need to integrate your services with our platform.

Getting started

The SubGram API provides programmatic access to our platform's features. You can manage bots, advertising orders, retrieve statistics and much more.

All API requests should be sent to the base URL:

https://api.subgram.org

Authentication

Interacting with the API requires an authentication key. Every request must include the HTTP header Auth with your key.

There are three main types of keys, each for its own purpose:

  • Access Key (Secret Key): 🔐 Your main and secret key for performing management actions: creating and updating orders (/orders), adding and configuring bots (/bots).
    How to get it: in our official bot @subgram_officialbot go to Profile → Access Key (Secret Key). Never share this key with anyone!
  • Statistics Token (API Token): 📈 This token is intended only for retrieving data such as balance (/get-balance) and statistics (/statistic).
    How to get it: in the bot @subgram_officialbot go to Profile → Copy api token.
  • Bot Key (API Key): 🤖 Generated for each added bot. Used for requests on behalf of a specific bot: getting sponsors (/get-sponsors) and checking subscriptions (/get-user-subscriptions).

Header example

Auth: YOUR_SECRET_ACCESS_KEY

Response structure

All API responses come in JSON format. Successful responses and errors share a standardized structure for predictable handling.

Example of a successful response (HTTP 200 OK)

{
                      "status": "ok",
                      "code": 200,
                      "message": "Operation completed successfully",
                      "result": { ... } 
                    }
Note: The key that carries the payload may be named result or response depending on the endpoint. Always check the documentation for the specific method.

Example of an error response (HTTP 4xx/5xx)

{
                      "status": "error",
                      "code": 401,
                      "message": "Invalid API token"
                    }

Publisher API

Methods for bot owners to monetize traffic.

Getting sponsors

POST /get-sponsors

The primary method for requesting the mandatory-subscription block. The service analyzes the user and picks the most relevant list of sponsors for them. Authentication is done via the bot's API key in the header Auth.

Request parameters (Body, JSON)

ParameterTypeRequiredDescription
chat_idIntegerYesID of the chat the request comes from.
user_idIntegerYesTelegram user ID.
first_nameStringConditional*The user's Telegram first name.
usernameStringConditional*The user's username (without @).
language_codeStringConditional*The user's Telegram interface language (e.g. "ru", "en").
is_premiumBooleanConditional*Whether the user has Telegram Premium: true or false.
actionStringNo Action type:
  • subscribe (default): The sponsor list is pinned to the user for N amount of time, set in the bot's settings in the service.
    The subscription check can be performed by a repeat request with the same user_id
  • newtask: The list is not pinned. On every new request the service will rebuild the sponsor list.
    The subscription check is performed via the "Checking subscriptions"
  • task: The list is "conditionally" pinned. Sponsors are also remembered in our system, but on a repeat request with the same user_id on a successful subscription the list is deleted and a new list will be selected on the next request.
    This method is a hybrid between subscribe and newtask
max_sponsorsIntegerNoMax number of sponsors (channel/chat links) in the response (1-10). By default the value is taken from the bot's settings.
Note: If the parameter is provided, it takes priority over the bot's settings.
get_linksIntegerNoReceive links in the API response (1) or let the service show the mandatory-subscription block itself (0).
By default the value is taken from the bot's settings.
Note: If the parameter is provided, it takes priority over the bot's settings.
exclude_resource_idsArray[String]NoArray of channel/bot IDs to exclude. Example: ["-100123...", "54321..."].
exclude_ads_idsArray[Integer]NoArray of order IDs to exclude.

* — these fields become required if your bot was added to the system without a token.

Request examples

# Example request body in JSON format (cURL)
curl -X POST https://api.subgram.org/get-sponsors \
-H "Auth: YOUR_BOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
      "user_id": 123456789,
      "chat_id": 123456789,
      #The parameters below are for when you did not pass the bot token
      #"first_name": "John",
      #"username": "ivanov",
      #"language_code": "ru",
      #"is_premium": false,
    }'

In this mode SubGram sends the user the mandatory-subscription block itself when needed. Your job is simply to call the API and check the status. If the status is blocking (warning), you don't need to do anything else — the service handles it all.


# --- File: utils/subgram_api.py ---
import aiohttp
import logging

API_KEY = "YOUR_BOT_API_KEY"
URL = "https://api.subgram.org/get-sponsors"

async def get_subgram_sponsors(user_id: int, chat_id: int, **kwargs) -> dict | None:
    """Universal function for requesting sponsors."""
    headers = { "Auth": API_KEY }
    payload = { "user_id": user_id, "chat_id": chat_id }
    payload.update(kwargs)
    
    async with aiohttp.ClientSession() as session:
        try:
            async with session.post(URL, headers=headers, json=payload, timeout=10) as response:
                return await response.json()
        except Exception as e:
            logging.error(f"SubGram API request error: {e}")
            return None

# --- File: handlers/start.py (example handler) ---
from aiogram import F, types, Router
from utils.subgram_api import get_subgram_sponsors
from aiogram.filters import CommandStart

router = Router()

@router.message(CommandStart())
async def handle_start_command(message: types.Message):
    # Call the subscription-check function
    response = await get_subgram_sponsors(
        user_id=message.from_user.id,
        chat_id=message.chat.id
    )

    if response:
        status = response.get("status")
       
        if status and status == 'warning':
            # The service will send the subscription-request message itself
            return
        if status and status == "error":
            logging.warning(f"SubGram API error: {response.get('message')}. Granting access.")

    await message.answer("✅ Access granted!")
    # ... your code to deliver the content ...


@router.callback_query(F.data == 'subgram-op')
async def handle_subgram_callback(callback: types.CallbackQuery):
    user_id = callback.from_user.id
    chat_id = callback.message.chat.id
    
    await callback.answer("⏳ Checking subscriptions...")

    # Send a repeat request to the SubGram API with the new data
    response = await get_subgram_sponsors(user_id, chat_id)

    if response:
        status = response.get("status")
       
        if status and status == 'warning':
            return
        if status and status == "error":
            logging.warning(f"SubGram API error: {response.get('message')}. Granting access.")

    await callback.message.answer("✅ Access granted!")
    # ... your code to deliver the content ...
            

// --- File: subgramApi.js ---
const axios = require('axios');

const API_KEY = 'YOUR_BOT_API_KEY';
const URL = 'https://api.subgram.org/get-sponsors';

async function getSubgramSponsors(userId, chatId, options = {}) {
  const headers = { 
      'Auth': API_KEY,
      'Content-Type': 'application/json'
  };
  const payload = { user_id: userId, chat_id: chatId, ...options };
  
  try {
    const response = await axios.post(URL, payload, { headers, timeout: 10000 });
    return response.data;
  } catch (error) {
    console.error('SubGram API request error:', error.response?.data || error.message);
    return null;
  }
}
module.exports = { getSubgramSponsors };


// --- File: bot.js (example with node-telegram-bot-api) ---
// ... bot initialization ...
// const { getSubgramSponsors } = require('./subgramApi');

bot.onText(/\/start/, async (msg) => {
    // Call the subscription-check function
    const response = await getSubgramSponsors(msg.from.id, msg.chat.id);

    if (response) {
        const status = response.status;
        
        if (status && status === 'warning') {
            // The service will send the subscription-request message itself
            return;
        }
        if (status && status === 'error') {
            console.warn(`SubGram API error: ${response.message}. Granting access.`);
        }
    }

    await bot.sendMessage(msg.chat.id, "✅ Access granted!");
    // ... your code to deliver the content ...
});

bot.on('callback_query', async (callbackQuery) => {
    if (callbackQuery.data === 'subgram-op') {
        const userId = callbackQuery.from.id;
        const chatId = callbackQuery.message.chat.id;

        await bot.answerCallbackQuery(callbackQuery.id, { text: "⏳ Checking subscriptions..." });

        // Send a repeat request
        const response = await getSubgramSponsors(userId, chatId);
        
        if (response) {
            const status = response.status;
            
            if (status && status === 'warning') {
                return;
            }
            if (status && status === 'error') {
                console.warn(`SubGram API error: ${response.message}. Granting access.`);
            }
        }

        await bot.sendMessage(chatId, "✅ Access granted!");
        // ... your code to deliver the content ...
    }
});
                        

<?php
// --- File: subgram_api.php ---
const API_KEY = "YOUR_BOT_API_KEY";
const URL = "https://api.subgram.org/get-sponsors";

function getSubgramSponsors($userId, $chatId, $options = []) {
    $payload = ['user_id' => $userId, 'chat_id' => $chatId];
    $payload = array_merge($payload, $options);
    
    $headers = [
        'Auth: ' . API_KEY,
        'Content-Type: application/json'
    ];
    
    $ch = curl_init(URL);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    $response_body = curl_exec($ch);
    curl_close($ch);
    
    if ($response_body === false) {
        error_log("SubGram API request error: cURL error");
        return null;
    }
    
    return json_decode($response_body, true);
}

// --- File: bot.php (example webhook handler) ---
// ... initialization and message-sending functions ...

$update = json_decode(file_get_contents('php://input'), true);

if (isset($update['message'])) {
    $userId = $update['message']['from']['id'];
    $chatId = $update['message']['chat']['id'];

    // Call the subscription-check function
    $response = getSubgramSponsors($userId, $chatId);

    if ($response) {
        $status = $response['status'] ?? null;
        
        if ($status && $status === 'warning') {
            // The service will send the subscription-request message itself
            exit(); 
        }
        if ($status && $status === 'error') {
            error_log("SubGram API error: " . $response['message'] . ". Granting access.");
        }
    }
    
    sendMessage($chatId, "✅ Access granted!");
    // ... your code to deliver the content ...

} elseif (isset($update['callback_query'])) {
    $callback = $update['callback_query'];
    
    if ($callback['data'] === 'subgram-op') {
        $userId = $callback['from']['id'];
        $chatId = $callback['message']['chat']['id'];

        answerCallbackQuery($callback['id'], ['text' => '⏳ Checking subscriptions...']);

        // Send a repeat request
        $response = getSubgramSponsors($userId, $chatId);
        
        if ($response) {
            $status = $response['status'] ?? null;
            
            if ($status && $status === 'warning') {
                exit();
            }
            if ($status && $status === 'error') {
                 error_log("SubGram API error: " . $response['message'] . ". Granting access.");
            }
        }

        sendMessage($chatId, "✅ Access granted!");
        // ... your code to deliver the content ...
    }
}
?>
                        

In this mode the service returns JSON in response to your request, and you must build and send the message with buttons to the user yourself. This approach requires handling all possible response statuses: showing sponsors, gender/age questions and the form.


# --- File: utils/subgram_api.py ---
import aiohttp
import logging

API_KEY = "YOUR_BOT_API_KEY"
URL = "https://api.subgram.org/get-sponsors"

async def get_subgram_sponsors(user_id: int, chat_id: int, **kwargs) -> dict | None:
    """Universal function for requesting sponsors."""
    headers = { "Auth": API_KEY }
    payload = { "user_id": user_id, "chat_id": chat_id }
    payload.update(kwargs)
    
    async with aiohttp.ClientSession() as session:
        try:
            async with session.post(URL, headers=headers, json=payload, timeout=10) as response:
                return await response.json()
        except Exception as e:
            logging.error(f"SubGram API request error: {e}")
            return None

# --- File: handlers/subgram.py ---
import logging
from aiogram import F, types, Router
from aiogram.filters import CommandStart
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.exceptions import TelegramBadRequest

from utils.subgram_api import get_subgram_sponsors

router = Router()

async def process_subgram_check(user: types.User, chat_id: int, api_kwargs: dict = None):
    """Main function that handles every status from SubGram."""
    if api_kwargs is None:
        api_kwargs = {}

    # Pass more user data if you did not provide the token.
    user_data = {
        "first_name": user.first_name,
        "username": user.username,
        "language_code": user.language_code,
        "is_premium": bool(user.is_premium)
    }
    user_data.update(api_kwargs)
    
    response = await get_subgram_sponsors(user_id=user.id, chat_id=chat_id, **user_data)

    if response:
        status = response.get("status")
        if status and status == "warning":
            builder = InlineKeyboardBuilder()
            text = "Please complete the tasks below:"
            sponsors = response.get("additional", {}).get("sponsors", [])
            for sponsor in sponsors:
                # Show only those the user still needs to subscribe to
                if sponsor.get("available_now") and sponsor.get("status") == "unsubscribed":
                    builder.button(text=sponsor.get("button_text", "Subscribe"), url=sponsor.get("link"))
            builder.button(text="✅ I'm done", callback_data="subgram-op")
            builder.adjust(1)
            
            # To keep the example generic, return the data to be sent:
            return False, text, builder.as_markup()
                
        else: # error, ok or unknown status -> let through
            return True, None, None
    else: # request error -> let through
        return True, None, None


@router.message(CommandStart())
async def handle_start_links(message: types.Message):
    is_allowed, text, reply_markup = await process_subgram_check(message.from_user, message.chat.id)

    if not is_allowed:
        await message.answer(text, reply_markup=reply_markup)
        return

    # Grant access
    await message.answer("✅ Access granted!")
    # ... your main code ...
    

@router.callback_query(F.data == 'subgram-op')
async def handle_callback_links(callback: types.CallbackQuery):
    # Delete the old message
    try:
        await callback.message.delete()
    except TelegramBadRequest:
        logging.info("Failed to delete the message (it may already be deleted)")

    await callback.answer("⏳ Checking subscriptions...")

    is_allowed, text, reply_markup = await process_subgram_check(callback.from_user, callback.message.chat.id)

    if not is_allowed:
        await callback.message.answer(text, reply_markup=reply_markup)
        return

    # Grant access
    await callback.message.answer("✅ Access granted!")
    # ... your main code ...
                        

// --- File: subgramApi.js ---
const axios = require('axios');

const API_KEY = 'YOUR_BOT_API_KEY';
const URL = 'https://api.subgram.org/get-sponsors';

async function getSubgramSponsors(userId, chatId, options = {}) {
  const headers = { 
      'Auth': API_KEY,
      'Content-Type': 'application/json'
  };
  const payload = { user_id: userId, chat_id: chatId, ...options };
  
  try {
    const response = await axios.post(URL, payload, { headers, timeout: 10000 });
    return response.data;
  } catch (error) {
    console.error('SubGram API request error:', error.response?.data || error.message);
    return null;
  }
}
module.exports = { getSubgramSponsors };


// --- File: bot.js ---
// ... bot initialization ...
// const { getSubgramSponsors } = require('./subgramApi');

async function processSubgramCheck(user, chatId, apiOptions = {}) {
    // Pass more user data
    const userData = {
        "first_name": user.first_name,
        "username": user.username,
        "language_code": user.language_code,
        "is_premium": !!user.is_premium
    };
    Object.assign(userData, apiOptions);

    const response = await getSubgramSponsors(user.id, chatId, userData);

    if (response) {
        const status = response.status;
        if (status === "warning") {
            const text = "Please complete the tasks below:";
            const sponsors = response.additional?.sponsors || [];
            const inline_keyboard = [];
            
            sponsors.forEach(sponsor => {
                // Show only those the user still needs to subscribe to
                if (sponsor.available_now && sponsor.status === "unsubscribed") {
                    inline_keyboard.push([{ text: sponsor.button_text || "Subscribe", url: sponsor.link }]);
                }
            });
            inline_keyboard.push([{ text: "✅ I'm done", callback_data: "subgram-op" }]);

            // Return an object instructing to send the message
            return { allowed: false, text, reply_markup: { inline_keyboard } };
        }
    }
    // error, ok, unknown status or network error -> let through
    return { allowed: true };
}


bot.onText(/\/start/, async (msg) => {
    const result = await processSubgramCheck(msg.from, msg.chat.id);

    if (!result.allowed) {
        await bot.sendMessage(msg.chat.id, result.text, { reply_markup: result.reply_markup });
        return;
    }

    // Grant access
    await bot.sendMessage(msg.chat.id, "✅ Access granted!");
});

bot.on('callback_query', async (callbackQuery) => {
    if (callbackQuery.data === 'subgram-op') {
        // Delete the old message
        bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id).catch(() => {});
        
        await bot.answerCallbackQuery(callbackQuery.id, { text: "⏳ Checking subscriptions..." });

        const result = await processSubgramCheck(callbackQuery.from, callbackQuery.message.chat.id);

        if (!result.allowed) {
            await bot.sendMessage(callbackQuery.message.chat.id, result.text, { reply_markup: result.reply_markup });
            return;
        }

        // Grant access
        await bot.sendMessage(callbackQuery.message.chat.id, "✅ Access granted!");
    }
});
                        

<?php
// --- File: subgram_api.php ---
const API_KEY = "YOUR_BOT_API_KEY";
const URL = "https://api.subgram.org/get-sponsors";

function getSubgramSponsors($userId, $chatId, $options = []) {
    $payload = ['user_id' => $userId, 'chat_id' => $chatId];
    $payload = array_merge($payload, $options);
    
    $headers = [
        'Auth: ' . API_KEY,
        'Content-Type: application/json'
    ];
    
    $ch = curl_init(URL);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    $response_body = curl_exec($ch);
    curl_close($ch);
    
    if ($response_body === false) return null;
    return json_decode($response_body, true);
}


// --- File: bot.php ---
// ... sendMessage, deleteMessage functions ...

function processSubgramCheck($user, $chatId, $apiOptions = []) {
    $userData = [
        "first_name" => $user['first_name'],
        "username" => $user['username'] ?? null,
        "language_code" => $user['language_code'] ?? null,
        "is_premium" => !empty($user['is_premium'])
    ];
    $userData = array_merge($userData, $apiOptions);
    
    $response = getSubgramSponsors($user['id'], $chatId, $userData);

    if ($response) {
        $status = $response['status'] ?? null;
        if ($status === "warning") {
            $text = "Please complete the tasks below:";
            $sponsors = $response['additional']['sponsors'] ?? [];
            $keyboard = [];

            foreach ($sponsors as $sponsor) {
                if (!empty($sponsor['available_now']) && $sponsor['status'] === "unsubscribed") {
                    $keyboard[] = [['text' => $sponsor['button_text'] ?? "Subscribe", 'url' => $sponsor['link']]];
                }
            }
            $keyboard[] = [['text' => "✅ I'm done", 'callback_data' => "subgram-op"]];
            
            // Return the block result and the message data
            return ['allowed' => false, 'text' => $text, 'keyboard' => $keyboard];
        }
    }
    
    // error, ok or null -> let through
    return ['allowed' => true];
}


$update = json_decode(file_get_contents('php://input'), true);

if (isset($update['message'])) {
    $result = processSubgramCheck($update['message']['from'], $update['message']['chat']['id']);

    if (!$result['allowed']) {
        sendMessage($update['message']['chat']['id'], $result['text'], json_encode(['inline_keyboard' => $result['keyboard']]));
        exit();
    }

    // Grant access
    sendMessage($update['message']['chat']['id'], "✅ Access granted!");
    // ... your code ...

} elseif (isset($update['callback_query'])) {
    $callback = $update['callback_query'];
    
    if ($callback['data'] === 'subgram-op') {
        // Delete the old message
        deleteMessage($callback['message']['chat']['id'], $callback['message']['message_id']);
        answerCallbackQuery($callback['id'], ['text' => '⏳ Checking subscriptions...']);

        $result = processSubgramCheck($callback['from'], $callback['message']['chat']['id']);

        if (!$result['allowed']) {
            sendMessage($callback['message']['chat']['id'], $result['text'], json_encode(['inline_keyboard' => $result['keyboard']]));
            exit();
        }

        // Grant access
        sendMessage($callback['message']['chat']['id'], "✅ Access granted!");
        // ... your code ...
    }
}
?>
                        

Response description

The server processes your request and returns JSON on which you should build the logic in your bot. The following options are possible:

Response statusHTTP CodeWhat to do
ok200You may let the user through. This means they are either already subscribed to all resources, or no suitable sponsors were found for them (the HTTP response code will be 404 in that case).
warning200Do not let through. The user needs to complete an action (e.g. subscribe to a channel).
error4xx/5xxAn error occurred (invalid API key, server error, etc.). The message will be in the field message.

Structure of an object in the array sponsors

FieldTypeDescription
ads_idStringUnique ID of the advertising campaign.
linkStringReady-to-use link for subscribing/opening.
resource_idStringUnique ID of the resource (channel or bot) on Telegram.
typeStringResource type:
  • channel: Channel or chat.
  • bot: Telegram bot.
  • smart_link: Smart redirect. The sponsor is determined when the user opens the link.
  • resource: External resource (mini app, website) or a channel/bot for which no subscription check is required.
statusString Current subscription status:
  • subscribed: Subscribed.
  • unsubscribed: Not subscribed.
  • notgetted: The user is subscribed to the channel and can be granted access to the content, but they were already in the channel before and unsubscribed. Telegram did not count them as a unique subscriber and did not register the join via the invite link (it is not shown in its statistics). In such cases we do not charge the advertiser and, accordingly, do not pay the bot owner
available_nowBooleanFlag indicating whether the sponsor is currently active. If false, it should not be shown.
button_textStringRecommended text for the button (e.g. "Subscribe").
resource_logoStringURL of the resource's logo. May be an empty string.
resource_nameStringResource name (channel, bot).

Managing bots

POST /bots

A single endpoint for adding, updating and retrieving information about your bots. Authentication is done via your Access Key (Secret Key)passed in the header Auth.
The request body must contain the field actionwhich defines the action.

Action: add — Adding a new bot

Registers a new bot in the system and returns its API key.

ParameterTypeRequiredDescription
actionStringYesAlways "add".
bot_tokenStringConditional*The bot token from @BotFather.
bot_idIntegerConditional*Bot ID. Required if there is no bot_token.
bot_nameStringConditional*Bot name. Required if there is no bot_token.
bot_nicknameStringConditional*Bot username (without @). Required if there is no bot_token.
*You must provide either bot_token, either the set bot_id, bot_name, bot_nickname.
time_purgeIntegerNoTime (in minutes) for which the sponsor list selected for a user will be cached. Default: 180. (5-4320).
max_sponsorsIntegerNoNumber of sponsors the service will return to a user per request. Default: 4. (1-10).
get_linksIntegerNoReceive links only (1) or let SubGram send the mandatory-subscription block itself (0). Default: 0.
use_smart_linkIntegerNoUse smart redirect (1) or not (0). Default: 1.
show_quizIntegerNoShow the Web App form for collecting gender/age (1) or not (0). Default: 0.
redirect_open_typeStringNoHow the redirect/form link opens: "url" (in the browser) or "miniapp" (inside Telegram). Default: "url".
show_botsIntegerNoWhether to show bots among sponsors (1 - yes, 0 - no). Default: 1.
show_resourcesIntegerNoWhether to show external resources (mini apps/websites) among sponsors (1 - yes, 0 - no). Default: 1.
text_opStringNoCustom text for the mandatory-subscription block. Pass null to reset to the default text.
image_opStringNoImage URL for the mandatory-subscription block. null to remove the image.
forbidden_themesArray[String]NoArray of advertising category codes to exclude.
The current list of codes can be obtained from the endpoint /filters (in the object filters.bots.forbidden_themes).
Request example (add)
{
                      "action": "add",
                      "bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
                      "max_sponsors": 3,
                      "text_op": "To access exclusive content, subscribe:",
                      "forbidden_themes": ["crypto", "games"]
                    }
Response example (add)
{
                      "status": "ok",
                      "message": "Bot added successfully.",
                      "result": { "api_key": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" }
                    }

Action: update — Updating a bot

Changes the settings of an existing bot.

ParameterTypeRequiredDescription
actionStringYesAlways "update".
bot_idIntegerYesID of the bot whose settings need to be changed.
is_onIntegerNoEnable (1) or disable (0) the bot in the SubGram system.
All other parameters from the action add are optional. Pass only those you want to change.
Request example (update)
{
                      "action": "update",
                      "bot_id": 123456789,
                      "is_on": 0,
                      "time_purge": 60
                    }

Action: info — Retrieving information

Returns full information and settings for the specified bot.

ParameterTypeRequiredDescription
actionStringYesAlways "info".
bot_idIntegerYesID of the bot whose information you want to get.
Response example (info)
{
                        "status": "ok",
                        "message": "Bot information retrieved.",
                        "result": {
                            "bot_id": 123456789,
                            "bot_name": "My Super Bot",
                            "bot_nickname": "MySuperBot",
                            "status": "active",
                            "note": null,
                            "is_on": 1,
                            "profit": 12345.67,
                            "profit_own_orders": 123.45,
                            "api_key": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
                            "time_purge": 180,
                            "max_sponsors": 4,
                            "get_links": 0,
                            "show_quiz": 1,
                            "text_op": "To access exclusive content, subscribe:",
                            "image_op": "https://api.subgram.org/image-op/some-image.jpg",
                            "forbidden_themes": ["crypto", "games"]
                        }
                    }

Action: list — Bot list

Returns a paginated list of your bots. In the response — an array in response.bots.

ParameterTypeRequiredDescription
actionStringYesAlways "list".
offsetIntegerNoPagination offset. Default: 0.
limitIntegerNoNumber of bots per page (1–100). Default: 15.
in_archiveIntegerNoShow archived bots (1) or active (0). Default: 0.
search_queryStringNoSearch by bot name or username.

Action: change_api_key — Change the bot's API key

Generates a new Bot Key (API Key). The old key stops working immediately — update it in all integrations. The new key comes in response.new_api_key.

ParameterTypeRequiredDescription
actionStringYesAlways "change_api_key".
bot_idIntegerYesID of the bot whose key is being changed.

Checking subscriptions

POST /get-user-subscriptions

Lets you check a specific user's subscription status for one or several resources. Authentication via bot's API key.

Request parameters (Body, JSON)

ParameterTypeRequiredDescription
user_idIntegerYesTelegram user ID whose subscriptions need to be checked.
linksArray[String]NoArray of links whose subscription status needs to be checked.
ads_idsArray[Int]NoArray of ads_id (order IDs) whose subscription status needs to be checked
Attention! With "Smart redirect" this filter will work incorrectly, since ads_id = 0 is assigned until the user visits the web page
start_dateStringNoStart date for the selection in the format YYYY-MM-DD.
end_dateStringNoEnd date for the selection in the format YYYY-MM-DD.
Note: If you pass neither links, nor the dates, the method will return all of the user's subscriptions for the last 30 days.

Request example

{
                      "user_id": 123456789,
                      "links": [
                        "https://t.me/+AbCdEfGhIjKlMnOp",
                        "https://t.me/some_bot"
                      ]
                    }

Example of a successful response (HTTP 200)

{
                      "status": "ok",
                      "code": 200,
                      "message": "Subscription information retrieved",
                      "additional": {
                        "sponsors": [
                          {
                            "ads_id": "123",
                            "link": "https://t.me/+AbCdEfGhIjKlMnOp",
                            "resource_id": "-100123456789",
                            "type": "channel",
                            "status": "subscribed",
                            "available_now": true,
                            "button_text": "Subscribe",
                            "resource_logo": "https://img.subgram.ru/...",
                            "resource_name": "Channel Name"
                          },
                          {
                            "ads_id": "124",
                            "link": "https://t.me/some_bot?start=ref",
                            "resource_id": "987654321",
                            "type": "bot",
                            "status": "unsubscribed",
                            "available_now": true,
                            "button_text": "Launch the bot",
                            "resource_logo": "",
                            "resource_name": "Some bot"
                          }
                        ]
                      }
                    }

User information

POST /get-user-info

Lets you get detailed data about a user (geo, device, demographics) collected by the system.
Authentication is done via bot's API key. The method returns data only for users interacting with the specific bot the API-key request comes from.

Request parameters (Body, JSON)

ParameterTypeRequiredDescription
user_idIntegerYesTelegram user ID.

Request example

curl -X POST https://api.subgram.org/get-user-info \
-H "Auth: YOUR_BOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
    "user_id": 6487476635
}'

Response example (HTTP 200)

{
    "status": "ok",
    "code": 200,
    "data": {
        "first_name": "John",
        "last_name": "Luchkov",
        "username": "lyckow",
        "lang_code": "ru",
        "age_category": 6,
        "age_category_info": "> 25",
        "gender": "male",
        "country": "Russia",
        "city": "Moscow",
        "device_type": "desktop",
        "device_os": "Mac OS",
        "ip_address": "80.190.82.244",
        "is_suspicious": false
    }
}

Response field descriptions (data)

FieldTypeDescription
first_nameStringThe user's first name.
last_nameStringThe user's last name.
usernameStringUsername (without @).
lang_codeStringTelegram app language code (e.g. "ru", "en").
genderStringThe user's gender: male, female.
age_categoryInteger Age category ID:
  • 0: Could not be determined
  • 1: < 10 years
  • 2: 11-13 years
  • 3: 14-15 years
  • 4: 16-17 years
  • 5: 18-25 years
  • 6: > 25 years
age_category_infoStringText label of the age category (e.g. "> 25").
countryStringThe user's country.
cityStringThe user's city.
device_typeStringDevice type:
  • mobile
  • desktop
  • tablet
device_osStringOperating system:
  • Android
  • iOS
  • Mac OS
  • Windows
ip_addressStringThe user's IP address.
is_suspiciousBooleantrue — the system suspects a multi-account (fraud), false — the user is clean.

Advertiser API

Methods for advertisers to manage campaigns.

Managing orders

POST /orders

A single endpoint for creating, updating and retrieving information about your orders (advertising campaigns). Authentication is done via your Access Key (Secret Key)passed in the header Auth.
The request body must contain the field actionwhich defines the action.

Action: create — Creating an order

Creates a new advertising campaign and sends it for moderation.

ParameterTypeRequiredDescription
actionStringYesAlways "create".
linkStringYesFull link to the resource (https://t.me/...).
ads_typeStringYesOrder type: channel, bot or resource.
nameStringNoOrder name. Default: "New order".
is_onIntegerNoWhether to start the order right after moderation (1) or leave it paused (0). Default: 1.
quantity_allIntegerYesTotal desired number of subscribers (10-10,000,000).
quantity_dayIntegerNoDaily subscriber limit. By default equals quantity_all.
priceFloatNoBase price for a regular subscriber (before coefficients are applied). If not specified, the standard one will be set.
price_premiumFloatNoBase price for a premium subscriber. Applied only if user_parameters specifies only_premium: 1.
bot_tokenStringConditional*Bot token or code from BotMembers. *Required if ads_type = bot.
to_bot_memberIntegerNoToken type for the bot: 0 = standard, 1 = code from BotMembers. Default: 0.
track_unsubscriptionsBooleanNoTrack unsubscribes (true) or not (false). Applicable only for ads_type = channel. Default: true.
is_liteIntegerNoActivate smooth distribution of joins throughout the day (1) or get subscribers as fast as possible (0). Default: 0.
sub_speedIntegerNoJoins per hour. Only meaningful if is_lite: 1.
placesIntegerNoOrder placement spots.
  • 0 - In your own bots
  • 1 - In other bots
  • 2 - In your own and other bots
Default: 2.
priorityIntegerNoOrder priority for placements in your own bots
  • 0 - Standard
  • 1 - High (always in the top positions of the mandatory-subscription block)
Default: 0.
is_freeIntegerNoChoice of how the order is placed in your own bots
  • 0 - Paid (the service fee is charged)
  • 1 - Free
Default: 0.
user_parametersObjectNoObject with targeting parameters. See the field descriptions in the table below.
forbidden_themesArray[String]NoArray of bot category codes to exclude. The codes can be obtained from /filters.
order_scheduleObjectNoObject for configuring the display schedule. See the field descriptions below.
Object structure user_parameters
ParameterTypeDescription and Coefficients
genderStringGender: male, female, all. Default: "all".
only_premiumIntegerPremium targeting: 0 - any, 1 - Premium only, 2 - non-Premium. Default: 0.
languagesArray[Integer]Array of language IDs for targeting. IDs can be obtained from /filters. Default: [1,2,3,4,5] (i.e. Russian, Uzbek, Kazakh, Ukrainian, Belarusian). Pass null to target all languages.
countriesArray[Integer]Array of country IDs. IDs can be obtained from /filters. Price coefficient: x1.5
citiesArray[Integer]Array of city IDs. IDs can be obtained from /filters. Price coefficient: x2
devicestypeArray[String]Array of device types (e.g. ["desktop", "mobile"]). IDs can be obtained from /filters. Price coefficient: x1.5
devicesosArray[Integer]Array of operating system IDs. IDs can be obtained from /filters. Price coefficient: x1.5
agesArray[Integer] Array of age category IDs. The IDs and the % share of each category can be obtained from /filters.
  • Under 10: x1.2
  • 11-13: x1.2
  • 14-15: x1.5
  • 16-17: x1.5
  • 18-24: x2.0
  • 25 and older: x2.5
When several categories are selected, a weighted-average coefficient is applied based on the number of users in each selected category:
Sum(Coefficient of a given category × the share of that category's users out of the total percentage of the selected categories)
has_photoIntegerRequire a profile photo (1 - yes, 0 - doesn't matter). Price coefficient: x1.1
has_real_photoIntegerRequire a real profile photo (1 - yes, 0 - doesn't matter). Price coefficient: x1.4
has_usernameIntegerRequire a username (1 - yes, 0 - doesn't matter). Price coefficient: x1.1
has_bioIntegerRequire a profile bio (1 - yes, 0 - doesn't matter). Price coefficient: x1.1
has_first_nameIntegerRequire a first name (1 - yes, 0 - doesn't matter). Price coefficient: x1.1
has_ru_nameIntegerRequire a Russian-language name (1 - yes, 0 - doesn't matter). Price coefficient: x1.1
has_rus_nameIntegerRequire a Russian name (1 - yes, 0 - doesn't matter). Price coefficient: x1.1
has_only_cyrillicIntegerRequire the name to be Cyrillic-only (1 - yes, 0 - doesn't matter). Price coefficient: x1.1
has_fake_checkIntegerRequire passing the anti-fraud check (1 - yes, 0 - doesn't matter). Price coefficient: x1.1
old_accountIntegerMinimum account age: 1 (≥2 years, x1.1), 2 (≥3 years, x1.2), 3 (≥5 years, x1.3), 4 (≥7 years, x1.4), 5 (≥9 years, x1.5). Default: 0 (any).
giftsIntegerMinimum number of gifts (Telegram Gifts) in the profile: 0 (doesn't matter), 1 (≥1, x1.1), 2 (≥10, x1.2), 3 (≥20, x1.3), 4 (≥50, x1.4), 5 (≥100, x1.5). Default: 0.
ratingIntegerMinimum Telegram profile rating: 0 (doesn't matter), 1 (≥1, x1.1), 2 (≥2, x1.2), 3 (≥5, x1.3), 4 (≥7, x1.4), 5 (≥10, x1.5). Default: 0.
Object structure order_schedule
ParameterTypeDescription
start_datetimeStringDelayed launch. Date and time in the format "YYYY-MM-DD HH:MM:SS".
start_timeStringDaily display start time. Format "HH:MM".
end_timeStringDaily display end time. Format "HH:MM".
excluded_daysArray[Integer]Array of weekdays to exclude, where 1=Monday, ..., 7=Sunday.

Action: update — Updating an order

Changes the parameters of an existing order.

ParameterTypeRequiredDescription
actionStringYesAlways "update".
order_idIntegerYesID of the order to change.
is_onIntegerNoStart (1) or stop (0) the campaign.
in_archiveIntegerNoMove to archive (1) or restore from archive (0).
All other parameters from the action create are also optional. Pass only those you want to change. The link link cannot be changed if the order already has subscribers.

Action: info — Retrieving information

Returns full information about the order, including all targeting settings and the final prices with coefficients.

ParameterTypeRequiredDescription
actionStringYesAlways "info".
order_idIntegerYesOrder ID to get information for.

Action: list — Order list

Returns a paginated list of your orders with brief information and progress.

ParameterTypeRequiredDescription
actionStringYesAlways "list".
offsetIntegerNoPagination offset. Default: 0.
limitIntegerNoNumber of orders per page. Default: 15.
search_queryStringNoSearch by order name or link.
in_archiveIntegerNoShow archived orders (1) or active (0). Default: 0.
statusStringNoFilter by order status (active, completed, moderation, rejected etc.).

Action: duplicate — Duplicate an order

Creates a copy of an existing order with all targeting settings. The new order is created paused (is_on = 0).

ParameterTypeRequiredDescription
actionStringYesAlways "duplicate".
order_idIntegerYesID of the order to duplicate.

Action: report — Generate a report

Starts asynchronous generation of an Excel report for the order and returns task_id to track readiness.

Limit: no more than 1 request per minute per order. Exceeding returns HTTP 429.
ParameterTypeRequiredDescription
actionStringYesAlways "report".
order_idIntegerYesID of the order for the report.
include_priceBooleanNoWhether to include prices in the report. Default: false.
time_rangeStringNoSelection period: today, 3days, 7days, all. Default: all.

Action: check_report — Check report readiness

Checks the report generation status by task_id. When the report is ready, the response contains file_url and file_name.

Limit: no more than 30 requests per minute per user. Exceeding returns HTTP 429.
ParameterTypeRequiredDescription
actionStringYesAlways "check_report".
task_idStringYesTask identifier obtained from report.

Action: download_id — Export subscriber IDs

Generates a txt file with a list of user_id of the order's subscribers and returns file_id. The ready file is downloaded via a GET request to /orders/download_id/{file_id}.

Limit: no more than 1 request per minute per order. Exceeding returns HTTP 429.
ParameterTypeRequiredDescription
actionStringYesAlways "download_id".
order_idIntegerYesID of the order whose subscribers are exported.
time_rangeStringNoSelection period: today, 3days, 7days, all. Default: all.

Examples and Responses

Creation request example (create)
{
                      "action": "create",
                      "link": "https://t.me/durov",
                      "ads_type": "channel",
                      "name": "Test order via API",
                      "quantity_all": 1000,
                      "price": 2.5,
                      "user_parameters": {
                        "gender": "male",
                        "languages": [1, 3],
                        "has_photo": 1
                      },
                      "order_schedule": {
                        "start_time": "09:00",
                        "end_time": "22:00",
                        "excluded_days": [6, 7]
                      }
                    }
Creation response example (create)
{
                      "status": "ok",
                      "code": 200,
                      "message": "Order created successfully and sent for moderation",
                      "response": {
                        "order_id": 12345
                      }
                    }
Get-info response example (info)

The response includes all order parameters, current progress, as well as the final prices and applied coefficients.

FieldTypeDescription
order_idIntegerOrder ID.
statusString Current order status:
  • Moderation — In moderation.
  • Processing — Active and running.
  • Stopped — Stopped manually.
  • Finished — Order completed (the limit was reached quantity_all).
  • Rejected — Rejected by a moderator.
  • Archived — Moved to archive (deleted).
reasonStringRejection reason (only for the status Rejected).
link, name, ......All other parameters passed during creation/update.
quantity_nowIntegerCurrent number of completed subscriptions.
remainsIntegerRemaining number of subscriptions.
old_priceFloatBase price per subscriber that you specified (before coefficients are applied).
real_priceFloatFinal real price per subscriber after all coefficients are applied.
user_parametersObjectObject with the applied targeting settings.
order_scheduleObjectObject with the applied schedule settings.
coefficientsObjectObject listing all applied coefficients and their values. Contains the key total with the final multiplier.
{
                        "status": "ok",
                        "code": 200,
                        "message": "Order information retrieved",
                        "response": {
                            "order_id": 12345,
                            "status": "Processing",
                            "reason": null,
                            "link": "https://t.me/durov",
                            "name": "Test order via API",
                            "ads_type": "channel",
                            "track_unsubscriptions": true,
                            "to_bot_member": null,
                            "quantity_all": 1000,
                            "quantity_day": 1000,
                            "quantity_now": 150,
                            "remains": 850,
                            "is_on": 1,
                            "in_archive": 0,
                            "old_price": 2.5,
                            "real_price": 4.13,
                            "is_lite": 0,
                            "sub_speed": null,
                            "user_parameters": {
                                "languages": [1, 3],
                                "ages": [5, 6],
                                "countries": [],
                                "cities": [101],
                                "devicestype": [],
                                "devicesos": [],
                                "gender": "male",
                                "only_premium": 0,
                                "has_photo": 1,
                                "has_real_photo": 0,
                                "has_username": 0,
                                "has_bio": 0,
                                "has_first_name": 0,
                                "has_ru_name": 0,
                                "has_rus_name": 0,
                                "has_only_cyrillic": 0,
                                "has_fake_check": 0,
                                "old_account": 0
                            },
                            "order_schedule": {
                                "start_datetime": null,
                                "start_time": "09:00:00",
                                "end_time": "22:00:00",
                                "excluded_days": [6, 7]
                            },
                            "coefficients": {
                                "cities": 2.5,
                                "ages": 1.88,
                                "has_photo": 1.1,
                                "total": 3.48
                            }
                        }
                    }

Possible errors

HTTP CodeError code (error_code)Description
400price_too_lowThe specified price or price_premium is below the minimum allowed.
400invalid_parameterAn invalid parameter was passed, for example, track_unsubscriptions for an order of type bot.
403forbidden_or_not_foundThe order was not found or you are not its owner (for update or info).
409update_forbiddenAn attempt to change a field that cannot be changed (e.g. the link in an order that already has subscribers).
409rejected, exist, moderationConflict on creation: the link is already rejected, exists or is in moderation.
422-Data validation error. The field message will contain a detailed description.

General API methods

Endpoints available to different user types.

Getting the balance

POST /get-balance

Returns the account's current balance and a brief summary of all connected bots. Authentication via user token.

This method requires no parameters in the request body.

Example of a successful response (HTTP 200)

{
                      "status": "ok",
                      "code": 200,
                      "message": "Balance and bot information retrieved successfully",
                      "balance": 219.21,
                      "bots_info": [
                        {
                          "bot_id": 12345678,
                          "bot_username": "my_super_bot",
                          "total_followers": 60667,
                          "revenue": 120542.45
                        }
                      ]
                    }

Getting filters

GET /filters

Returns the full list of all available values for configuring targeting in orders (Advertiser API) and for excluding topics in bot settings (Publisher API).

Note: This endpoint is public and requires no authentication.

Request parameters

This method requires no parameters in the request body or in the URL.

Request example (cURL)

curl -X GET https://api.subgram.org/filters

Response example (HTTP 200)

{
                      "filters": {
                        "ads": {
                          "countries": [
                            { "id": 1, "name": "Russia", "percentage": 75.4 },
                            { "id": 2, "name": "Ukraine", "percentage": 8.1 }
                          ],
                          "languages": [
                            { "id": 1, "name": "Russian", "percentage": 89.2 },
                            { "id": 2, "name": "Ukrainian", "percentage": 5.5 }
                          ],
                          "cities": [
                            { "id": 101, "name": "Moscow", "percentage": 25.0 },
                            { "id": 102, "name": "Saint Petersburg", "percentage": 12.3 }
                          ],
                          "ages": [
                            { "id": 5, "name": "18 - 24", "percentage": 40.5 },
                            { "id": 6, "name": "25 and older", "percentage": 30.1 }
                          ],
                          "devicestype": [
                            { "id": "mobile", "name": "Smartphone/Tablet", "percentage": 92.3 },
                            { "id": "desktop", "name": "Desktop/Laptop", "percentage": 7.7 }
                          ],
                          "devicesos": [
                            { "id": 1, "name": "Android", "percentage": 65.4 },
                            { "id": 2, "name": "iOS", "percentage": 25.1 }
                          ],
                          "forbidden_themes": [
                            { "id": "stars", "name": "Stars/NFT", "percentage": 15.8 },
                            { "id": "adult", "name": "18+", "percentage": 10.2 }
                          ]
                        },
                        "bots": {
                          "forbidden_themes": [
                            { "id": "news", "name": "News", "percentage": 5.3 },
                            { "id": "useful", "name": "Useful", "percentage": 12.1 }
                          ]
                        }
                      }
                    }

Response field descriptions

FieldTypeDescription
filtersObjectRoot object containing all filters.
filters.adsObjectObject with filters applicable to orders (Advertiser API).
filters.ads.*Array[Object]Arrays of objects for each filter type (countries, languages, ages etc.).
filters.botsObjectObject with filters applicable to bot settings (Publisher API).
filters.bots.forbidden_themesArray[Object]Array of advertising topics that can be blocked from showing in your bots.
Structure of an object in the arrays
idInteger/StringUnique identifier of the value (e.g. a country ID or a category code) to use when creating/updating orders and bots.
nameStringHuman-readable name of the value (e.g. "Russia" or "Cryptocurrency").
percentageFloatApproximate share of this parameter among all users in the SubGram system. Helps estimate how much the filter will narrow the audience.

Getting statistics

GET /statistic

A universal endpoint for getting statistics both on income (for bot owners) and on spending (for advertisers). Authentication is done via user tokenpassed in the query parameter api_token.

Request parameters (Query)

ParameterTypeRequiredDescription
api_tokenStringYesYour main user token.
actionStringYes Type of statistics requested:
  • For advertisers: allads, ads, source.
  • For bot owners: allbots, bots, sponsor.
ads_idIntegerNoOrder ID (required for action=ads and action=source).
bot_idIntegerNoBot ID (required for action=bots and action=sponsor for a single bot).
start_dateStringNoStart date (YYYY-MM-DD). Default: 9 days ago.
end_dateStringNoEnd date (YYYY-MM-DD). Default: today.
output_formatStringNoResponse format: html (for a web page) or json (for the API).

Request URL example

https://api.subgram.org/statistic?api_token=YOUR_TOKEN&action=allads&output_format=json

Response field descriptions (for output_format=json)

The response contains the object data, whose structure depends on the parameter action.

FieldTypeDescription
Common fields (for action=ads/allads/bots/allbots)
data.labelsArray[String]Array of labels (dates) for the X axis on the chart.
data.subscribers_dataArray[Integer]Array of subscription-count data for each date.
data.value_dataArray[Float]Array of spending/income data for each date.
data.avg_price_dataArray[Float]Array of average subscriber-price data for each date.
data.total_subscribersIntegerTotal number of subscriptions for the period.
data.total_valueFloatTotal spending/income for the period.
Fields for action=source / sponsor
data.table_dataArray[Object]Array of objects for building the table. The structure depends on action:
• For source: bot_id, bot_nickname, subscribers, value, is_excluded.
• For sponsor: link, ads_id, subscribers, is_excluded.
Additional fields for action=bots/allbots
data.show_extended_tableBooleantrue, if the bot had subscriptions from "its own" orders, which activates the extended table.
data.table_dataArray[Object]If show_extended_table equals true, the objects in the array will contain a detailed breakdown: service_subs, service_value, own_subs, own_value etc.
data.requests_statsObjectObject with statistics on API requests to your bot. Includes total_requests, successful_requests and others.
Additional fields for action=ads
data.language_statsArray[Object]Array of objects with statistics on the languages of the acquired users. Example: {"lang": "ru", "percentage": "85.4%"}.

Managing exclusions

POST /toggle-exclusion

A universal endpoint for managing "blacklists". Lets advertisers exclude unwanted traffic sources (bots), and bot owners exclude unwanted sponsors.

Authentication

This method uses authentication via user tokenwhich must be passed as a query parameter.

POST /toggle-exclusion?api_token=YOUR_MAIN_API_TOKEN

Request parameters (Body, JSON)

ParameterTypeRequiredDescription
actionStringYesThe action to perform:
  • exclude: Add to the blacklist.
  • activate: Remove from the blacklist.
contextStringYes Defines the operating logic. The key parameter:
  • advertiser: Advertiser mode. You exclude a bot from being shown for your order.
  • publisher: Bot-owner mode. You exclude a sponsor from being shown in your bots.
ads_idIntegerYesOrder ID. Depending on the context this is either your order that you are configuring, or the sponsor's order that you are blocking.
bot_idIntegerConditional*Bot ID. *Required if context equals advertiser. For publisher it is optional (see the examples).

Use cases and examples

Example 1: An advertiser excludes a bot from their campaign

You (the advertiser) want your order with ID 12345 to never again be shown in the bot with ID 98765.

{
                      "action": "exclude",
                      "context": "advertiser",
                      "ads_id": 12345,
                      "bot_id": 98765
                    }
Example 2: A bot owner excludes a sponsor (locally)

You (the bot owner) want the sponsor with order ID 54321 to no longer be shown in your specific bot with ID 888777.

{
                      "action": "exclude",
                      "context": "publisher",
                      "ads_id": 54321,
                      "bot_id": 888777
                    }
Example 3: A bot owner excludes a sponsor (globally)

You (the bot owner) want the sponsor with order ID 54321 to no longer be shown in any of your bots.

For a global exclusion, simply do not pass the parameter bot_id.
{
                      "action": "exclude",
                      "context": "publisher",
                      "ads_id": 54321
                    }

Responses

Example of a successful response (HTTP 200)
{
                      "status": "ok",
                      "message": "Resource excluded successfully."
                    }

On success you will get a simple confirmation. On errors (invalid token, no rights to manage the resource) a standard error response with the corresponding HTTP code (401, 403, 404) will be returned.

Webhooks

Get real-time notifications about subscribe and unsubscribe events.

What are Webhooks?

Webhooks let your server receive instant POST notifications from SubGram about events related to users' subscriptions. This removes the need to constantly poll our API with the /get-user-subscriptions method to check the status.

As soon as a user subscribes to or unsubscribes from a resource via a link issued to your bot, SubGram sends a request to your URL with the details of that event.

Step 1: Setup

Webhook reception is configured in the official SubGram bot: @subgram_officialbot.

  1. Open the bot and go to /start → Profile.
  2. Click the "Webhooks" button.
  3. The bot will ask you for your server's URL (endpoint) where SubGram will send POST requests. Provide a valid, publicly accessible URL (be sure to include https://).
  4. Next, the bot will offer you a choice of which event types you want to receive:
    • Subscriptions only (statuses subscribed, notgetted)
    • Unsubscribes only (status unsubscribed)
    • Subscriptions and unsubscribes (all status types)
Important: Your server must be ready to accept POST requests at the specified URL and must be reachable from the Internet.

Step 2: Receiving and handling the request

SubGram will periodically (every few seconds) send POST requests to your URL if new events have occurred since the last send.

Request authentication

To verify that the request really came from SubGram and relates to your bot, we pass the API key of your bot in the HTTP header Api-Key. You should always check this header on your server.

POST /your-webhook-endpoint HTTP/1.1
        "Content-Type": "application/json",
        "Api-Key": "YOUR_BOT_API_KEY",
        ...

        { ... request body ... }

Request body (Payload)

The request body contains a JSON object with the key webhooks, whose value is an array of objects. Each object in the array represents a single event.

{
          "webhooks": [
            {
              "webhook_id": 2757,
              "ads_id": 123,
              "link": "https://t.me/+5hritdfa...",
              "user_id": 5440481282,
              "bot_id": 5811111111,
              "status": "subscribed",
              "subscribe_date": "2025-04-14"
            },
            {
              "webhook_id": 2763,
              "ads_id": 124,
              "link": "https://t.me/+-gZ5RJI...",
              "user_id": 949444369,
              "bot_id": 5811111111,
              "status": "unsubscribed",
              "subscribe_date": "2025-04-14"
            }
          ]
        }

Step 3: Interpreting the data

Each object in the array webhooks contains the following fields:

FieldTypeDescription
webhook_idIntegerUnique event ID. A larger value means a later event. Helps determine the order if a user quickly subscribed and unsubscribed.
ads_idIntegerID of the advertising order in the SubGram system.
linkStringThe link through which the interaction happened.
user_idIntegerTelegram ID of the user the event is for.
bot_idIntegerID of your Telegram bot the event relates to.
statusString Current subscription status:
  • subscribed: The user subscribed successfully and the subscription was counted.
  • notgetted: The user is subscribed to the resource, but the subscription was not counted for this link (they were subscribed earlier).
  • unsubscribed: The user unsubscribed from the resource.
subscribe_dateStringDate (YYYY-MM-DD) when the user first subscribed to the resource. This date does not change on unsubscribe or re-subscribe.

Step 4: Handling recommendations

  • Respond quickly: Your server should respond to the webhook request as fast as possible with the HTTP status 200 OKto confirm receipt. Long processing (e.g. updating a database) is better done asynchronously after sending the response.
  • Check the Api-Key: Always check the value of the header Api-Keyto make sure the request is authentic and to know which of your bots the event came from.
  • Process the array: Remember that a single request may contain an array of several events in the field webhooks. Your handler should loop through all elements of the array.
  • Use webhook_id: If the exact order of events (subscribe/unsubscribe) matters to you, use webhook_id to sort them or to prevent processing stale events that may have arrived with a delay.
  • Logging: It is recommended to log all incoming webhooks for debugging and analysis in case problems arise.