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 install sidekick-ads# with framework extras:pip install sidekick-ads[aiogram] # aiogram 3.xpip 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 Sidekicksk = 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, # optionalparse_mode="HTML", # optional — matches your bot's parse_mode)await message.answer(result.message, reply_markup=result.keyboard, parse_mode="HTML")
Constructor
| Parameter | Type | Description |
|---|---|---|
api_key / apiKeyrequired | string | Your API key (sk_live_…). Generate in settings. settings → |
platform_id / platformIdrequired | string | Platform public ID (plt_…). platforms dashboard → |
base_url / baseUrl | string | API base URL. Default: "https://sidekick-ads.com". |
timeout | number | Request timeout in seconds. Default: 3.0. |
platform | string | Surface type. Default: "telegram". |
inject()
The only method you call. Async, never throws — on any error returns the original message and keyboard unchanged.
| Parameter | Type | Description |
|---|---|---|
user_id / userIdrequired | number | Telegram user ID (ctx.from.id). Telegram User docs → |
messagerequired | string | LLM response text to inject an ad into. |
language_code / languageCode | string? | IETF language tag. SDK sends "" if null/None. Telegram User docs → |
is_premium / isPremium | bool? | Telegram Premium status. SDK sends false if null/None. Telegram User docs → |
keyboard | InlineKeyboardMarkup? | Your existing keyboard. SDK merges the ad button into it. |
parse_mode / parseMode | string? | "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
| Field | Type | Description |
|---|---|---|
message | string | Ready-to-send text — original or with ad CTA appended. |
has_ad / hasAd | bool | Whether an ad was injected. |
impression_id / impressionId | string? | Impression ID for tracking. |
ad | object? | Ad object with text, button_text, button_url. |
keyboard | InlineKeyboardMarkup? | 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).
from sidekick_ads import Sidekicksk = 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
| Parameter | Type | Description |
|---|---|---|
user_id / userIdrequired | number | Telegram user ID (ctx.from.id). Telegram User docs → |
language_code / languageCoderequired | string | IETF language tag from the Telegram User object. Telegram User docs → |
is_premium / isPremium | bool? | Telegram Premium status. Default: false. |
parse_mode / parseMode | string? | "HTML", "MarkdownV2", or "Markdown". When set, response includes a pre-formatted ad_text_formatted field. |
accept_formats / acceptFormats | string[]? | 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)
| Field | Type | Description |
|---|---|---|
has_ad / hasAd | bool | Whether an ad was returned. |
impression_id / impressionId | string? | Impression ID (tracks the impression). Null when has_ad is false. |
ad | object? | Null when has_ad is false. Otherwise an object with the fields below. |
ad.ad_text / ad.adText | string | Raw campaign text. |
ad.ad_url / ad.adUrl | string | Click URL. |
ad.button_text / ad.buttonText | string | Inline button label. |
ad.button_url / ad.buttonUrl | string | Inline button URL (click tracking). |
ad.ad_text_formatted / ad.adTextFormatted | string? | 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 }.
from sidekick_ads import Sidekickfrom 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_textif 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
| Parameter | Type | Description |
|---|---|---|
user_id / userIdrequired | number | Telegram user ID (ctx.from.id). Telegram User docs → |
language_code / languageCoderequired | string | IETF language tag from the Telegram User object. Telegram User docs → |
is_premium / isPremium | bool? | Telegram Premium status. Default: false. |
parse_mode / parseMode | string? | "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)
| Field | Type | Description |
|---|---|---|
has_ad / hasAd | bool | Whether an image ad was returned. |
impression_id / impressionId | string? | Impression ID. Null when has_ad is false. |
ad | object? | 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.adText | string | Plain-text caption for the Telegram message. |
ad.ad_text_formatted / ad.adTextFormatted | string? | 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.adUrl | string | Click-tracking URL (same as button_url). |
ad.button_text / ad.buttonText | string | Inline button label. |
ad.button_url / ad.buttonUrl | string | Inline button URL (click tracking). |
ad.media_type / ad.mediaType | "photo" | "animation" | Telegram media method to use (sendPhoto or sendAnimation). |
ad.image_url / ad.imageUrl | string | Direct URL to the image or GIF. You usually don't need to fetch this yourself — see image_data below. |
ad.image_mime / ad.imageMime | string | MIME type, e.g. "image/jpeg", "image/gif", "video/mp4". |
ad.image_data / ad.imageData | bytes / Uint8Array | Pre-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.imageFileName | string | Suggested 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
from aiogram import Bot, Dispatcher, typesfrom sidekick_ads import Sidekickfrom sidekick_ads.middleware import SidekickMiddlewarebot = Bot(token="BOT_TOKEN")dp = Dispatcher()# Register middleware — injects `sidekick` into every handlersidekick_middleware = SidekickMiddleware(api_key="sk_live_xxxxx",platform_id="plt_xxxxx",)dp.message.middleware(sidekick_middleware)# Close the HTTP pool cleanly on dispatcher shutdowndp.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
from aiogram import Bot, Dispatcher, typesfrom sidekick_ads import Sidekickbot = 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+
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButtonfrom telegram.ext import ApplicationBuilder, MessageHandler, ContextTypes, filtersfrom sidekick_ads import Sidekicksk = Sidekick(api_key="sk_live_xxxxx", platform_id="plt_xxxxx")async def handle(update: Update, context: ContextTypes.DEFAULT_TYPE):user = update.message.from_userllm_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)
import httpxfrom sidekick_ads import Sidekicksk = 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 clientasync 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.
# 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.