Back to Blog
Developers

Can a Slack Webhook Send a File? What We Tested, and What Works

An incoming webhook cannot upload a file. It can show an image from a public URL in an image block, or a link Slack unfurls. For a real file you need a bot token and the upload API. Every payload we tried on 9 November 2026, with Slack's answer.

Slack Green Team
November 9, 2026
November 9, 2026
4 min read
Share:
slack
slack api
slack webhook

A Slack incoming webhook cannot send a file. It accepts only a JSON message, so there is no field for file bytes, and it ignores one if you add it. What a webhook can do is show an image that is already on a public URL, through an image block or a link that Slack unfurls. To attach a real file (a PDF, a CSV, a log) you need the app's bot token and Slack's upload API. On 9 November 2026 we sent 14 payloads through one webhook in a test workspace and uploaded the same files with a bot token.

Every payload we sent

What we sent through the webhookSlack's answerWhat showed in the channel
Image block with a public PNG URL200 ok (1.6 to 1.9 s)The image, full width
Image block with a URL that returns 404400 invalid_blocksNothing
Image block with a data:image/png;base64,... URL400 invalid_blocksNothing
Image block with slack_file pointing at a file the bot uploaded400 invalid_blocks, for a private file and for one shared in a public channelNothing
PNG URL in plain text200 okThe link plus an image preview
The same PNG URL again a few seconds later, unfurl_media: true200 okThe link only, no preview
PDF URL in text with unfurl_links: true200 okThe link only
Legacy attachment with image_url200 okThe image inside the attachment
A file field with base64 bytes200 okThe text only. The field was ignored
A Slack message posted by an incoming webhook with an image block showing a green bar chart of nightly build times

The image block is the closest a webhook gets to an attachment. Slack downloads the image when you post, which is why those requests took about 1.6 s instead of the usual 0.3 s, and why a dead URL fails at once with invalid_blocks instead of posting a broken image.

Show an image with a webhook

Host the image anywhere public (an S3 or Supabase bucket, your CI's artifact URL if it needs no login) and post an image block:

{
  "text": "Nightly build chart",
  "blocks": [
    { "type": "section", "text": { "type": "mrkdwn", "text": "*Nightly build*: 9 min on Friday" } },
    { "type": "image", "image_url": "https://example.com/build-chart.png", "alt_text": "Nightly build time chart" }
  ]
}

Keep text as the fallback for notifications. The Slack image block guide covers sizes, titles and the formats Slack accepts.

A plain link also works for images, because Slack unfurls it into a preview:

Two webhook messages with the same image link: the first shows an image preview, the second, sent seconds later, shows only the link

The second message in that screenshot posted the same link 0.3 s later with unfurl_media: true and got no preview. Slack did not unfurl the repeated link, so do not rely on a link preview for every message. The image block showed every time.

Why base64 and slack_file fail

People try two tricks to get a file through a webhook. A data: URL in image_url fails with invalid_blocks, because Slack fetches the image over HTTP. A slack_file reference (the image block field that points at a file already in Slack) also failed from the webhook, even for a file the same app's bot had shared in that public channel. The same block sent with the bot token posted fine. The webhook posts as a separate sender from the bot user, so it cannot see the bot's files, as our webhook vs bot token test shows.

Never appear "away" on Slack again

Cloud-based. No downloads. Works 24/7 even when your laptop is off.

Send a real file with the bot token

The app that owns your webhook can also have a bot token (xoxb-) with the files:write scope. With the Python SDK (slack_sdk 3.46.0 in our test), files_upload_v2 does the whole upload:

import os, time, json
from slack_sdk import WebClient
client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
t0 = time.time()
resp = client.files_upload_v2(
    channel=os.environ["CHANNEL_ID"],
    file="build-report.pdf",
    title="Build report",
    initial_comment="Nightly build report attached",
)
f = resp["file"]
print(round(time.time() - t0, 2), "s")
print(json.dumps({k: f.get(k) for k in ["id", "name", "mimetype", "size", "permalink"]}, indent=2))

Our run printed:

1.14 s
{
  "id": "F0CG46GGSA0",
  "name": "build-report.pdf",
  "mimetype": "",
  "size": 13264,
  "permalink": "https://slack-0yr1948.slack.com/files/U0CG27WHFC1/F0CG46GGSA0/build-report.pdf"
}

The PDF showed in the channel with a preview of its first page. Note the empty mimetype: Slack was still processing the file when the call returned. files.info a few seconds later gave application/pdf and listed the share in the channel.

Without the SDK, the same upload is three requests: files.getUploadURLExternal with the file name and byte length, a POST of the bytes to the URL it returns, then files.completeUploadExternal with the file ID and channel_id. A 96 KB PNG took 1.34 s that way. The old one-call files.upload method no longer exists; see files.upload deprecation and its replacement.

Which way to go

  • • A chart or screenshot that is already on a public URL: an image block through the webhook.
  • • A file people should download, or anything private: the bot token and files_upload_v2. Our webhook guide shows where both live in the app settings.
  • • A link to a file in your own storage: plain text through the webhook, but expect no preview for PDFs or for a link Slack just unfurled.

FAQ

Can a webhook send a CSV or log file?

No. Upload it with the bot token, or post the first lines in a code block. Long text gets split or cut, as our Slack message character limit test shows, so a file is the better home for a long log.

Does the image URL have to stay up?

We did not test how long Slack keeps its copy. The safe choice is a URL that stays up as long as the message matters.

Can I reply in a thread with a file?

Yes, with the bot token: files_upload_v2 takes a thread_ts argument. A webhook can reply in a thread with text or an image block, but only if you already know the parent message's ts.

Always Active

Stop Jiggling Your Mouse.

Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.

Related Articles

Developers

Slack Webhook vs Bot Token: 10 Things We Tested Both Ways

An incoming webhook can post, reply in a thread and do little else. A bot token can also edit, delete, upload files, pick the channel and post as a custom name. We sent the same Block Kit message both ways on 9 November 2026 and recorded each answer.

Slack Green Team
Developers

Slack Progress Bar: Block Kit, Emoji and chat.update, Tested

Block Kit has no progress bar element, so a Slack progress bar is text you redraw with chat.update. We drew one three ways on 9 November 2026, timed every update, and pushed chat.update until Slack answered ratelimited.

Slack Green Team
Developers

Slack Channel Welcome Message: A Tested Bot and the Free-Plan Catch

Slack has no built-in welcome message per channel, and Workflow Builder is closed to Free workspaces. A 30-line Bolt app that listens for member_joined_channel does the job on any plan. We ran it on 9 November 2026 and timed the welcome.

Slack Green Team