When users post messages in Slack containing links, we attach previews, adding context and continuity to conversations.
Classic link unfurling is the default treatment for links posted in Slack. When a link is spotted, Slack crawls it and provides a preview. Slack app unfurling is a more customizable experience initiated when Slack recognizes a link associated with your app and then sends your Slack app a corresponding event.
Teach links new tricks by using the Events API and Web API together. To begin, you'll need your very own Slack app.
While we'll provide explanation of each step, here's a glance at what the configuration and process of unfurling looks like for an app:
link_shared
event, giving your app the hints it needs for the unfurling process.chat.unfurl
to attach custom unfurling behavior to the original message. Like a cherry on top of an already delicious sundae, you can use blocks to make the message rich and interactive.We'll go into the details of each step in the following sections, from configuring your app to contextualizing the link in conversation.
To support custom app unfurling, your app needs to have permissions to listen to links posted in Slack and to unfurl those links.
Navigate to your app settings and choose the OAuth & Permissions sidebar to select scopes.
links:read
lets your app read specific links that are posted in Slack.links:write
gives your app permission to unfurl content associated with links.A bot token is recommended for all link unfurling operations.
If you haven't already, configure your app to support the Events API. After that, navigate to the Event Subscriptions sidebar.
link_shared
event to receive information about links posted in Slack that are associated with your app.link_shared
event will dispatch for all channels in each workspace it's installed in.These events do not contain the message itself, they contain only the info about the message you need to provide it unfurl behavior: the message's ts
value, the channel it appeared in, and which URLs it contained matching your registered domains.
On the Event Subscriptions page, select App unfurl domains. Your app can register up to five domains, and will only receive link_shared
events for URLs that match your registered domains.
Adding or removing domains requires re-installation of your Slack app. Every time an app is installed, the installing user is agreeing to those specifically-mentioned domains.
Each domain you register will be matched to URLs by a few heuristics:
example.com
and another.example.com
are valid, example
is not and nor is .com
).http://
or https://
). Slack will not unfurl decidedly ambiguous domain and URL mentions.link_shared
event is dispatched when a message posted includes a matching domain, with the exception of messages posted by classic apps. Apps will also not receive link_shared
events for their own messages. Refer to Quickstart: differences between classic apps and Slack apps for more information.https://example.com:23/skidoo
), Slack will still consider it a clean match to a registered example.com
.You should own these domains and if you don't, you must follow all the terms, conditions, rules, policies, proclamations, warnings, and edicts around a domain. Be courteous, kind, and helpful.
Be sure to leave out the protocol part of a URL when registering your domain. Discard any http://
or https://
or path or query string components. Finally, Unicode domains are not supported.
link_shared
events You've set everything up: your Slack app, the Events API, a link_shared
event subscription, and you've registered your domain(s). You've installed your Slack app on a workspace, specifically requesting the scopes you needed after you've registered your domains.
You have the knowledge. You have the power. You are ready to receive and react to link_shared
events.
When a user shares a link in a channel that matches your criteria, you'll receive an event shaped like this:
{
"token": "XXYYZZ",
"team_id": "TXXXXXXXX",
"api_app_id": "AXXXXXXXXX",
"event": {
"type": "link_shared",
"channel": "Cxxxxxx",
"is_bot_user_member": false,
"user": "Uxxxxxxx",
"message_ts": "123452389.9875",
"unfurl_id": "C123456.123456789.987501.1b90fa1278528ce6e2f6c5c2bfa1abc9a41d57d02b29d173f40399c9ffdecf4b",
"thread_ts": "123456621.1855",
"source": "conversations_history",
"links": [
{
"domain": "example.com",
"url": "https://example.com/12345"
},
{
"domain": "example.com",
"url": "https://example.com/67890"
},
{
"domain": "another-example.com",
"url": "https://yet.another-example.com/v/abcde"
}
]
},
"type": "event_callback",
"authed_users": [
"UXXXXXXX1",
"UXXXXXXX2"
],
"event_id": "Ev08MFMKH6",
"event_time": 123456789
}
For details on these fields, reference the link_shared
event page.
Once you've received this event, you can decide what to do next.
Most likely, you'll use those url
values within the event
object to look up what to display to the user. Maybe you'll make an API call to another service. Maybe you'll just reference your app's own base of knowledge and never query another service at all.
Be sure and respond with a friendly HTTP 200 OK to the event as quickly as possible. Do not wait to wrestle an unfurl with chat.unfurl
before telling Slack you received the event: consider this link_shared
event a kind of ping. Now it's up to you to pong with chat.unfurl
.
chat.unfurl
events The chat.unfurl
method requires a token and your content. It also requires either a combination of ts
and channel
or unfurl_id
and source
.
You can fetch the ts
and channel
values from the link_shared
event sent to your app. Once you're in possession of these parameters, you can call chat.unfurl
and include your content within the unfurls
parameter.
The unfurls
parameter expects a JSON map where the keys correspond to the URLs contained in the link_shared
event.
Each defined URL may contain an array of blocks, including your custom arrangement of text alongside actionable buttons and selects. All of the typical formatting available to you in messages holds true.
If you call chat.unfurl
with a application/x-www-form-encoded type
content type (rather than application/json
), the unfurls parameter will expect a URL-encoded string fragment of your JSON map.
Here's an example if you're using a application/json
content type:
{
"channel": "C12345",
"ts": "156762948.24601",
"unfurls": {
"https://gentle-buttons.com/carafe": {
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Take a look at this carafe, just another cousin of glass"
},
"accessory": {
"type": "image",
"image_url": "https://gentle-buttons.com/img/carafe-filled-with-red-wine.png",
"alt_text": "Stein's wine carafe"
}
}
]
}
}
}
To explore all chat.unfurl
parameters, reference its method page.
If your attempt to attach your unfurl to the message is successful, you'll be the proud winner of a rather generic HTTP 200 OK response:
{
"ok": true
}
There are error responses too. You'll receive them if you forget to include ts
, channel
, or properly-formed unfurls
parameter. See the errors section of chat.unfurl
for more details.
That's it, you made a link unfurl! While your work could be done here, you can opt to make your unfurl richer with interactivity.
Since the blocks you provide in link unfurls are just like other Slack app-enabled message layouts, you can also make them interactive with interactive components. Let's build on the knowledge gained so far and, provided you've set yourself up to use message buttons already, let's examine a more complicated example.
Let's say your app received an event detailing a match for https://figment.example.com/imagine
. This is a service you provide to help stimulate the imagination. At this specific URL, you generate a random imagination exercise to stimulate the mind.
This imagination machine might construct its JSON hash response to something resembling this:
{
"https://figment.example.com/imagine": {
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Let's pretend we're on a rocket ship to Neptune.*\nThe planet Neptune looms near. What do you want to do?"
},
"accessory": {
"type": "button",
"action_id": "orbit",
"text": {
"type": "plain_text",
"text": "Orbit"
},
"style": "primary"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"action_id": "land",
"text": {
"type": "plain_text",
"text": "Attempt to land"
}
},
{
"type": "button",
"action_id": "leave",
"text": {
"type": "plain_text",
"text": "Go home"
},
"style": "danger"
}
]
}
]
}
}
Now your interactive unfurl is firmly attached to the originating message, complete with buttons. Almost everything you already know about interactive components is true for these kind of buttons too.
When a user decides to click on one of your interactive components, we'll send your request URL an invocation payload as usual.
The only catch is that there will be no message
field. Instead, there will be an app_unfurl
field that will only contain the blocks that your app added when providing the unfurl. You still can't see the entire message.
Here's an example invocation:
{
"type": "block_actions",
"actions": [
{
"action_id": "orbit",
"block_id": "05cM",
"text": { ... },
"style": "primary",
"type": "button",
"action_ts": "123456791.2111"
}
],
"app_unfurl": {
"id": 1,
"blocks": [ ... ],
"app_unfurl_url": "https://figment.example.com/imagine",
"is_app_unfurl": true
},
"team": {
"id": "T123456",
"domain": "example"
},
"channel": {
"id": "C123456",
"name": "generators"
},
"user": {
"id": "U061F7AUR",
"name": "exemplar"
},
"token": "xxx",
"container": {
"type": "message_attachment",
"message_ts": "123456789.9875",
"attachment_id": 1,
"channel_id": "C123456",
"is_ephemeral": false,
"is_app_unfurl": true,
"app_unfurl_url": "https://figment.example.com/imagine"
},
"trigger_id": "112345.327112347633.59cee77e21deda86fd37639d3b5ddbcc",
"response_url": "https://hooks.slack.com/actions/T123456/XXXX/XXXX",
}
Here's a closer look at the most relevant fields in this kind of interactive unfurl:
Field | Type | Description | Required? |
---|---|---|---|
is_app_unfurl |
boolean | When set to true , this invocation is related to a Slack app unfurl your app is registered to handle. When absent or false , it's a standard action. |
No |
app_unfurl |
object | Contains blocks relevant to link unfurling from the original message that started this flow. See below. | No |
From this point forward, you can use the response_url
and all the other tools in the interactive toolbox to evolve this interaction. What next will befall our intrepid interstellar travelers?
Not all links are wild and free, full of content anyone can see. Some links require users to validate their identity. Slack provides several paths to unfurl non-public content based on user authentication.
Note that for apps using these authenticated unfurl requests, users who are on the same team that owns the app will always receive these requests, regardless of whether they have muted unfurl requests (i.e. selecting No
as shown below) or whether the app is already one of the user's Muted Apps:
If you have a Slack Marketplace app, we provide a way to ask the user posting a link to your service to authenticate before proceeding with an unfurl that the whole channel can see.
If you react to a link_shared
event with a call to chat.unfurl
with the user_auth_required
parameter set to true
, instead of displaying custom unfurl content, Slack displays an ephemeral message encouraging the user to install your app:
By selecting Install from Slack Marketplace, users will be taken to your application's installation or configuration page — even if it's not a part of the Slack Marketplace.
Unless you're building an internal integration, you'll likely want to provide an Add to Slack button on your app's home page that requests the links:read
and links:write
scopes during the OAuth flow.
This way, you use the OAuth sequence to validate the authority of the member to unfurl privileged content within a channel for everyone to see. During the callback step where the user returns to your website, you would capture any needed additional information about the user and their identity on your service. What you need to do is up to you and the context of your content. Avoid surprising users by doing something unexpected.
After installation, the next time the user posts a message mentioning your links (or even retroactively), you can provide unfurl content.
To send users more directly to your website and a personalized authentication flow, use chat.unfurl
's user_auth_url
parameter to provide a "just in time" URL on your servers.
Or, painstakingly author an amazing ephemeral message with formatting using the user_auth_message
parameter, including the link to your custom authentication flow in the message. Your ephemeral message can also include block kit content with use of user_auth_blocks
.
When building an app that also needs to authenticate with your service or a third party service outside of your control — for example, if your app uses Facebook to authorize users or you require them to sign in to your app first — you can handle that authentication step as part of this flow.
user_auth_url
or user_auth_message
parameters when using chat.unfurl
. The URL should point to a page you control. If relevant to your app, include any state
information you need in the URL.Whether you authorize users before or after Slack redirects the user to your OAuth redirect URI depends on how you want to juggle maintaining state, and where you expect any authentication fatigue-based drop-off to occur.
http
or https
, is required.message
object. For limited information about the original message, you can instead look in the app_unfurl
object.link_shared
event doesn't contain the original message; your app just learns about any links that match your registered domains.link_shared
event is not dispatched when a message posted by an app or integration includes a matching domain.link_shared
events for the same domain, only the originally installed app will receive the event.invalid_blocks
error when passing a valid Block Kit message containing a rich text section element to the chat.unfurl
API method. Even though rich text blocks are now generally supported for use with the Slack API, the chat.unfurl
API method does not currently support them.In addition to Slack app unfurling, we generate content previews by default. Learn more about classic unfurling below.
If a workspace doesn't have a Slack app handler for a specific domain, unfurling will fall back to classic behavior: Slack crawls the URL, looks for common OpenGraph and X (formerly known as Twitter) Card metadata, and renders some micro-approximation of the content. For some domains, Slack even provides its own extra bells and whistles. See the anatomy of a classic unfurl below:
When deciding whether to unfurl a link, we consider the type of content that was linked to. Our servers must fetch every URL in a message to determine what kind of content it references. We treat "media"—images, videos, or audio—differently to pages that are primarily text-content.
Here are some examples of media content:
While these are examples of text-based content:
By default, we unfurl all links in any messages posted by users and Slack apps. This applies to messages posted via incoming webhooks, chat.postMessage
and chat.postEphemeral
. We also unfurl links to media based content within Block kit blocks
.
If you'd like to override these defaults on a per-message basis, you can pass unfurl_links
or unfurl_media
top-level fields while posting the message. unfurl_links
applies to text-based content, while unfurl_media
applies to media-based content. These flags are mutually exclusive; the unfurl_links
flag has no effect on media content.
Ensure that the links:write
scope is added to your app settings for apps that will be performing app unfurling.
To prevent links from unfurling, set both unfurl_links
and unfurl_media
to false
when posting messages to stop Slack from crawling a link and attempting to display an unfurl.
For example, Slack will not crawl or unfurl the URL featured in text
below:
{
"channel": "birds",
"text": "Here's an example of a bird.",
"unfurl_links": false,
"unfurl_media": false,
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Check out the Easter Bluebird <https://www.allaboutbirds.org/guide/Eastern_Bluebird/|Eastern Bluebird>"
}
}
]
}
There is one notable exception to these rules: we never unfurl links where the label is a complete substring of your URL minus the protocol. This is so that a paragraph of text can contain domain names or abbreviated URLs that are treated as a reference rather than link to be unfurled. For example, if a message contains a link to http://example.com
with the label example.com
, that link will not be unfurled. There are more examples of this rule in the following section.
All of these examples are for incoming webhooks, but similar rules apply to our other APIs:
api.slack.com
is text-based, so this link will not unfurl:
{
"text": "<https://api.slack.com>"
}
Passing "unfurl_links": true
means the link will unfurl:
{
"text": "<https://api.slack.com>",
"unfurl_links": true
}
This xkcd link is an image, so the content will be unfurled by default:
{
"text": "<http://imgs.xkcd.com/comics/regex_golf.png>"
}
We can then disable that using the unfurl_media
flag:
{
"text": "<http://imgs.xkcd.com/comics/regex_golf.png>",
"unfurl_media": false
}
Even though unfurl_links
is true, this link has a label that matches the URL minus the protocol, so the link will not unfurl:
{
"text": "<https://api.slack.com|api.slack.com>",
"unfurl_links": true
}
The label for this link does not match the URL minus the protocol, so this link will unfurl:
{
"text": "<https://api.slack.com|Slack API>",
"unfurl_links": true
}