Skip to main content
SocialCutter

AI and agents

Upload and share images in Slack with files.uploadV2

Upload images to Slack with files.uploadV2 or share them by URL, and chain SocialCutter so a bot replies with every format ready.

  • Slack
  • files.uploadV2
  • files:write
  • Block Kit
  • bot
  • SocialCutter
  • Python

What this solves

Slack is where a team shares images before publishing them. A bot can use that moment: someone drops the image into the channel, the bot crops it with a centered crop to each network’s dimensions and replies with the ready-made files. The piece that uploads images to Slack is files.uploadV2, and the piece that generates the formats is SocialCutter. This guide covers both and how to chain them.

Requirements: app, bot and scopes

ScopeWhat it is for
files:writeUpload, edit and delete files
chat:writePost messages and share by URL
files:readRead metadata (files.info, files.list), only if you need it

The bot uses an xoxb- token. It must be invited to the channel where it will post; otherwise you get not_in_channel. Official references: https://docs.slack.dev/messaging/working-with-files and https://docs.slack.dev/reference/scopes/files.write.

Upload a file with files.uploadV2

files.upload is retired. In its place Slack asks for the files.getUploadURLExternal and files.completeUploadExternal sequence. The official libraries wrap it in a “v2” method, so you do not have to orchestrate the two steps by hand:

from slack_sdk import WebClient

client = WebClient(token="xoxb-your-token")

client.files_upload_v2(
    channel="C0123456789",
    file="./instagram-post.jpg",
    title="Instagram 1:1",
    filename="instagram-post.jpg",
    alt_text="Creative for the Instagram feed",
    initial_comment="Format ready for Instagram",
)
ParameterWhat it is for
channelChannel where the file is shared
file / contentFile path or bytes in memory
filenameName with extension
titleVisible file title
initial_commentMessage that goes with the file
alt_textAlternative text for accessibility

Version note: Slack retired files.upload and recommends the two-step sequence, and the behaviour of the file methods has changed over time. Check the current docs in the official changelog before locking in an implementation: https://docs.slack.dev/changelog/2024-04-a-better-way-to-upload-files-is-here-to-stay.

Share by URL with Block Kit

If you already have a public URL (SocialCutter returns one per destination), you do not need to upload it to Slack: you can display it with an image block inside chat.postMessage. This path only needs chat:write.

client.chat_postMessage(
    channel="C0123456789",
    blocks=[
        {"type": "image", "image_url": out["url"], "alt_text": f"{out['platform']} {out['format']}"},
        {"type": "context", "elements": [{"type": "mrkdwn", "text": f"{out['width']}x{out['height']} ready to publish"}]},
    ],
)

The catch: the image block loads the image from Slack and needs a URL reachable without authentication. If the destination requires a session or expires quickly, upload the file with files.uploadV2 instead of linking it. For general block reference, see the official Block Kit docs: https://docs.slack.dev/block-kit.

Chaining SocialCutter: the bot that replies with the formats

The full flow:

  1. Someone uploads the image to the channel. Your service receives the file_shared event (or app_mention).
  2. The bot downloads the file. With url_private_download and the xoxb- token in the Authorization header.
  3. The bot calls SocialCutter. POST /api/v1/images/process/upload as multipart, with the binary and the destinations list.
  4. SocialCutter replies with image_id and an outputs array, one URL per destination.
  5. The bot returns the formats to the channel, with files_upload_v2 per output or with an image block.
import requests
from slack_sdk import WebClient

SLACK_TOKEN = "xoxb-your-token"
SC_KEY = "sc_your_key"
client = WebClient(token=SLACK_TOKEN)

def process_file(file_id, channel):
    # 1. Metadata and download with the bot token
    info = client.files_info(file=file_id)["file"]
    img = requests.get(info["url_private_download"],
                       headers={"Authorization": f"Bearer {SLACK_TOKEN}"}, timeout=30)
    img.raise_for_status()

    # 2. Ask SocialCutter for the formats
    r = requests.post(
        "https://api.socialcutter.theboomer.dev/api/v1/images/process/upload",
        headers={"X-API-Key": SC_KEY},
        files={"file": ("original.jpg", img.content, "image/jpeg")},
        data={"destinations": '[{"platform":"instagram","format":"post"},'
                              '{"platform":"twitter","format":"post"},'
                              '{"platform":"tiktok","format":"cover"}]'},
        timeout=60,
    )
    r.raise_for_status()
    outputs = r.json()["outputs"]

    # 3. Send each format back to the channel
    for out in outputs:
        f = requests.get(out["url"], timeout=30)
        client.files_upload_v2(
            channel=channel,
            content=f.content,
            filename=f"{out['platform']}-{out['format']}.jpg",
            title=f"{out['platform']} {out['format']} ({out['width']}x{out['height']})",
            initial_comment=f"Ready for {out['platform']}",
        )

Honest details about the result: the crop is centered and deterministic; there is no subject or face detection. If your image has an off-center subject, check the framing before publishing. And SocialCutter does not publish to Instagram, X or TikTok: it hands back files. Slack receives the versions; publishing is another step.

file_shared carries the file_id and the channel_id; the exact payload fields of the event can change between Events API versions, so validate the event against the official Slack docs before depending on a specific field.

Cost

ConceptValue
Cost per processing1 use per destination (platform and format)
Duplicate destinations in the same requestNot charged twice
Slack file uploadsNo API cost, within the workspace limits
SocialCutter plans0, 3, 9 and 29 EUR, with API and MCP included

The example bot asks for three destinations: it uses 3 uses per image that lands in the channel and returns three files.

Common errors

ErrorWhat is really happeningWhat to do
invalid_authThe token is invalid or revokedReinstall the app and use the current xoxb-
missing_scopefiles:write or chat:write is missingAdd the scope, reinstall and restart the bot
not_in_channelThe bot is not in the target channelInvite it with /invite
file_not_foundThe file_id does not exist or the bot cannot see itCheck the files:read scope and the id
The image block does not showThe URL is not public or redirects to a loginUpload the file with files.uploadV2
413 while processingThe image is over 5 MBCompress the master or serve a lighter version
The file looks blurry in SlackA small crop was uploaded and is scaled up on screenStart from a larger master for that destination

Next steps

Frequently asked questions

Which scopes does the bot need?

files:write to upload files, chat:write to post messages and files:read if you also read file metadata. The bot token starts with xoxb-.

Does files.upload still work?

Better not to rely on it. Slack retired files.upload in favour of the getUploadURLExternal plus completeUploadExternal sequence. The official libraries wrap it in the uploadV2 method, which is what you should use.

How do I share an image without downloading it into Slack?

With an image block in Block Kit inside chat.postMessage, pointing at a public URL. It only needs chat:write, but the image must be reachable without authentication.

How is the bot billed?

Each SocialCutter processing uses 1 use per destination. Slack does not charge for uploads within the workspace plan limits.

Does the bot publish to social networks?

No. SocialCutter generates the files at the right dimensions and returns them as URLs; the bot uploads them to Slack. Publishing to the social network is a separate step.