Skip to content

Course Imagery

Every course can carry two pieces of artwork, returned on GET /courses and GET /courses/{id}:

Field What it is Shape
thumbnailUrl Card artwork, for catalog grids and lists 3:2 landscape
heroImageUrl Banner artwork, for a course detail page 2:1 landscape

Both are absolute URLs on FSRI's public image CDN. They need no signing, no API key and no token, so you can put them straight into an <img> tag. A course with no artwork omits the field rather than returning null.

Always request a size

The URLs point at full resolution masters. On the production catalog a hero master is up to about 5 MB and a thumbnail up to about 1.6 MB. Append ?width= and ?format=auto as described below and the same image arrives in kilobytes. There is no reason to serve a master to a browser.

Resizing and format conversion

Add query parameters to the image URL itself. Nothing about the API call changes.

https://img.prod.lms.fsri.cloud/course-images/68ae086f6e3c6.png?width=400&format=auto

Measured on published courses, that turns a 662 KB thumbnail into a 4 KB image, and a multi megabyte hero into roughly 66 KB at ?width=1920&format=auto.

Parameters go on the image URL, not on the API

Partner API endpoints reject unknown query parameters with a 400. Calling GET /courses?width=400 is an error. Take thumbnailUrl from the response and add the parameters to that.

Parameter Accepts Behavior
width any positive integer Snapped to the nearest supported width (below). Height follows the aspect ratio.
format auto, webp, jpeg, avif, png auto picks the best format the browser accepts. See below.
quality any positive integer Snapped to 60, 75 or 85. Default 75.

All three are optional, and any combination works. Leave all three off and you get the original file, unchanged.

Supported widths

160, 240, 320, 400, 512, 576, 640, 768, 1024, 1280, 1600, 1920, 2560

A width that is not on this list snaps to the nearest one instead of failing. Ask for 333 and you receive the 320 wide image. The largest width is 2560; anything larger snaps to it.

Images are never enlarged. If you ask for a width larger than the master, you get the master's own width. Some thumbnail masters in the catalog are 648 pixels wide, so ?width=1280 on those returns a 648 pixel image. Lay out your UI by aspect ratio, not by assuming the delivered pixel width.

format=auto

auto reads the browser's Accept header and returns WebP where the browser supports it and JPEG otherwise. Every current browser gets WebP.

It deliberately never returns AVIF. On this catalog AVIF files measured 29 to 44 percent larger than WebP at every width, because the artwork is illustration rather than photography. You can still request format=avif explicitly.

Use format=png only if you need lossless output; it is much larger.

Examples

A responsive card image. The browser picks the smallest width that fills the slot, so phones download less than desktops.

<img
  src="{thumbnailUrl}?width=400&format=auto"
  srcset="{thumbnailUrl}?width=320&format=auto 320w,
          {thumbnailUrl}?width=640&format=auto 640w,
          {thumbnailUrl}?width=1024&format=auto 1024w"
  sizes="(max-width: 600px) 100vw, 320px"
  width="320" height="213"
  alt="{course title}"
  loading="lazy">

Set width and height in the 3:2 ratio (2:1 for heroes) so the page does not shift while the image loads.

function imageUrl(baseUrl, width, { format = 'auto', quality } = {}) {
  if (!baseUrl) return null; // course has no artwork
  const url = new URL(baseUrl);
  url.searchParams.set('width', String(width));
  url.searchParams.set('format', format);
  if (quality) url.searchParams.set('quality', String(quality));
  return url.toString();
}

const card = imageUrl(course.thumbnailUrl, 400);
const banner = imageUrl(course.heroImageUrl, 1920);
from urllib.parse import urlencode

def image_url(base_url, width, fmt="auto", quality=None):
    if not base_url:  # course has no artwork
        return None
    params = {"width": width, "format": fmt}
    if quality:
        params["quality"] = quality
    return f"{base_url}?{urlencode(params)}"

card = image_url(course.get("thumbnailUrl"), 400)
banner = image_url(course.get("heroImageUrl"), 1920)
function imageUrl(?string $baseUrl, int $width, string $format = 'auto', ?int $quality = null): ?string
{
    if ($baseUrl === null) {
        return null; // course has no artwork
    }
    $params = ['width' => $width, 'format' => $format];
    if ($quality !== null) {
        $params['quality'] = $quality;
    }

    return $baseUrl.'?'.http_build_query($params);
}

$card = imageUrl($course['thumbnailUrl'] ?? null, 400);
$banner = imageUrl($course['heroImageUrl'] ?? null, 1920);
# Compare the master with a 400px WebP of the same image
curl -s -o /dev/null -w '%{size_download} bytes\n' "$THUMBNAIL_URL"
curl -s -o /dev/null -w '%{size_download} bytes  %{content_type}\n' \
  -H 'Accept: image/webp' "$THUMBNAIL_URL?width=400&format=auto"

What you can rely on

Aspect ratios. Every thumbnail in the catalog is 3:2 and every hero is 2:1. Pixel dimensions of the masters vary, so design to the ratio.

A URL never changes its picture, but it can stop existing. When FSRI replaces a course's artwork, the new image gets a new URL and the old file is removed, so the old URL starts returning 404. The course's updatedAt moves at the same time, so a delta sync picks up the new URL.

In practice: take image URLs from your most recent sync rather than storing them permanently, and if an image returns 404, re-fetch that course. Caching the image bytes under their URL is safe, because a URL is never reused for a different picture.

Same request, same bytes. A given URL and parameters always produce the same image. Two requests that snap to the same variant, such as ?width=638 and ?width=640, return identical files.

Caching. Images are served with Cache-Control: public, max-age=86400, so browsers and your own CDN may cache them for a day. Resized variants are also cached at FSRI's edge, so the first request for a new size can take a moment longer than later ones.

Cross origin use. Images are served with Access-Control-Allow-Origin: *. You can fetch() them from your own pages or draw them to a canvas. For canvas use, add crossorigin="anonymous" to the <img> tag, or the canvas is marked as tainted and cannot be read back.

Missing images. A URL that does not exist returns 404. Unknown query parameters are ignored.

About smallThumbnailUrl

The course schema also lists smallThumbnailUrl. It is absent from every course and will stay that way: it predates URL resizing and no artwork was ever produced for it. Do not build against it. For a small thumbnail, use thumbnailUrl with a small width, for example ?width=160&format=auto.