Skip to content

Unfurl

Unfurl resolves one URL into the link preview a message would show for it. The API forwards the URL to the TEBHOS unfurl service, which picks a site-specific resolver or falls back to reading the page's Open Graph and meta tags. The same resolver chain is used for link previews attached to messages.

Unfurl embed object

The response of Resolve URL embed. This is a flat preview object, not a list of message embeds.

Structure

Field Type Description
url string The URL that was resolved
title ?string The page or media title
description ?string The page description
image ?string A preview image URL
site_name ?string The site or provider name
author ?string The author name, when the page exposes one
type ?string The preview type, for example link, video, article, social or gifv
video ?unfurl video object Playable video details, when the resolver found a video
color ?string A theme colour hint, for example #1DA1F2
favicon ?string The site favicon URL

Text fields (title, description, site_name, author) are sanitised before they are returned.

Unfurl video object

Field Type Description
url string The video URL
width ?integer The video width
height ?integer The video height
embed_url ?string An embeddable player URL (for example a YouTube iframe URL)

Example

{
  "url": "https://example.com/article",
  "title": "Example article",
  "description": "A short description taken from the page metadata.",
  "image": "https://example.com/cover.png",
  "site_name": "Example",
  "author": null,
  "type": "article",
  "video": null,
  "color": null,
  "favicon": "https://example.com/favicon.ico"
}

Site resolvers

Site Matched URL Notes
YouTube youtube.com, www.youtube.com, m.youtube.com, music.youtube.com, youtube-nocookie.com, www.youtube-nocookie.com, youtu.be, when a video ID can be parsed Uses the YouTube Data API. The instance needs a YouTube API key; without one no preview is produced. Non-video YouTube pages use the generic resolver
Klipy klipy.com and www.klipy.com with a path under /gif, /gifs, /clip or /clips Uses the Klipy API and returns a gifv preview. The instance needs a Klipy API key
Wikipedia Any URL containing .wikipedia.org/wiki/ Uses the Wikipedia REST summary; falls back to the generic resolver on failure
Twitter / X Any URL containing twitter.com or x.com Generic scrape with the site name set to Twitter or X, type social, and the author derived from the path; falls back to the generic resolver on failure
Generic Every other URL Reads og:*, twitter:* and standard meta tags from the first 500 KB of an HTML page. A non-HTML response produces a bare {url, type: "link"} preview

Network policy

The API rejects a URL before resolution when it cannot be parsed, its scheme is not http or https, or its host is a local or private address (localhost, loopback, link-local, private IPv4 ranges, cloud metadata hosts).

The unfurl service applies a second check to every URL: it must be at most 8192 characters with no control characters, have no user information, not use port 0, have a valid public hostname, and resolve only to public IP addresses. A page fetch times out after 5 seconds and follows at most 3 redirects. The whole resolution is abandoned after 20 seconds.

Cache behaviour

Successful results are cached for one hour per exact URL. A repeated request within that time is served from the cache without contacting the site.

Resolve URL embed

GET /embed

Resolves the supplied URL and returns one unfurl embed object. No authentication is required and no permission applies.

Query parameters

Field Type Description
url string Absolute http or https URL to resolve

Response

Status Body Condition
200 unfurl embed object The URL was resolved
400 error response url is missing, malformed, blocked by the network policy, cannot be fetched, or is a YouTube/Klipy URL on an instance without the matching API key (BAD_REQUEST)
500 error response The unfurl service is unavailable or failed internally (INTERNAL_SERVER_ERROR)

Side effects

Resolution can fetch the URL and the provider APIs listed above. It creates no message.

Rate limit

This route has no route-specific bucket. Authenticated requests count toward the global per-client limit described in Rate limits; unauthenticated GET requests are not counted.

Example

curl -G "https://tebhos.com/api/v1/embed" \
  --data-urlencode "url=https://example.com/article"