Messaging and bots
Telegram bot that sizes your photos with SocialCutter
Python Telegram bot that takes a photo, processes it with the SocialCutter API and replies with the exact size for each network using sendPhoto.
- Telegram
- bot
- sendPhoto
- Python
- requests
- SocialCutter
- social media
What this bot does
A bot that receives a photo in Telegram and replies with that same photo already resized for each social network. The user sends the image with a command in the caption (/ig, /story, /li, /all) and gets one reply per format, each with the platform, the dimensions and the download link.
Worth saying plainly: SocialCutter does not publish to social networks. It generates the files with a centered crop and returns one public URL per output. Sending them to the chat is the bot’s job; publishing to Instagram, LinkedIn, X or TikTok is the job of your publishing tool or of your own integration with each platform.
Requirements
- A bot token, created with @BotFather.
- Python 3 and the
requestslibrary (pip install requests). - An
sc_SocialCutter key from https://dash.socialcutter.theboomer.dev under Profile → API keys. - A way to receive messages:
getUpdatespolling (simple) or an HTTPS webhook withsetWebhook(better in production).
pip install requests
export TELEGRAM_BOT_TOKEN="123456:ABC-your-token"
export SOCIALCUTTER_API_KEY="sc_your_key"
The Bot API reference is at https://core.telegram.org/bots/api and SocialCutter’s is at https://docs.socialcutter.theboomer.dev.
The flow, step by step
- Photo and command arrive. The user sends a photo with
/ig linkedinin the caption. With no command the bot applies a default destination (just one, to keep costs down). - Download the file.
getFilereturnsfile_pathand the file is fetched fromhttps://api.telegram.org/file/bot<token>/<file_path>. - Request the formats. The file goes to
POST /api/v1/images/process/upload(multipart, 5 MB maximum) with the destination list. - Reply with sendPhoto. Every entry in the
outputsarray is sent with its public URL. Telegram downloads the image and shows it in the chat.
Why download the file instead of passing Telegram’s URL
The tempting shortcut is to give SocialCutter Telegram’s file URL as source. Don’t:
- That URL contains the bot token; handing it to an external service leaks the credential that controls your bot.
- Telegram only guarantees the link for at least 1 hour, and direct downloads are capped at 20 MB (https://core.telegram.org/bots/api#file).
Download the file and upload it as multipart: the credential never leaves your server and the limit that applies is SocialCutter’s 5 MB.
Full code
import json
import os
import time
import requests
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
SC_KEY = os.environ["SOCIALCUTTER_API_KEY"]
TG = f"https://api.telegram.org/bot{BOT_TOKEN}"
SC_URL = "https://api.socialcutter.theboomer.dev"
# Caption command -> SocialCutter destinations. 1 use per destination.
COMMANDS = {
"/ig": [{"platform": "instagram", "format": "post"}],
"/story": [{"platform": "instagram", "format": "story"}],
"/li": [{"platform": "linkedin", "format": "post"}],
"/all": [
{"platform": "instagram", "format": "post"},
{"platform": "linkedin", "format": "post"},
{"platform": "twitter", "format": "post"},
],
}
DEFAULT = COMMANDS["/ig"] # one output when the user asks for nothing
OPTIONS = {"format": "jpg", "quality": 88}
def tg(method, **payload):
r = requests.post(f"{TG}/{method}", json=payload, timeout=60)
r.raise_for_status()
return r.json()
def download(file_id):
info = tg("getFile", file_id=file_id)["result"]
r = requests.get(f"https://api.telegram.org/file/bot{BOT_TOKEN}/{info['file_path']}", timeout=60)
r.raise_for_status()
return r.content
def process(image, destinations):
r = requests.post(
f"{SC_URL}/api/v1/images/process/upload",
headers={"X-API-Key": SC_KEY},
files={"file": ("photo.jpg", image, "image/jpeg")},
data={"destinations": json.dumps(destinations), "options": json.dumps(OPTIONS)},
timeout=120,
)
r.raise_for_status()
return r.json()
def handle(message):
chat_id = message["chat"]["id"]
if "photo" not in message:
tg("sendMessage", chat_id=chat_id, text="Send me a photo with a command: /ig, /story, /li or /all.")
return
words = (message.get("caption") or "").split()
destinations = COMMANDS.get(words[0] if words else "", DEFAULT)
photo = message["photo"][-1] # the last one is the largest
if photo.get("file_size", 0) > 5 * 1024 * 1024:
tg("sendMessage", chat_id=chat_id, text="That photo is over 5 MB: SocialCutter's limit.")
return
note = tg("sendMessage", chat_id=chat_id, text=f"Generating {len(destinations)} format(s)...")["result"]
try:
data = process(download(photo["file_id"]), destinations)
except requests.HTTPError as e:
code = e.response.status_code
message = {401: "The API key is not valid.", 413: "Image over 5 MB.",
429: "Quota exhausted."}.get(code, f"Error {code} while processing.")
tg("editMessageText", chat_id=chat_id, message_id=note["message_id"], text=message)
return
except requests.RequestException:
tg("editMessageText", chat_id=chat_id, message_id=note["message_id"],
text="Could not reach the service. Try again.")
return
tg("deleteMessage", chat_id=chat_id, message_id=note["message_id"])
for out in data["outputs"]:
tg("sendPhoto", chat_id=chat_id, photo=out["url"],
caption=f"{out['platform']} {out['format']} · {out['width']}x{out['height']}")
def main():
offset = None
while True:
params = {"timeout": 50, "offset": offset} if offset else {"timeout": 50}
for update in requests.get(f"{TG}/getUpdates", params=params, timeout=60).json()["result"]:
offset = update["update_id"] + 1
if "message" in update:
handle(update["message"])
time.sleep(0.5)
if __name__ == "__main__":
main()
Details that matter:
message["photo"]is an array of sizes; the last one is the largest. With afile_idyou never re-upload the file to Telegram.OPTIONSforces ajpgoutput. The API default iswebp, and Telegram photos are comfortably sent asjpgorpng.sendPhotoaccepts a public URL or afile_id. With a URL, Telegram downloads the image and the documented maximum is 5 MB (https://core.telegram.org/bots/api#sendphoto).- In production, swap polling for a webhook (
setWebhook) and always answer 200 so Telegram stops retrying.
How to keep the cost down
Every destination costs 1 use, so the bot decides the bill, not the user:
- One command maps to one destination list.
/allwith three destinations costs 3 uses per photo; make that an explicit choice and say so in the message. - Keep the default to a single destination, and combine only when asked:
/ig /story. - Deduplicate: if the same
file_unique_idarrives again within a few minutes, return the URLs you already generated instead of calling again. - Send an
Idempotency-Keywith theupdate_idorfile_unique_idso a network retry does not create new work. - Check
GET /api/v1/creditsbefore large batches and stop with a clear message when the balance is low.
Typical errors
| Symptom | Cause | Fix |
|---|---|---|
401 from SocialCutter | Missing or revoked key | Check X-API-Key; only one active key per account |
413 from SocialCutter | File over 5 MB | Compress before upload or ask the user for a smaller photo |
Telegram 400 when sending the photo | The output format is not usable by the client | Request options.format as jpg or png |
| Bot goes silent | Process died or the webhook is failing | Check getWebhookInfo or the polling logs |
429 from SocialCutter | Daily quota exhausted | Stop the bot and warn the user; the quota resets daily |
Privacy
Outputs are served as public URLs without authentication (the storage and image endpoints are open): anyone with the link can view the image, so treat them as public material and do not process internal documents. The photo the user sends leaves Telegram and travels to the SocialCutter API, where the file is downloaded to generate the crops. If your bot is internal, state in the bot itself what is sent and why; if it is public, put that in the welcome message.
Cost
- 1 use per destination (platform and format) per photo; repeated destinations are not charged twice.
- Failed processings are refunded.
- Every plan includes API and MCP: Free 3 uses/day, Basic 10, Pro 30, Agency 100.
Next steps
- Automation: Automate image resizing with n8n
- Terminal: Process images with the API from the terminal (curl)
- Python: Process images with the SocialCutter API from Python
- Agents: Use SocialCutter from your LLM or editor with MCP
- Documentation: https://docs.socialcutter.theboomer.dev
Frequently asked questions
Does the bot publish to Instagram, LinkedIn or X?
No. SocialCutter generates the resized images and returns one public URL per format; the bot forwards them to the chat. Publishing to each network is the job of your publishing tool or of your own integration with that platform's API.
How does the bot receive the user's photo?
Through getUpdates or a webhook. The message carries a photo array with several sizes; use the last one (the largest), request its file_path with getFile and download it with the bot token.
Why not hand Telegram's file URL straight to SocialCutter?
Because that URL contains the bot token (https://api.telegram.org/file/bot<token>/<file_path>), so you would be giving a third party the credential that controls your bot. It also expires: Telegram guarantees the link lives at least 1 hour. Download the file and upload it as multipart instead.
Which formats can I request?
Instagram post 1080x1080, story 1080x1920 and landscape 1080x566; Facebook post 1200x630, story 1080x1920 and cover 820x312; X post 1200x675 and header 1500x500; LinkedIn post 1200x627 and cover 1128x191; YouTube thumbnail 1280x720 and banner 2560x1440; TikTok cover 1080x1920.
What does each photo sent by a user cost?
1 use per destination, that is, per platform and format combination. Instagram post plus LinkedIn post for the same photo is 2 uses; a single command is 1 use.