Slack video block: why a raw MP4 URL fails and what page it needs
Slack's video block wants an embeddable player page on an unfurl domain and the links.embed:write scope, not a bare file. Serve a tiny player page.

A bare MP4 link does not work in Slack's video block. Its video_url must be a public page that can be embedded in an iframe, hosted on a domain registered as one of your app's unfurl domains, and your app needs the links.embed:write scope. So to show a Sume video inline you publish a small player page that wraps the asset URL, then point the block at that page.
If you only need the file in the channel, uploading it is simpler. The video block is worth the extra page when you want a titled, inline player.
The block's requirements
The Slack reference lists type, alt_text, title, video_url and thumbnail_url as required. The URL must be in the app's unfurl domains, be public, be embeddable in an iframe, return a 2xx (or fewer than five redirects ending in one), and not be a Slack domain. Title and description are limited to 200 characters. The block is available in modals, messages and home tabs.
A Sume job webhook gives you the artifact URL; the thumbnail can be a Sume image from the same project or any public image. Neither is the video_url. That one is your player page, which can take the asset address as a query string or look it up by job id on your server.
| Field | Rule | What to supply for a Sume video |
|---|---|---|
| video_url | Public, iframe-embeddable, unfurl domain, 2xx | Your player page URL |
| thumbnail_url | Required | A still from the video or a generated image |
| title | Under 200 characters | Short job label |
| alt_text | Required | One sentence describing the clip |
| App scope | links.embed:write | Add it before installing |
Post the block, then the page
The Python function posts the message with the Web API. The second snippet is the whole player page; serve it from the unfurl domain and let it read the asset URL from ?src=. Validate that src points at a host you expect before you serve the page, or it becomes an open redirect into arbitrary media.
import os, requests
def post_video(channel, page_url, thumb_url, title):
block = {"type": "video", "title": {"type": "plain_text", "text": title[:150]},
"alt_text": "Generated video", "video_url": page_url, "thumbnail_url": thumb_url}
r = requests.post("https://slack.com/api/chat.postMessage",
headers={"Authorization": f"Bearer {os.environ['SLACK_BOT_TOKEN']}"},
json={"channel": channel, "text": title, "blocks": [block]}, timeout=10)
body = r.json()
if not body.get("ok"):
raise RuntimeError(body.get("error"))
return body["ts"]
# player.html, served from your unfurl domain
# <video controls autoplay muted playsinline style="width:100%"
# src="https://cdn.example.com/clip.mp4"></video>Checks before you ship it
The code checks ok and raises with Slack's error name, so test with a real message in a sandbox workspace before relying on the docs alone. Make sure your page responds without cookies and without an X-Frame-Options header that forbids framing, since either will leave Slack showing an empty player.
Sources
Related posts
More in Integrations
- Square Online video section: 100 MB, one hour, loop and autoplay
Square Online's Video section takes your own file up to 100 MB and one hour, with loop and automatic playback. How to cut and size a product clip for it.
- Stitch MCP beside Sume MCP: design a screen, then film the launch
Google Stitch's MCP setup lists an X-Goog-Api-Key header or OAuth. Sume's hosted MCP takes OAuth or a bearer key. How to run both in one coding agent.
- Stripe product images: build a set of 8 with the Sume image API
Stripe's product object holds up to 8 image URLs. Plan a set of eight shots with Sume, price it by model, and host copies you control before you set the field.
- stt_create in a read-only MCP session: insufficient_scope, not a bug
A read-only OAuth session on Sume's MCP server can list tools but paid calls such as stt_create and tts_create return insufficient_scope. What to switch on.
Written by Sume