Back home
API Reference

SDK Integration

One method. Pass the LLM response — get it back with a native ad injected and the keyboard merged. SDK handles errors, timeouts, and Telegram limits automatically.

Install

pip
pip install sidekick-ads
# with framework extras:
pip install sidekick-ads[aiogram] # aiogram 3.x
pip install sidekick-ads[ptb] # python-telegram-bot 20+

Quick start

Initialize with your API key and platform ID (from your platforms dashboard), then call inject() on every LLM response.

from sidekick_ads import Sidekick
sk = Sidekick(
api_key="sk_live_xxxxx",
platform_id="plt_xxxxx",
)
# in your handler:
result = await sk.inject(
user_id=message.from_user.id,
message=llm_reply,
language_code=message.from_user.language_code,
is_premium=message.from_user.is_premium,
keyboard=your_existing_keyboard, # optional
parse_mode="HTML", # optional — matches your bot's parse_mode
)
await message.answer(result.message, reply_markup=result.keyboard, parse_mode="HTML")

Constructor

ParameterTypeDescription
api_key / apiKeyrequiredstringYour API key (sk_live_…). Generate in settings. settings
platform_id / platformIdrequiredstringPlatform public ID (plt_…). platforms dashboard
base_url / baseUrlstringAPI base URL. Default: "https://sidekick-ads.com".
timeoutnumberRequest timeout in seconds. Default: 3.0.
platformstringSurface type. Default: "telegram".

inject()

The only method you call. Async, never throws — on any error returns the original message and keyboard unchanged.

ParameterTypeDescription
user_id / userIdrequirednumberTelegram user ID (ctx.from.id). Telegram User docs
messagerequiredstringLLM response text to inject an ad into.
language_code / languageCodestring?IETF language tag. SDK sends "" if null/None. Telegram User docs
is_premium / isPremiumbool?Telegram Premium status. SDK sends false if null/None. Telegram User docs
keyboardInlineKeyboardMarkup?Your existing keyboard. SDK merges the ad button into it.
parse_mode / parseModestring?"HTML", "MarkdownV2", or "Markdown". Server escapes and formats the ad CTA for the given parse mode.
ad_button_position / adButtonPosition"bottom" | "top"Where to add the ad button row. Default: "bottom".

Return value

FieldTypeDescription
messagestringReady-to-send text — original or with ad CTA appended.
has_ad / hasAdboolWhether an ad was injected.
impression_id / impressionIdstring?Impression ID for tracking.
adobject?Ad object with text, button_text, button_url.
keyboardInlineKeyboardMarkup?Merged keyboard (your buttons + ad button), or original if no ad.

fetch()

Fetch an ad object without sending your message text. Returns the ad as separate fields that you render into your bot's reply yourself. Async, never throws — on any error returns { hasAd: false } (or { has_ad: False } in Python).

fetch.py
from sidekick_ads import Sidekick
sk = Sidekick(api_key="sk_live_xxx", platform_id="plt_xxx")
result = await sk.fetch(
user_id=ctx.from_user.id,
language_code=ctx.from_user.language_code or "en",
parse_mode="HTML", # optional
)
if result.has_ad and result.ad is not None:
await message.answer(
f"{llm_reply}\n\n{result.ad.ad_text_formatted}",
parse_mode="HTML",
reply_markup={"inline_keyboard": [[
{"text": result.ad.button_text, "url": result.ad.button_url}
]]},
)
else:
await message.answer(llm_reply)

Parameters

ParameterTypeDescription
user_id / userIdrequirednumberTelegram user ID (ctx.from.id). Telegram User docs
language_code / languageCoderequiredstringIETF language tag from the Telegram User object. Telegram User docs
is_premium / isPremiumbool?Telegram Premium status. Default: false.
parse_mode / parseModestring?"HTML", "MarkdownV2", or "Markdown". When set, response includes a pre-formatted ad_text_formatted field.
accept_formats / acceptFormatsstring[]?Ad formats this caller renders. Default ["text"] (text-only, legacy behavior). Pass ["text", "image"] to receive image ads too — the SDK will pre-download the asset bytes into ad.image_data / ad.imageData so you can hand them straight to InputFile.

Return value (FetchResult)

