Slack doesn’t run chart libraries in messages, so a chart reaches Slack in one of three forms:

  1. An image block: a JSON block with the URL of a PNG, JPG or GIF. Works from incoming webhooks, chat.postMessage, n8n and Make. Best for charts you already host or render on demand.
  2. A file upload: the image bytes, uploaded to Slack through the Web API (files:write scope), n8n, Make or Zapier. Best for charts that exist only as a file, or must not be public.
  3. A data visualization block: you send the numbers and Slack draws the chart. Best for small line, bar, area or pie charts with simple data.

The image block is the simplest. File uploads changed in 2025, and many older tutorials still show the retired method.

Option 1: an image block

An image block needs three fields: type (always image), alt_text (required, plain text, up to 2,000 characters), and either image_url (“the URL for a publicly hosted image”, up to 3,000 characters) or a slack_file reference to a file already in Slack. Supported formats are PNG, JPG, JPEG and GIF.

Incoming webhooks accept blocks, so this works with nothing more than a webhook URL:

curl -X POST -H 'Content-Type: application/json' \
  --data '{
    "text": "Weekly signups: 1,240 (up 12%)",
    "blocks": [
      { "type": "section", "text": { "type": "mrkdwn", "text": "*Weekly signups:* 1,240 (up 12%)" } },
      {
        "type": "image",
        "image_url": "https://example.com/charts/signups-2026-w42.png",
        "alt_text": "Bar chart of weekly signups: 1,240 this week, up 12% on last week."
      }
    ]
  }' \
  "$SLACK_WEBHOOK_URL"

The top-level text is the fallback shown in notifications and by clients that can’t render blocks; always set it.

What the image URL needs

Slack’s documentation asks for “a publicly hosted image”. If the image doesn’t appear, check the URL against this list:

  • Public. No login, cookie, VPN or IP allowlist. A URL that works in your browser only because you’re signed in will fail.
  • A direct image, over HTTPS. It should return the image itself with an image/png (or JPEG or GIF) content type, not an HTML page that shows the image. Slack doesn’t require HTTPS, but there is no reason to serve a chart over plain HTTP.
  • One URL per version. If a weekly report reuses one URL, nobody can tell which week an old message showed. Put the date or a version in the path, so each message points at its own image.
  • A sensible size. A PNG 1200 to 1600 pixels wide is sharp on high-density screens without being heavy.

Slack also unfurls plain image links pasted in a message by default; set "unfurl_media": false if you post a link and don’t want a preview (unfurling docs).

Option 2: upload the file

If the chart isn’t public, upload it. The old files.upload method is gone: apps created on or after May 16, 2024 could never use it, and Slack retired it on November 12, 2025. Uploads now take three calls, with a bot token that has the files:write scope:

FILE=signups.png
TOKEN=xoxb-your-bot-token
CHANNEL=C0123456789

# 1. Ask Slack for an upload URL (filename and size in bytes are required).
read -r UPLOAD_URL FILE_ID < <(curl -s https://slack.com/api/files.getUploadURLExternal \
  -H "Authorization: Bearer $TOKEN" \
  -F filename="$FILE" -F length="$(wc -c < "$FILE" | tr -d ' ')" \
  | jq -r '"\(.upload_url) \(.file_id)"')

# 2. Send the bytes to that URL.
curl -s -X POST "$UPLOAD_URL" -F file=@"$FILE" > /dev/null

# 3. Complete the upload and share it in a channel.
curl -s https://slack.com/api/files.completeUploadExternal \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json; charset=utf-8' \
  --data "{\"files\":[{\"id\":\"$FILE_ID\",\"title\":\"Weekly signups\"}],\"channel_id\":\"$CHANNEL\",\"initial_comment\":\"Weekly signups: 1,240 (up 12%)\"}"

Details from the method docs that matter:

  • files.getUploadURLExternal requires filename and length, and returns upload_url and file_id. The bytes can be sent “as raw bytes or … multipart form encoded”.
  • files.completeUploadExternal takes the files array (ids and optional titles), plus channel_id, thread_ts and initial_comment. Leave out channel_id and “the file will be private”. It “can only be called once” per upload.

Incoming webhooks accept a JSON message and nothing else, so they can’t upload files; for that you need a bot token.

Option 3: let Slack draw it

Since June 16, 2026, Block Kit has a data visualization block that renders “line, bar, area, or pie” charts from numbers you send. No image, no hosting:

{
  "type": "data_visualization",
  "title": "Weekly signups",
  "chart": {
    "type": "bar",
    "series": [
      {
        "name": "Signups",
        "data": [
          { "label": "W39", "value": 1010 },
          { "label": "W40", "value": 1080 },
          { "label": "W41", "value": 1107 },
          { "label": "W42", "value": 1240 }
        ]
      }
    ],
    "axis_config": { "categories": ["W39", "W40", "W41", "W42"], "x_label": "Week", "y_label": "Signups" }
  }
}

It suits small, simple charts, and the limits are tight: at most 2 per message, a title of up to 50 characters, 1 to 12 series, 1 to 20 data points per series, and labels of up to 20 characters. Every series must have a value for every category. For anything larger, or a chart that must look the same in Slack, email and a PDF, send an image.

From n8n

The Slack node supports both image methods:

  • Image block: use Message > Send, set Message Type to Blocks, and paste the blocks JSON from option 1, with the image URL as an expression (for example {{ $json.chartUrl }}).
  • File upload: get the PNG into the workflow as binary data (for example with an HTTP Request node that downloads it, with the response format set to file), then use File > Upload. Set File Property to the binary property name (data by default) and, under options, Channel Name or ID, Initial Comment and Title.

Version 2.2 and later of the Slack node use Slack’s new upload flow, although the operations table in n8n’s docs still names the retired files.upload method. If uploads fail on an old workflow, update the node to its latest version. The node needs a Slack credential with the files:write scope for uploads.

From Zapier

Zapier’s Slack integration has no Blocks field, so use an image URL as an attachment:

  1. Add the Send Channel Message action (or Send Direct Message).
  2. Write the message text, including the key number.
  3. Put your chart’s URL in Attach Image by URL. Zapier describes it this way: “If specified, this will be uploaded from the provided URL and attached to the message.”

The action also has a File field for a file produced by an earlier step. For anything else, such as the data visualization block, use the API Request (Beta) action to call chat.postMessage with your own JSON.

From Make

Make’s Slack app covers both methods:

  • Send a Message: has a Blocks field. Map the blocks JSON from option 1, with the image URL from an earlier module.
  • Upload a File: takes File name and Data (for example from an HTTP “Get a file” module), plus Title, Thread ID (timestamp) and Initial comment. Use this module, not the one labeled Upload a File (Deprecated).
  • Make an API Call: for anything else, such as the data visualization block.

Where the chart image comes from

Every option except the chart block needs a PNG. You can:

  • Export one from Chart Link: paste CSV or JSON, preview the chart and download a PNG, all in your browser. Hosted chart image links, which an automation could reference directly, are in early access.
  • Render one in code, for example Chart.js on a server.

The same image rules apply to charts in email: a solid background, a size that stays sharp, and alt text that says what the chart shows.

Sources

All sources were checked on October 14, 2026. The curl examples follow Slack’s documented request formats; they were not run against a live Slack workspace.