# 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.
- URL: https://socialcutter.theboomer.dev/en/guides/telegram/
- Idioma: en
- Familia: mensajeria
- Actualizado: 2026-09-24
- Palabras clave: 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).

```bash
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

```python
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

| 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](/en/guides/n8n/)
- Terminal: [Process images with the API from the terminal (curl)](/en/guides/curl/)
- Python: [Process images with the SocialCutter API from Python](/en/guides/python/)
- Agents: [Use SocialCutter from your LLM or editor with MCP](/en/guides/mcp/)
- Documentation: https://docs.socialcutter.theboomer.dev