Flambe docs
Guides

Files

Uploading images, resolving metadata in bulk, and using the CDN.

Files are the binary half of the API: the photographs you import from, and the images attached to recipes.

Uploading

POST /api/files takes multipart/form-data. The field name is files, and it repeats — one request can carry several.

curl -X POST https://api.flambe.dev/api/files \
  -H "Authorization: Bearer $FLAMBE_TOKEN" \
  -F 'files=@page-42.jpg' \
  -F 'files=@page-43.jpg'
201 Created
{
  "files": [
    {
      "id": "6f1c2e5a-6f0a-4c4e-9d2f-2c5a5b9e1a11",
      "url": "https://cdn.flambe.dev/files/6f1c2e5a...",
      "content_type": "image/jpeg",
      "width": 3024,
      "height": 4032,
      "blur_hash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj"
    }
  ]
}

Ids come back in upload order. Images are processed for width variants and given a BlurHash on the way in.

This is the first half of a media import: upload here, then pass the ids as file_ids.

Resolving metadata

Three ways, for three situations:

EndpointUse when
GET /api/files/{id}/metadataYou need exactly one file.
POST /api/files/metadata/queryYou have a list of ids. One round trip.
POST /api/files/metadata/streamThe list is long and you want to render progressively.
curl -X POST https://api.flambe.dev/api/files/metadata/query \
  -H "Authorization: Bearer $FLAMBE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "ids": ["6f1c2e5a-6f0a-4c4e-9d2f-2c5a5b9e1a11"] }'

Partial results are normal

Ids the caller cannot read are omitted from the response rather than raising an error. Never assume items.length === ids.length — match results back by id.

/metadata/stream returns application/x-ndjson: one JSON object per line, in no guaranteed order.

const res = await fetch('https://api.flambe.dev/api/files/metadata/stream', {
  method: 'POST',
  headers,
  body: JSON.stringify({ ids }),
});

const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;

  const lines = buffer.split('\n');
  buffer = lines.pop() ?? ''; // keep the partial line for the next chunk
  for (const line of lines) {
    if (line.trim()) render(JSON.parse(line));
  }
}

Serving images

Prefer the CDN url over GET /api/files/{id} for anything user-facing — it is cached at the edge, and the API route is not.

Width variants come from appending @{width}w.webp. Only these widths exist:

320, 480, 640, 800, 960, 1200, 1600

Anything else silently falls back to the original, full-resolution file — which is exactly the accident that makes a grid of thumbnails download tens of megabytes. Pick from the list.

Fall back to the canonical URL if a variant 404s; variants are generated on demand and a brand-new upload may briefly have none.

On this page