Skip to content

Video Unfurling with MP4s linked from Azure Blob Storage #1811

@oes-beckerb

Description

@oes-beckerb

I'm attempting to enable video unfurling within Slack for videos (MP4s) hosted in Azure Blob Storage. These videos can have temporary, time-bound publicly available links, but cannot be hosted on public sites like YouTube. Currently video URLs that we are posting open up the video to play within the browser. We'd like this to be streamlined so that users can play videos without having to redirect them to their browser, i.e. similar to if we post links to YouTube. After quite a lot of back and forth since I first opened the case on 9/27, Slack support asked me to open an issue on GitHub, so here I am.

Slack documentation here https://docs.slack.dev/reference/block-kit/blocks/video-block/#requirements calls out a set of requirements that our use case satisfies without issue as far as I can tell.

Requirements

  • Video blocks can only be posted by apps; users are not allowed to post embedded videos directly from Block Kit Builder.
    • Not applicable as I’m not using Block Kit Builder
  • Your app must have the links.embed:write scope for both user and bot tokens.
    • To remove all possibility of there being a permissions issue, support had me put chat:write, links.embed:write, links:read, and links:write on both the Bot Token as well as User Token scopes.
  • video_url has to be included in the unfurl domains specified in your app.
    • Configured correctly, Lambda function is getting triggered and I can see the link to the MP4 file coming in.
  • video_url should be publicly accessible, unless the app relies on information received from the Events API payloads to make a decision on whether the viewer(s) of the content should have access. If so, the service could create a unique URL accessible only via Slack.
    • Video is publicly accessible, I can play it directly in any browser.
  • video_url must be compatible with an embeddable iFrame.
    • Tested without issue in an iFrame
  • video_url must return a 2xx code OR 3xx with less than 5 redirects and an eventual 2xx.
    • True as far as I can tell given it’s publicly accessible and it’s a direct link to the file. Network trace shows immediate 2XXs coming through
  • video_url must not point to any Slack-related domain.
    • True, points to myexample.blob.core.windows.net

I've configured my application to call an AWS Lambda Function. I can see that the function is getting successfully called, so the event subscription configuration within the Slack App is configured correctly. Calling the slack_sdk.chat_unfurl method ends up with an cannot_unfurl_message error response.

Python code:

def lambda_handler(event, context):
    
    headers = event.get("headers", {})
    raw_body = event.get("body", "")

    # Validate Slack request, helper method that's not relevant to include here here
    if not verify_slack_request(headers, raw_body):
        return {
            "statusCode": 401,
            "body": json.dumps({"error": "Unauthorized"})
        }

    try:
        body = json.loads(raw_body)
    except json.JSONDecodeError:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Invalid JSON"})
        }

    # Handle Slack's URL verification challenge
    if body.get("type") == "url_verification":
        return {
            "statusCode": 200,
            "body": json.dumps({"challenge": body["challenge"]})
        }

    # Handle link_shared event
    if body.get("event", {}).get("type") == "link_shared":
        event_data = body["event"]
        links = event_data.get("links", [])
        channel = event_data.get("channel")
        message_ts = event_data.get("message_ts")

        unfurls = {}

        for link in links:
            url = html.unescape(link.get("url", ""))

            if (".mp4" in url or ".html" in url) and "blob.core.windows.net" in url:
                unfurls[url] = {
                    "blocks": [
                        {
                            "type": "video",
                            "title": {
                                "type": "plain_text",
                                "text": "Video title text test",
                                "emoji": True
                            },
                            # "title_url": url,
                            "description": {
                                "type": "plain_text",
                                "text": "Azure MP4 URL description",
                                "emoji": True
                            },
                            "video_url": url,
                            "alt_text": "Azure MP4 alt_text",
                            "thumbnail_url": "https://i.ytimg.com/vi/8876OZV_Yy0/hqdefault.jpg"
                        }
                    ]
                }

        # If there's anything to unfurl, send it
        if unfurls:
            try:
                client.chat_unfurl(
                    channel=str(channel),
                    ts=str(message_ts),
                    unfurls=unfurls
                )
            except SlackApiError as e:
                print(f"Slack API error: {e}")
                print(f"Slack API error response: {e.response}")

    return {
        "statusCode": 200,
        "body": json.dumps({"status": "ok"})
    }

Unfurls JSON that gets sent over as part of the chat_unfurl call:

