API reference

Base URL: https://<your-domain>/api/v1. All responses are JSON:

{ "success": true, "data": { } }
{ "success": false, "error": { "code": "MEDIA_NOT_FOUND", "message": "Media file not found." } }

API responses are never cached (Cache-Control: no-store). Every response carries an X-Request-Id header; quote it when reporting a problem.

Authentication

Two ways to authenticate:

API keys can use the media, folder and usage endpoints. Key management, password changes and admin endpoints require a browser session, so a leaked key cannot create more keys or escalate.

Rate limits

When a limit is hit the API answers 429 RATE_LIMITED with a Retry-After header (seconds).

Media

Upload a file

POST /media?filename=<name>&folderId=<optional folder id>

The request body is the raw file (not multipart). Allowed types are checked from the file content: JPEG, PNG, WebP, GIF, AVIF, MP4, M4V, WebM, MOV, MP3, OGG, WAV, M4A and PDF. The extension in filename must match the content. The size limit is set by the server (MAX_UPLOAD_SIZE, 100 MB by default).

curl -X POST "https://media.example.com/api/v1/media?filename=photo.jpg" \
  -H "Authorization: Bearer $MEDIA_KEY" \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg

Response 201 with { media, duplicate: false }. If the same file (same SHA-256) is already in your library and deduplication is on (default), the response is 200 with the existing item and duplicate: true; no copy is stored and the existing item keeps its folder.

Images and videos are processed in the background: status is PROCESSING first and becomes READY (or FAILED with processingError). Poll GET /media/:id if you need the thumbnails right away. The original url works immediately.

A media object:

{
  "id": "cmh1x2y3z0000abcd1234efgh",
  "publicId": "a1B2c3D4e5F6g7H8",
  "filename": "photo.jpg",
  "originalFilename": "photo.jpg",
  "mimeType": "image/jpeg",
  "extension": "jpg",
  "kind": "IMAGE",
  "size": 482113,
  "width": 2400,
  "height": 1600,
  "duration": null,
  "status": "READY",
  "folderId": null,
  "url": "https://media.example.com/m/a1B2c3D4e5F6g7H8.jpg",
  "thumbnailUrl": "https://media.example.com/m/a1B2c3D4e5F6g7H8/thumbnail.webp",
  "posterUrl": null,
  "variants": [
    { "name": "thumbnail", "format": "webp", "width": 320, "height": 213, "size": 9120, "url": "…/thumbnail.webp" }
  ],
  "createdAt": "2026-10-26T09:30:00.000Z"
}

List files

GET /media

Query parameters, all optional:

Response: { items: Media[], nextCursor: string | null }.

Get, rename, move, delete

Folders

Usage

GET /usage?days=7|30|90 returns { current: { storageBytes, mediaCount }, totals, series }, where series has one entry per UTC day with storageBytes, bandwidthBytes, requests, uploads and downloads.

API keys (browser session only)

Account

Passwords need at least 10 characters with letters and numbers (max 72 bytes).

Admin (ADMIN role, browser session)

Admins cannot demote, disable or delete themselves, and the last active admin cannot be removed.

Public delivery

Public URLs do not need authentication and are safe to embed anywhere:

Originals are served Cache-Control: public, max-age=31536000, immutable; sizes (variants) public, max-age=86400, stale-while-revalidate=604800, because they are rendered again when watermark settings change. All responses have an ETag and Last-Modified, support Range requests (video seeking) and HEAD, and send Access-Control-Allow-Origin: *.

With WATERMARK=true, an image's original URL (/m/<id>.<ext>) serves its watermarked full size (WebP) instead of the uploaded file. The owner gets the clean original from GET /api/v1/media/<id>/original (session or API key, Content-Disposition: attachment, no-store).

Error codes

Example: upload from Node.js

import { readFile } from "node:fs/promises";

const res = await fetch("https://media.example.com/api/v1/media?filename=banner.png", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.MEDIA_KEY}`, "Content-Type": "image/png" },
  body: await readFile("banner.png"),
});
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log(data.media.url);