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.

4 min readSume
All posts

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.

Video block fields (read 2026-10-03)
FieldRuleWhat to supply for a Sume video
video_urlPublic, iframe-embeddable, unfurl domain, 2xxYour player page URL
thumbnail_urlRequiredA still from the video or a generated image
titleUnder 200 charactersShort job label
alt_textRequiredOne sentence describing the clip
App scopelinks.embed:writeAdd 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

All Integrations posts

Written by Sume