{
    "https://myexample.blob.core.windows.net/mycontainer/afolder/myvideo.mp4?sp=r&st=2025-11-25T20:39:42Z&se=2025-11-28T04:54:42Z&spr=https&sv=2024-11-04&sr=b&sig=somesignaturecharactershere": {
        "blocks": [
            {
                "type": "video",
                "title": {
                    "type": "plain_text",
                    "text": "Video title text test",
                    "emoji": true
                },
                "description": {
                    "type": "plain_text",
                    "text": "Azure MP4 URL description",
                    "emoji": true
                },
                "video_url": "https://myexample.blob.core.windows.net/mycontainer/afolder/myvideo.mp4?sp=r&st=2025-11-25T20:39:42Z&se=2025-11-28T04:54:42Z&spr=https&sv=2024-11-04&sr=b&sig=somesignaturecharactershere",
                "alt_text": "Azure MP4 alt_text",
                "thumbnail_url": "https://i.ytimg.com/vi/8876OZV_Yy0/hqdefault.jpg"
            }
        ]
    }
}

And for good measure, timestamp is in the format 1766451012.377049 and channel is in the format D123ABCDE12. Response from chat_unfurl message is {'ok': False, 'error': 'cannot_unfurl_message'}

Slack support also had me try to link to an HTML file that "wrapper" for the MP4, vs to the MP4 directly. That HTML was structured like below. I attempted as is, and with all ampersand & characters URL encoded as &, neither with any luck.

<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Video Title Text Test</title>

        <meta name="description" content="Azure MP4 URL description">
       
        <meta property="og:site_name" content="My Video Service">
        <meta property="og:title" content="Video title text test">
        <meta property="og:description" content="Azure MP4 URL description">
        <meta property="og:image" content="https://i.ytimg.com/vi/8876OZV_Yy0/hqdefault.jpg">
        <meta property="og:type" content="video.other">

        <meta property="og:video" content="https://myexample.blob.core.windows.net/mycontainer/afolder/myvideo.mp4?sp=r&st=2025-11-25T20:39:42Z&se=2025-11-28T04:54:42Z&spr=https&sv=2024-11-04&sr=b&sig=somesignaturecharactershere">
        <meta property="og:video:secure_url" content="https://myexample.blob.core.windows.net/mycontainer/afolder/myvideo.mp4?sp=r&st=2025-11-25T20:39:42Z&se=2025-11-28T04:54:42Z&spr=https&sv=2024-11-04&sr=b&sig=somesignaturecharactershere">
        <meta property="og:video:type" content="video/mp4">
        <meta property="og:video:width" content="1280">
        <meta property="og:video:height" content="720">

        <meta name="twitter:card" content="player">
        <meta name="twitter:title" content="Video title text test">
        <meta name="twitter:image" content="https://i.ytimg.com/vi/8876OZV_Yy0/hqdefault.jpg">
        <meta name="twitter:player" content="https://myexample.blob.core.windows.net/mycontainer/afolder/videowrapper.html?sp=r&st=2025-11-25T20:39:42Z&se=2025-11-28T04:54:42Z&spr=https&sv=2024-11-04&sr=b&sig=somesignaturecharactershere">
        <meta name="twitter:player:width" content="1280">
        <meta name="twitter:player:height" content="720">

        <style>
            body, html { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: #000; }
            video { width: 100%; height: 100%; object-fit: contain; }
        </style>
    </head>
    <body>

        <video controls autoplay name="media">
            <source src="https://myexample.blob.core.windows.net/mycontainer/afolder/myvideo.mp4?sp=r&st=2025-11-25T20:39:42Z&se=2025-11-28T04:54:42Z&spr=https&sv=2024-11-04&sr=b&sig=somesignaturecharactershere" type="video/mp4">
            Your browser does not support the video tag.
        </video>

    </body>
</html>

The last thing support had me try was calling the API directly through Postman, which likewise fails with the same cannot_unfurl_message error and an HTTP 200 response code.

--header 'Authorization: Bearer xoxb-somecharactershere' \
--header 'Content-type: application/json; charset=utf-8' \
--data '{
  "channel": "D123ABCDE12",
  "ts": "1766451012.377049",
  "unfurls": {
    "https://myexample.blob.core.windows.net/mycontainer/afolder/myvideo.mp4?sp=r&st=2025-11-25T20:39:42Z&se=2025-11-28T04:54:42Z&spr=https&sv=2024-11-04&sr=b&sig=somesignaturecharactershere": {
            "blocks": [
                {
                    "type": "video",
                    "title": {
                        "type": "plain_text",
                        "text": "Video title text test",
                        "emoji": true
                    },
                    "video_url": "https://myexample.blob.core.windows.net/mycontainer/afolder/myvideo.mp4?sp=r&st=2025-11-25T20:39:42Z&se=2025-11-28T04:54:42Z&spr=https&sv=2024-11-04&sr=b&sig=somesignaturecharactershere",
                    "thumbnail_url": "https://i.ytimg.com/vi/8876OZV_Yy0/hqdefault.jpg",
                    "alt_text": "Azure MP4 alt_text"
                }
            ]
        }
    }
}'

I've provided several different actual URLs to Slack support; the ones I've linked above have been scrubbed of identifying information/data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions