Media API
The Media API provides file upload, management, and image transformation capabilities. Upload images, videos, documents, and other files, then serve images with on-the-fly CDN transformations.
File Upload
Upload files using the REST endpoint with a multipart form data request.
Endpoint
POST https://api.foir.io/api/files/uploadRequest
Send a multipart/form-data request with the file in the file field:
curl -X POST https://api.foir.io/api/files/upload \
-H "x-api-key: sk_live_your_api_key" \
-F "file=@/path/to/image.jpg"async function uploadFile(file) {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("https://api.foir.io/api/files/upload", {
method: "POST",
headers: {
"x-api-key": process.env.FOIR_API_KEY,
},
body: formData,
});
return response.json();
}Limits
- Maximum file size: 10 MB
- Authentication: Requires an API key with
files:writescope
Supported File Types
| Category | Formats |
|---|---|
| Images | JPEG, PNG, GIF, WebP, AVIF, SVG, TIFF, BMP, ICO |
| Video | MP4, WebM, MOV, AVI |
| Audio | MP3, WAV, OGG, FLAC, AAC |
| Documents | PDF, DOC, DOCX, XLS, XLSX, CSV, TXT, RTF |
| Fonts | WOFF, WOFF2, TTF, OTF, EOT |
Upload Response
{
"id": "file_abc123",
"fileName": "image.jpg",
"fileUrl": "https://cdn.foir.io/file_abc123",
"contentType": "image/jpeg",
"fileSize": 245760,
"status": "PROCESSING"
}After upload, files enter a processing pipeline where metadata like dimensions, blur hash, and dominant color are extracted.
File Processing Status
| Status | Description |
|---|---|
PENDING | Upload received, waiting for processing |
PROCESSING | Extracting metadata and generating variants |
READY | Fully processed and available for delivery |
FAILED | Processing failed (file is still accessible) |
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.io/{fileId}?t=eyJzIjoibWVkaXVtIn0The ?t= value is a base64url-encoded JSON object:
{ "s": "medium", "c": "10,10,80,80", "f": "0.5,0.4" }| Field | Meaning | Format |
|---|---|---|
s | Size preset | thumbnail (150 px) / small (320) / medium (640) / large (1280) / xlarge (1920) |
c | Crop rectangle | x,y,width,height as 0–100 percentages |
f | Focal point | x,y as 0–1 floats |
All three fields are optional. A token with only s resizes to a fixed 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.
query {
product(naturalKey: "blue-widget") {
hero { # ImageValue
url # https://cdn.foir.io/file_abc?t=… (large default)
sources { # responsive sizes the platform pre-emits
url
media
}
width
height
alt
blurHash
dominantColor
}
}
}<picture>
<source srcset="https://cdn.foir.io/file_abc?t=eyJzIjoibGFyZ2UifQ" media="(min-width: 1024px)" />
<source srcset="https://cdn.foir.io/file_abc?t=eyJzIjoibWVkaXVtIn0" media="(min-width: 640px)" />
<img src="https://cdn.foir.io/file_abc?t=eyJzIjoic21hbGwifQ" alt="..." loading="lazy" />
</picture>Blurhash Placeholders
Images processed by Foir include 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({ file }) {
const [loaded, setLoaded] = useState(false);
return (
<div style={{ position: "relative" }}>
{!loaded && file.blurHash && (
<Blurhash hash={file.blurHash} width={400} height={300} />
)}
<img
src={file.fileUrl}
alt={file.altText}
onLoad={() => setLoaded(true)}
style={{ display: loaded ? "block" : "none" }}
/>
</div>
);
}GraphQL File Queries
Get a Single File
query GetFile {
file(id: "file_abc123") {
id
fileName
fileUrl
contentType
fileSize
width
height
blurHash
dominantColor
altText
caption
status
createdAt
}
}Required scope: files:read
List Files
query ListFiles {
files(
limit: 20
offset: 0
contentType: "image"
folder: "banners"
) {
items {
id
fileName
fileUrl
contentType
fileSize
width
height
blurHash
}
total
hasMore
}
}Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | Int | Maximum files to return |
offset | Int | Files to skip |
contentType | String | Filter by content type prefix (e.g., "image", "video") |
folder | String | Filter by folder |
Check Storage Usage
query StorageUsage {
fileStorageUsage {
totalBytes
fileCount
}
}GraphQL File Mutations
Update File Properties
mutation UpdateFile {
updateFile(id: "file_abc123", input: {
fileName: "updated-name.jpg"
folder: "heroes"
tags: ["marketing", "homepage"]
}) {
id
fileName
folder
tags
}
}Required scope: files:write
Update File Metadata
Update translatable metadata like alt text and captions:
mutation UpdateMetadata {
updateFileMetadata(id: "file_abc123", input: {
altText: "A scenic mountain landscape at sunset"
caption: "Photo taken in the Swiss Alps"
locale: "en"
}) {
id
altText
caption
}
}Required scope: files:write
Delete a File
mutation DeleteFile {
deleteFile(id: "file_abc123") {
success
message
}
}Required scope: files:delete
File Type Reference
type File {
id: ID!
fileName: String!
fileUrl: String!
contentType: String!
fileSize: Int!
width: Int
height: Int
blurHash: String
dominantColor: String
altText: String
caption: String
folder: String
tags: [String]
status: FileStatus!
createdAt: DateTime!
updatedAt: DateTime!
}