FieldTypeDescription
has_ad / hasAdboolWhether an ad was returned.
impression_id / impressionIdstring?Impression ID (tracks the impression). Null when has_ad is false.
adobject?Null when has_ad is false. Otherwise an object with the fields below.
ad.ad_text / ad.adTextstringRaw campaign text.
ad.ad_url / ad.adUrlstringClick URL.
ad.button_text / ad.buttonTextstringInline button label.
ad.button_url / ad.buttonUrlstringInline button URL (click tracking).
ad.ad_text_formatted / ad.adTextFormattedstring?Present only if parse_mode was in the request. Escaped text with the click URL embedded per parse mode.

fetch() vs inject() — use inject() if you want the SDK to handle message splicing and keyboard merging for you. Use fetch() if your bot serves LLM text that may contain user data (personal names, query fragments) and you don't want to send that text to Sidekick's servers.

fetch_image()

Fetch an image ad (photo or animation) to display separately from your LLM reply. Calls the strict image-only /api/v1/ad/fetch-image endpoint. Async, never throws — on any error returns { has_ad: False }.

fetch_image.py
from sidekick_ads import Sidekick
from aiogram.types import (
InlineKeyboardMarkup, InlineKeyboardButton, BufferedInputFile,
)
sk = Sidekick(api_key=..., platform_id=...)
result = await sk.fetch_image(
user_id=message.from_user.id,
language_code="en",
parse_mode="HTML",
)
if result.has_ad and result.ad:
ad = result.ad
# SDK already downloaded the bytes — hand straight to InputFile.
media = BufferedInputFile(ad.image_data, filename=ad.image_filename)
kb = InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(text=ad.button_text, url=ad.button_url)
]])
caption = ad.ad_text_formatted or ad.ad_text
if ad.media_type == "animation":
await message.answer_animation(media, caption=caption,
parse_mode="HTML", reply_markup=kb)
else:
await message.answer_photo(media, caption=caption,
parse_mode="HTML", reply_markup=kb)

Parameters

ParameterTypeDescription
user_id / userIdrequirednumberTelegram user ID (ctx.from.id). Telegram User docs
language_code / languageCoderequiredstringIETF language tag from the Telegram User object. Telegram User docs
is_premium / isPremiumbool?Telegram Premium status. Default: false.
parse_mode / parseModestring?"HTML", "MarkdownV2", or "Markdown". When set, response includes a pre-formatted ad_text_formatted field — the caption wrapped in an anchor pointing at the click URL.

Return value (FetchImageResult)

FieldTypeDescription
has_ad / hasAdboolWhether an image ad was returned.
impression_id / impressionIdstring?Impression ID. Null when has_ad is false.
adobject?Null when has_ad is false. Otherwise an object with the fields below.
ad.format"image"Always "image" for this endpoint.
ad.ad_text / ad.adTextstringPlain-text caption for the Telegram message.
ad.ad_text_formatted / ad.adTextFormattedstring?Present only if parse_mode was in the request. The caption wrapped in an anchor tag pointing at the click URL — pass this as the Telegram caption to make the text tappable.
ad.ad_url / ad.adUrlstringClick-tracking URL (same as button_url).
ad.button_text / ad.buttonTextstringInline button label.
ad.button_url / ad.buttonUrlstringInline button URL (click tracking).
ad.media_type / ad.mediaType"photo" | "animation"Telegram media method to use (sendPhoto or sendAnimation).
ad.image_url / ad.imageUrlstringDirect URL to the image or GIF. You usually don't need to fetch this yourself — see image_data below.
ad.image_mime / ad.imageMimestringMIME type, e.g. "image/jpeg", "image/gif", "video/mp4".
ad.image_data / ad.imageDatabytes / Uint8ArrayPre-downloaded asset bytes — hand straight to aiogram BufferedInputFile or grammy InputFile. The SDK fetches these for you so you don't need to download the image yourself, and it works even if Telegram's URL fetcher can't reach the asset host.
ad.image_filename / ad.imageFileNamestringSuggested filename derived from the MIME type, e.g. "ad.jpg".

Framework examples

Copy-paste examples for every supported framework. Each one is a complete, working handler.

aiogram 3.x — with middleware

