Skip to content

Read and asset routes

A media read route resolves one path to one file and returns it. The image asset and stored object routes sit under the base URL published as endpoints.media by instance discovery (https://tebhos.com/api/v1). The media proxy routes sit at the site root (https://tebhos.com).

Shared route contract

Methods and Authorisation define the contract every route here uses. No route here has a request-count rate limit. Byte ranges defines which ranges are recognised on the media routes.

Common responses

Every route under endpoints.media can produce these statuses in addition to the ones in its own table. Error bodies are described in Responses and limits.

Status Condition
404 The object does not exist
416 The requested range is unsatisfiable
422 The object is an image but could not be decoded or re-encoded
502 The object store could not be read

Image asset contract

An image asset is a stored picture that TEBHOS serves at a requested size and format, such as a user avatar or a guild icon. The API returns the asset's hash on the owning resource (for example avatar on a user or icon on a guild). The URL combines the asset class, the owner's ID, that hash and a file extension:

{endpoints.media}/{asset_class}/{owner_id}/{hash}.{ext}
https://tebhos.com/api/v1/avatars/1290847135893127168/3f9a0c5b7e2d41a6b8c9d0e1f2a3b4c5.webp?size=64

Use the hash exactly as the API returned it. Do not calculate one from the image yourself. A hash that starts with a_ marks an animated asset; the prefix is part of the hash and must be kept in the URL.

A null hash means the resource has no custom image. TEBHOS then has nothing to serve: clients render a default avatar or a placeholder locally and make no media request.

Path rules:

  • owner_id must be an integer ID, otherwise 400.
  • ext selects the output format and must be webp, png, jpg, jpeg or gif (case-insensitive), otherwise 400. A filename without an extension returns the stored format.
  • The hash, after an optional a_ prefix, must be 1–64 ASCII letters and digits, otherwise 400.
Asset class owner_id Size class
avatars User ID (also a webhook ID for webhook avatars and the bot user ID for bot avatars) Icon
banners User ID Banner
server-icons Guild ID Icon
server-banners Guild ID Banner
server-invite-bg Guild ID Banner
server-embed-bg Guild ID Banner
live-room-banners ID of the user who uploaded the banner Banner

Asset size selection defines the size classes.

Query parameters

Field Type Description
size? integer Requested square edge in pixels, snapped up to the size ladder and clamped to the size class. An absent or unparsable value selects 128 before clamping; no value is rejected
animated? boolean true/1 keeps animation, false/0 returns the first frame. Absent: animated only when the hash starts with a_

Any other query parameter is ignored. The route accepts a Range over the selected representation.

Response

Status Body Condition
200 The asset representation The original or transformed asset is available
206 Selected bytes One range is satisfiable
400 Error object Invalid owner ID, extension or hash
404 Error object The asset does not exist, or the stored object is not an image

Common responses also apply. Successful responses are cached for a year and marked immutable: a new upload always gets a new hash and therefore a new URL.

Get user avatar

GET {endpoints.media}/avatars/{user_id}/{hash}.{ext}

Returns a user's avatar under the icon size class. Bot users and webhooks use the same route with their own ID.

Field Type Description
user_id snowflake The owning user (or webhook) ID
hash string The avatar hash from the user object
ext string Output format, see the image asset contract

Get user banner

GET {endpoints.media}/banners/{user_id}/{hash}.{ext}

Returns a user's profile banner under the banner size class.

Get guild icon

GET {endpoints.media}/server-icons/{guild_id}/{hash}.{ext}

Returns a guild icon under the icon size class. hash is the guild's icon value.

Get guild banner

GET {endpoints.media}/server-banners/{guild_id}/{hash}.{ext}

Returns a guild banner under the banner size class.

Get guild invite background

GET {endpoints.media}/server-invite-bg/{guild_id}/{hash}.{ext}

Returns the background image shown on the guild's invite page, under the banner size class.

Get guild embed background

GET {endpoints.media}/server-embed-bg/{guild_id}/{hash}.{ext}

Returns the background image used when the guild is embedded, under the banner size class.

Get live room banner

GET {endpoints.media}/live-room-banners/{user_id}/{hash}.{ext}

Returns the banner of a live voice room under the banner size class. The API returns this URL in full on the room object.

Get stored object

GET {endpoints.media}/files/{key}

Returns any stored object by its storage key, optionally resized and converted. This is the resizing path for objects whose canonical URL is on the uploaded-file CDN, such as attachments, custom emoji and stickers: the key is the path of the CDN URL.

https://cdn.tebhos.com/emojis/1290847135893127168/7b0f.../a61c9e2d.png
https://tebhos.com/api/v1/files/emojis/1290847135893127168/7b0f.../a61c9e2d.png?size=48

key may contain /. A key that is empty or contains .. returns 400.

Query parameters

Field Type Description
size? integer Requested square edge. Takes precedence over width and height
width? integer Maximum output width
height? integer Maximum output height
format? string webp, png, jpg, jpeg or gif. Any other value returns 400
animated? boolean false/0 returns the first frame. Default: animation is kept

size, width and height must be non-negative integers (a non-numeric value returns 400). Each is snapped up to the size ladder and clamped by the size class derived from the first segment of the key, see Asset size selection. When no dimension is given the original size is kept.

When format is absent and the request's Accept header contains image/webp, a PNG, JPEG or GIF image is returned as WebP and the response has Vary: Accept, Accept-Encoding. Browsers loading <img> elements send such an Accept header; a download tool that sends Accept: */* receives the original format.

Response

Status Body Condition
200 The image representation The object is a PNG, JPEG, GIF or WebP image
200 The stored bytes The object is not one of those image types (see below)
206 Selected bytes One range over an image representation is satisfiable
400 Error object Invalid key or format, or a non-numeric dimension

An image representation is cached for a year (public, max-age=31536000, without immutable, since storage keys carry their own unique ID).

An object that is not a PNG, JPEG, GIF or WebP image is returned unchanged: query parameters are ignored, Range is not applied, the Content-Type is derived from the key extension (svg, mp3, ogg, m4a, wav, mp4, webm, otherwise application/octet-stream), the response is cached for seven days, and it carries Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox so that an SVG cannot run script.

Common responses also apply.

Get signed external media

GET https://tebhos.com/external/{signature}/v2/{target}

Fetches media from a third-party URL through the TEBHOS media proxy, so that viewers never connect to the third-party host directly. Only URLs signed by TEBHOS are proxied; the proxy is not an open relay. Get a signed URL from Sign an external URL, or use the URL the API already returned (for example an embed's proxied image URL).

Path parameters

Field Type Description
signature string Base64url (unpadded) HMAC-SHA256 over v2/{target}, computed with a server-side secret
target string The target URL, base64url-encoded without padding

The only supported version segment is v2. A wrong version, a signature that does not verify, or a target that does not decode to UTF-8 returns 400.

Target validation

Before and while fetching, the proxy enforces:

  • The target scheme is http or https.
  • The host is not localhost or a similar name, does not end in .local, .internal, .lan or .localhost, and is not a cloud metadata host.
  • Every address the host resolves to is public. Loopback, private, link-local (including 169.254.169.254), carrier-grade NAT, benchmarking, documentation, multicast, broadcast, unspecified, IPv6 unique-local and IPv4-mapped private addresses are refused.
  • Redirects are followed up to 5 hops; a redirect to a private host name stops the fetch.
  • The origin must answer 2xx, with a Content-Type that starts with image/, audio/, video/mp4, video/webm or video/ogg.
  • A declared Content-Length above the size limit is refused before the body is read; a body that grows beyond it while streaming is cut off.

Query parameters

Field Type Description
width? integer Target width in pixels
height? integer Target height in pixels
format? string Only webp is recognised; it enables the transformations below

Without format=webp the origin bytes are streamed through unchanged. Signed external transformations describes what format=webp does for still images, GIFs and videos.

Response

Status Body Condition
200 The media representation The target was fetched and, when requested, transformed
400 text/plain reason Invalid signature, target refused by validation, origin failure, content type not allowed, too large, or transformation failed

A passthrough response keeps the origin's Content-Type and has Cache-Control: public, max-age=86400 and X-Content-Type-Options: nosniff. A transformed response is image/webp with Cache-Control: public, max-age=604800.

Get OpenGraph metadata

GET https://tebhos.com/og?url={url}

Fetches an HTML page and returns its OpenGraph and Twitter Card metadata as JSON. The same target validation applies, except that the origin must answer with text/html or application/xhtml+xml. Only the first 2 MiB of the page is read.

Query Type Description
url string The page URL, URL-encoded
Field Type Description
title ?string og:title, twitter:title, or the page <title>
description ?string og:description, twitter:description or description
image ?string og:image, twitter:image or twitter:image:src
site_name ?string og:site_name
url ?string og:url
og_type ?string og:type
video ?string og:video, og:video:url, og:video:secure_url or twitter:player
video_width ?integer og:video:width
video_height ?integer og:video:height
image_width ?integer og:image:width
image_height ?integer og:image:height
author_name ?string From the page's oEmbed data, when available
author_url ?string From the page's oEmbed data, when available
provider_url ?string From the page's oEmbed data, when available

When the page links a JSON oEmbed document, or the host is a known oEmbed provider (YouTube, Vimeo, SoundCloud, TikTok, Reddit), TEBHOS also fetches that document and uses it to fill the author and provider fields, site_name and image when the page itself lacks them. The response is cached for an hour (public, max-age=3600). Failures return 400 with a text/plain reason.

For rich link previews in messages, use the HTTP API unfurl behaviour instead of calling this route directly.

Sign an external URL

GET /media/sign?url={url}

HTTP API route (under /api/v1) that turns a third-party URL into a signed external media URL. Requires a signed-in user session (Authorization: Bearer ...); a bot token receives 403.

Query Type Description
url string An http:// or https:// URL, URL-encoded. Anything else returns 400
{
  "url": "https://tebhos.com/external/Q3Jx...Zw/v2/aHR0cHM6Ly9leGFtcGxlLmNvbS9jYXQuZ2lm"
}

Signing does not fetch or validate the target; validation happens when the signed URL is read.