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:
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_idmust be an integer ID, otherwise 400.extselects the output format and must bewebp,png,jpg,jpegorgif(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
httporhttps. - The host is not
localhostor a similar name, does not end in.local,.internal,.lanor.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-Typethat starts withimage/,audio/,video/mp4,video/webmorvideo/ogg. - A declared
Content-Lengthabove 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 |
Signing does not fetch or validate the target; validation happens when the signed URL is read.