Charts in Slack
Two patterns for sending charts to Slack — file upload (recommended) and Block Kit image blocks. Both work in bots and automated workflows.
How it works
Slack supports two ways to display images in channels:
- File upload — POST the PNG bytes to Slack's Files API. Slack hosts the image internally. No public URL needed. Best for most bots.
- Block Kit image block — Pass a public URL. Slack fetches and displays the image. Simpler to implement if the image is publicly accessible.
Use padding: "slack" on your spec for a padding preset tuned for Slack's dark sidebar environment.
File upload (recommended)
Render the chart to a buffer, then upload it with the Slack SDK. No public URL, no CDN configuration required.
# Step 1: render chart to file
curl -s -X POST https://chart-output.com/api/v1/render \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
--output chart.png \
-d '{
"type": "bar",
"width": 800,
"height": 400,
"format": "png",
"padding": "slack",
"brandKitId": "polar",
"data": {
"labels": ["Mon", "Tue", "Wed", "Thu", "Fri"],
"datasets": [{ "label": "Signups", "data": [34, 56, 42, 71, 88] }]
}
}'
# Step 2: upload to Slack
curl -X POST https://slack.com/api/files.getUploadURLExternal \
-H "Authorization: Bearer xoxb-YOUR-SLACK-TOKEN" \
-F "filename=chart.png" \
-F "length=$(wc -c < chart.png)"Block Kit image block
Use returnUrl: true to get a CDN URL, then pass it to a Slack image block. The URL is publicly accessible and cached — Slack will fetch it once and display it inline.
# Render and get CDN URL
URL=$(curl -s -X POST https://chart-output.com/api/v1/render \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"line","width":800,"height":400,"format":"png","returnUrl":true,"data":{"labels":["Jan","Feb","Mar"],"datasets":[{"data":[14200,17800,22900]}]}}' | python3 -c "import sys,json;print(json.load(sys.stdin)['url'])")
# Post to Slack
curl -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer xoxb-YOUR-SLACK-TOKEN" \
-H "Content-Type: application/json" \
-d "{"channel":"C0123","blocks":[{"type":"image","image_url":"$URL","alt_text":"Chart"}]}"Full card in Slack
A card composition with header, KPI strip, and the polar brand kit looks sharp in Slack's dark sidebar:
{
"type": "bar",
"brandKitId": "polar",
"width": 800,
"height": 420,
"format": "png",
"padding": "slack",
"header": {
"eyebrow": "WEEKLY REPORT",
"title": "Signups",
"badge": { "text": "+14%", "backgroundColor": "rgba(96,165,250,0.15)", "color": "#60a5fa" }
},
"kpiStrip": [
{ "label": "Total", "value": "291", "color": "#f0f6fc" },
{ "label": "Avg/day", "value": "41.6", "color": "#f0f6fc" },
{ "label": "Peak", "value": "88 (Fri)", "color": "#60a5fa" }
],
"data": {
"labels": ["Mon", "Tue", "Wed", "Thu", "Fri"],
"datasets": [{
"label": "Signups",
"data": [34, 56, 42, 71, 88],
"borderRadius": 4
}]
}
}Incoming webhook
If you use Slack's incoming webhooks instead of the bot API, combine returnUrl: true with an image attachment:
const { url } = await renderChart({ returnUrl: true, ...spec });
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: 'Weekly signups:',
attachments: [{ image_url: url, fallback: 'Signups chart' }],
}),
});