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
| Scope | What it is for |
|---|---|
files:write | Upload, edit and delete files |
chat:write | Post messages and share by URL |
files:read | Read 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",
)
| Parameter | What it is for |
|---|---|
channel | Channel where the file is shared |
file / content | File path or bytes in memory |
filename | Name with extension |
title | Visible file title |
initial_comment | Message that goes with the file |
alt_text | Alternative 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:
- Someone uploads the image to the channel. Your service receives the
file_sharedevent (orapp_mention). - The bot downloads the file. With
url_private_downloadand thexoxb-token in theAuthorizationheader. - The bot calls SocialCutter.
POST /api/v1/images/process/uploadas multipart, with the binary and thedestinationslist. - SocialCutter replies with
image_idand anoutputsarray, one URL per destination. - The bot returns the formats to the channel, with
files_upload_v2per output or with animageblock.
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
| Concept | Value |
|---|---|
| Cost per processing | 1 use per destination (platform and format) |
| Duplicate destinations in the same request | Not charged twice |
| Slack file uploads | No API cost, within the workspace limits |
| SocialCutter plans | 0, 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
| Error | What is really happening | What to do |
|---|---|---|
invalid_auth | The token is invalid or revoked | Reinstall the app and use the current xoxb- |
missing_scope | files:write or chat:write is missing | Add the scope, reinstall and restart the bot |
not_in_channel | The bot is not in the target channel | Invite it with /invite |
file_not_found | The file_id does not exist or the bot cannot see it | Check the files:read scope and the id |
The image block does not show | The URL is not public or redirects to a login | Upload the file with files.uploadV2 |
413 while processing | The image is over 5 MB | Compress the master or serve a lighter version |
| The file looks blurry in Slack | A small crop was uploaded and is scaled up on screen | Start from a larger master for that destination |
Next steps
- Python: Process images with the SocialCutter API from Python
- Node.js: Process images with the SocialCutter API from Node.js
- MCP: Use SocialCutter from your LLM or editor with MCP
- No-code: Automate image resizing with Zapier or n8n
- Strategy: Automating social media images: the 4 real paths
- Sizes: Social media sizes: dimensions and ratios
- API documentation: https://docs.socialcutter.theboomer.dev
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.