Skip to main content
SocialCutter

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 requests library (pip install requests).
  • An sc_ SocialCutter key from https://dash.socialcutter.theboomer.dev under Profile → API keys.
  • A way to receive messages: getUpdates polling (simple) or an HTTPS webhook with setWebhook (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

  1. Photo and command arrive. The user sends a photo with /ig linkedin in the caption. With no command the bot applies a default destination (just one, to keep costs down).
  2. Download the file. getFile returns file_path and the file is fetched from https://api.telegram.org/file/bot<token>/<file_path>.
  3. Request the formats. The file goes to POST /api/v1/images/process/upload (multipart, 5 MB maximum) with the destination list.
  4. Reply with sendPhoto. Every entry in the outputs array 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 a file_id you never re-upload the file to Telegram.
  • OPTIONS forces a jpg output. The API default is webp, and Telegram photos are comfortably sent as jpg or png.
  • sendPhoto accepts a public URL or a file_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. /all with 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_id arrives again within a few minutes, return the URLs you already generated instead of calling again.
  • Send an Idempotency-Key with the update_id or file_unique_id so a network retry does not create new work.
  • Check GET /api/v1/credits before large batches and stop with a clear message when the balance is low.

Typical errors

SymptomCauseFix
401 from SocialCutterMissing or revoked keyCheck X-API-Key; only one active key per account
413 from SocialCutterFile over 5 MBCompress before upload or ask the user for a smaller photo
Telegram 400 when sending the photoThe output format is not usable by the clientRequest options.format as jpg or png
Bot goes silentProcess died or the webhook is failingCheck getWebhookInfo or the polling logs
429 from SocialCutterDaily quota exhaustedStop 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

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.