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'{
"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:
| Endpoint | Use when |
|---|---|
GET /api/files/{id}/metadata | You need exactly one file. |
POST /api/files/metadata/query | You have a list of ids. One round trip. |
POST /api/files/metadata/stream | The 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, 1600Anything 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.