Image Proxy

Use the Nowly image proxy when Discord cannot load a presence image directly from a supported external CDN.

What It Is For

The image proxy is built for a specific case: a presence can see a public image on the page, but Discord cannot reliably load it from the original URL.

This is useful for video thumbnails, avatars, posters, or category images served by an external CDN when:

  • the image is public and already visible in the browser;
  • the CDN URL is too long for Discord Rich Presence;
  • Discord rejects, expires, or fails to download the direct image;
  • the presence needs to show content-specific artwork instead of only the service logo.

This is not a general-purpose image proxy. The API only accepts explicitly allowed services and domains.

How It Works

The recommended flow uses a temporary short URL:

TXT
POST https://api.nowly.me/images-proxy
GET  https://api.nowly.me/images-proxy/<id>

The presence sends the real CDN image URL to the API. The API checks that the service is supported, downloads the image, validates its MIME type and size, then stores it temporarily in cache. It returns a short URL such as:

TXT
https://api.nowly.me/images-proxy/abc123...

That URL directly returns the image bytes with the correct Content-Type. It is intended for largeImageKey or smallImageKey.

import { createCachedImageProxyUrl } from "@nowly/sdk"

const image = await createCachedImageProxyUrl("tiktok", imageUrl)

await presence.setActivity({
  details: "Watching a video",
  largeImageKey: image ?? Assets.Logo,
})

The cache is short-lived, currently five minutes. The helper also keeps an in-memory cache inside the presence so it does not call the API on every update.

Direct Endpoint

The older direct proxy remains available for manual testing or URLs that are already short enough:

TXT
https://api.nowly.me/image-proxy?service=<service>&url=<encoded-url>
https://api.nowly.me/i?u=<encoded-url>

The url and u parameters must be encoded with encodeURIComponent. For Discord Rich Presence, prefer createCachedImageProxyUrl because Discord limits image keys to 300 characters.

Setting It Up In A Presence

First, extract the real image URL from the page:

const getPoster = (video: HTMLVideoElement): string | undefined => {
  if (video.poster) return video.poster

  const image = video.closest("[data-video]")?.querySelector("img")
  return image?.getAttribute("src") ?? undefined
}

Then pass that URL through the helper before setting the activity:

import { createCachedImageProxyUrl } from "@nowly/sdk"

const poster = await createCachedImageProxyUrl("tiktok", getPoster(video))

await presence.setActivity({
  details: "Watching a video",
  largeImageKey: poster ?? Assets.Logo,
})

Always keep a fallback asset. If the page has no image, the image is too large, or the origin is not allowed, the presence should continue to work with Assets.Logo or a local asset.

Adding A Service

Allowed domains are checked by the API, not by the presence. To add a new service, add a proxy configuration entry with:

  • a stable service id;
  • the allowed CDN host suffixes;
  • any headers required by the CDN;
  • a clear limit on what the presence can send to the proxy.

If a presence needs a new image domain, you can use the helper in your presence PR and explain:

  • which exact hostnames need support;
  • why Discord cannot use the direct image URL;
  • what image the user will see;
  • why the image is public and safe to proxy.

Reviewers will then decide whether to add those URLs to the supported services. They may reject origins that are too broad, unstable, private, or unsafe.

Limits And Security

The proxy should only serve public images needed by reviewed and accepted presences. It rejects non-HTTPS URLs, non-allowlisted domains, responses that are not images, and files that are too large.

Do not use it for private account content, authenticated resources, tracking URLs, arbitrary user input, or non-image content.