Skip to Content
API ReferenceMedia API

Media API

The Media API covers serving images with on-the-fly CDN transformations. For uploading files and the GraphQL File type, query surface, and lifecycle mutations, see Files API — this page does not restate them.

Uploading

Two paths upload into the media library:

  • GraphQL (recommended) — the two-step createFileUpload → PUT bytes → confirmFileUpload flow. Binary bytes never touch the GraphQL endpoint. See Files API.
  • REST multipart — a single POST for small files that hides the three-step dance:
POST https://api.foir.dev/api/files/upload
curl -X POST https://api.foir.dev/api/files/upload \ -H "x-api-key: sk_your_secret_key" \ -F "file=@/path/to/image.jpg" \ -F "folder=campaigns/spring-26"
async function uploadFile(file) { const formData = new FormData(); formData.append("file", file); const response = await fetch("https://api.foir.dev/api/files/upload", { method: "POST", headers: { "x-api-key": process.env.FOIR_API_KEY, }, body: formData, }); return response.json(); // → a File object (see Files API) }

The response is the canonical File object — id, filename, mimeType, size, url, width, height, blurhash, alt, createdAt. There is no separate processing “status”: for images, width, height, and blurhash are populated synchronously during the upload.

Limits

  • Maximum file size: 10 MB (the REST path is intended for small files; for large media use the GraphQL pre-signed flow).
  • Authentication: an API key with the files:write scope.

Supported file types

The REST endpoint validates the declared content type against the file’s magic bytes and accepts:

CategoryFormats
ImagesJPEG, PNG, GIF, WebP, AVIF, SVG
VideoMP4, WebM, MOV
AudioMP3, WAV, OGG
DocumentsPDF, JSON, CSV, XML
FontsWOFF, WOFF2
ArchivesZIP

Image Transformations

Image URLs include an opaque ?t= token the platform mints at resolution time. The token encodes the size preset and any per-usage crop/focal data. Free-form ?width=…&fit=… query parameters are not honoured — only the platform’s own tokens trigger transforms.

URL format

https://cdn.foir.dev/{fileId}?t=eyJzIjoibWVkaXVtIn0

The ?t= value is a base64url-encoded JSON object:

{ "s": "medium", "c": "10,10,80,80", "f": "0.5,0.4" }
FieldMeaningFormat
sSize preset (long-edge width in px)thumbnail (160) / small (640) / medium (1024) / large (1280) / xlarge (1920) / 2xl (2560) / 3xl (3840) / 4xl (5120)
cCrop rectanglex,y,width,height as 0–100 percentages
fFocal pointx,y as 0–1 floats

All three fields are optional. A token with only s resizes to a fixed long-edge width; adding c applies a per-usage crop before resize; f controls smart-crop fallback.

Format negotiation

Output format follows the Accept header. Browsers that send Accept: image/avif get AVIF; everyone else gets the best supported format. There’s no format parameter on the URL.

Why opaque tokens

Free-form transform parameters would let scrapers synthesise arbitrary variants and burn through CDN-side image-resize bills. The token grammar is opaque enough that only URLs the platform emits hit the transform pipeline; unknown query strings return the original image.

Resolved image values

The platform emits transform URLs automatically when you resolve image fields. Don’t construct ?t= tokens client-side — let the API response carry them. An image field resolves to an ImageValue:

query { product(naturalKey: "blue-widget") { hero { # ImageValue url # https://cdn.foir.dev/file_abc?t=… (default tier) width height alt blurhash dominantColor variants(format: WEBP) { # responsive tiers the platform pre-emits thumbnail { size format url width height } small { size format url width height } medium { size format url width height } large { size format url width height } xlarge { size format url width height } } } } }

Each tier is an ImageVariant object — not a bare URL string — so it requires a sub-selection. Its fields are size (the tier name), format (the rendered format), url, and the true rendered width/height (capped at the source and, for cropped images, derived from the crop aspect — so you can render without layout shift).

The variants(format: …) field takes an optional format argument — one of WEBP (the default), AVIF, JPEG, or PNG — that selects the format for the whole tier set. Each tier is a distinct, CDN-cacheable URL per format, so to drive a <picture> query the set once per format using aliases:

hero { webp: variants(format: WEBP) { large { url width height } } avif: variants(format: AVIF) { large { url width height } } }

The variants object also exposes the larger tiers under _2xl, _3xl, and _4xl (a GraphQL identifier can’t start with a digit, hence the leading underscore).

<picture> <source srcset="https://cdn.foir.dev/file_abc?t=eyJzIjoibGFyZ2UifQ" media="(min-width: 1024px)" /> <source srcset="https://cdn.foir.dev/file_abc?t=eyJzIjoibWVkaXVtIn0" media="(min-width: 640px)" /> <img src="https://cdn.foir.dev/file_abc?t=eyJzIjoic21hbGwifQ" alt="..." loading="lazy" /> </picture>

Blurhash placeholders

Resolved image values carry a blurhash field — a compact string encoding of a blurred placeholder. Use it to display a low-resolution preview while the full image loads.

import { Blurhash } from "react-blurhash"; import { useState } from "react"; function ImageWithPlaceholder({ image }) { const [loaded, setLoaded] = useState(false); return ( <div style={{ position: "relative" }}> {!loaded && image.blurhash && ( <Blurhash hash={image.blurhash} width={400} height={300} /> )} <img src={image.url} alt={image.alt} onLoad={() => setLoaded(true)} style={{ display: loaded ? "block" : "none" }} /> </div> ); }
  • Files API — uploads, the File type, listing, and lifecycle mutations (deleteFile, restoreFile, permanentlyDeleteFile).
  • Media & Files — feature overview.
Last updated on