bot.py
from aiogram import Bot, Dispatcher, types
from sidekick_ads import Sidekick
from sidekick_ads.middleware import SidekickMiddleware
bot = Bot(token="BOT_TOKEN")
dp = Dispatcher()
# Register middleware — injects `sidekick` into every handler
sidekick_middleware = SidekickMiddleware(
api_key="sk_live_xxxxx",
platform_id="plt_xxxxx",
)
dp.message.middleware(sidekick_middleware)
# Close the HTTP pool cleanly on dispatcher shutdown
dp.shutdown.register(sidekick_middleware.aclose)
@dp.message()
async def handle(message: types.Message, sidekick: Sidekick):
llm_reply = await get_llm_response(message.text)
# Your existing keyboard (if any)
kb = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="👍", callback_data="like"),
types.InlineKeyboardButton(text="👎", callback_data="dislike")],
])
result = await sidekick.inject(
user_id=message.from_user.id,
message=llm_reply,
language_code=message.from_user.language_code,
is_premium=message.from_user.is_premium,
keyboard=kb,
)
await message.answer(result.message, reply_markup=result.keyboard)

aiogram 3.x — without middleware

bot.py
from aiogram import Bot, Dispatcher, types
from sidekick_ads import Sidekick
bot = Bot(token="BOT_TOKEN")
dp = Dispatcher()
sk = Sidekick(api_key="sk_live_xxxxx", platform_id="plt_xxxxx")
@dp.message()
async def handle(message: types.Message):
llm_reply = await get_llm_response(message.text)
result = await sk.inject(
user_id=message.from_user.id,
message=llm_reply,
language_code=message.from_user.language_code,
is_premium=message.from_user.is_premium,
)
await message.answer(result.message, reply_markup=result.keyboard)

python-telegram-bot v20+

bot.py
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ApplicationBuilder, MessageHandler, ContextTypes, filters
from sidekick_ads import Sidekick
sk = Sidekick(api_key="sk_live_xxxxx", platform_id="plt_xxxxx")
async def handle(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.message.from_user
llm_reply = await get_llm_response(update.message.text)
kb = InlineKeyboardMarkup([
[InlineKeyboardButton("👍", callback_data="like"),
InlineKeyboardButton("👎", callback_data="dislike")],
])
result = await sk.inject(
user_id=user.id,
message=llm_reply,
language_code=user.language_code,
is_premium=user.is_premium,
keyboard=kb,
)
await update.message.reply_text(result.message, reply_markup=result.keyboard)
app = ApplicationBuilder().token("BOT_TOKEN").build()
app.add_handler(MessageHandler(filters.TEXT, handle))
app.run_polling()

Raw usage (no framework)

main.py
import httpx
from sidekick_ads import Sidekick
sk = Sidekick(api_key="sk_live_xxxxx", platform_id="plt_xxxxx")
async def on_message(user_id: int, text: str, lang_code: str | None, premium: bool | None):
llm_reply = await get_llm_response(text)
result = await sk.inject(
user_id=user_id,
message=llm_reply,
language_code=lang_code,
is_premium=premium,
)
# Send via any HTTP client
async with httpx.AsyncClient() as client:
await client.post(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
json={
"chat_id": user_id,
"text": result.message,
**({"reply_markup": result.keyboard} if result.keyboard else {}),
},
)

Error handling

inject() never throws. On any error — timeout, network failure, 5xx, malformed JSON — it returns your original message and keyboard unchanged. Your bot keeps working no matter what.

What happens on error
# Sidekick is down? Bot still works.
result = await sk.inject(user_id=123, message="Hello world")
# result.message == "Hello world" (unchanged)
# result.has_ad == False
# result.keyboard == None (or your original keyboard)

Keyboard merging

  • Ad button is always added as a separate row — never mixed with your buttons.
  • Position controlled by ad_button_position: "bottom" (default) or "top".
  • Telegram allows max 13 keyboard rows. If you already have 13 — ad button is skipped, text CTA only.
  • Your existing buttons are never modified or removed.
  • If message + ad exceeds 4096 chars, text CTA is dropped but button is still added.

Notes

  • Impressions are tracked automatically. When an ad is returned, the server already recorded the impression. No extra call needed.
  • Clicks go through the redirect. The button URL handles click tracking and redirects to the advertiser. Just use it as-is.
  • Connection pooling. Python SDK reuses an httpx.AsyncClient across calls. Node.js uses the built-in fetch agent.
  • Timeout default is 3 seconds. If the API doesn't respond in time, your bot gets the original message back instantly.