Feeds and batch URL minting
A feed shows many files at once. Minting one URL per file costs a round trip each and runs into the rate limit; minting a page at once costs one call. This guide shows the pattern and the choices around it. It is for backend developers who render pages with media.
files:managefiles:readOne call per page
Section titled “One call per page”POST P/files/urls takes up to 200 file ids and the same options for all of them, and answers one entry per id, in the order you sent:
{ "items": [ { "file_id": "01J9Z6S9Q5F5R6Y2W2GZ3B3K4D", "url": "https://…", "expires_at": "2026-10-22T10:15:00.000Z" }, { "file_id": "01J9Z6T1A0B4C9D8E7F6G5H4J3", "error": "rendition_not_available" }] }A per-item error never fails the batch. More than 200 ids answer too_many_file_ids for the whole call.
import type { UrlsResultItem } from '@central-code-station/ccs-files-client';import { files } from './files-client';import type { Post } from '../shared/app';
// GET /feed: one call per page mints every URL the page shows. The URLs are short-lived: never store them.export async function renderFeedPage(page: Post[]) { const ids = page.flatMap((post) => post.media.map((m) => m.file_id)); const minted = new Map<string, UrlsResultItem>();
// 1. The `preview` rendition (images). One entry per id, in order; a per-item error never fails the batch. const previews = await files.files.urls(ids, { rendition: 'preview' }); for (const item of previews.items) minted.set(item.file_id, item);
// 2. No preview yet (still processing) or none for this kind (a video): the original, shown inline. const missing = previews.items.filter((i) => i.error === 'rendition_not_available').map((i) => i.file_id); if (missing.length > 0) { const originals = await files.files.urls(missing, { disposition: 'inline' }); for (const item of originals.items) minted.set(item.file_id, item); }
return page.map((post) => ({ ...post, media: post.media.map((m) => ({ ...m, url: minted.get(m.file_id)?.url ?? null, expires_at: minted.get(m.file_id)?.expires_at ?? null })), }));}Choosing what to mint
Section titled “Choosing what to mint”| You show | Mint |
|---|---|
| A grid of thumbnails | rendition: 'thumbnail' (256 px) |
| An image in a post | rendition: 'preview' (1280 px), or large (2048 px) for a full-screen view |
| A video in a post | The original with disposition: 'inline' to play, and rendition: 'poster' for the still frame |
| A PDF or a document to open | The original, inline for the browser’s viewer or attachment to save |
| A “download” button | The original, attachment (the default) |
A rendition that is not ready yet, or that the kind does not have, answers rendition_not_available for that entry: mint the original for those ids, as the sample does, or show a placeholder. Inline originals and renditions count as view; an attachment of the original counts as download and writes an audit row per URL, which is why the feed mints inline.
How long URLs live
Section titled “How long URLs live”URLs live the application’s download_url_ttl_seconds, 15 minutes by default; a machine client may ask for up to 24 hours with ttlSeconds. Rules that keep pages working:
- Mint when you render, and put the URLs in the response. Never store them in your database.
- The app may cache a URL until a minute before its
expires_at, never longer. - A long-lived screen (an open feed) asks your API again for a page whose URLs are about to expire, rather than minting a longer TTL for everything.
Public files
Section titled “Public files”When the tenant allows it and the media is public anyway (visibility: 'public'), the file has a stable public_url, and public_renditions holds the stable URLs of its renditions once they exist. Store them once when the upload completes and the file is ready, and skip minting for those files. See Delivery and visibility.
One file at a time
Section titled “One file at a time”| Route | For |
|---|---|
GET P/files/:file_id/download |
One URL with { url, expires_at, mime_type, size, name } |
GET P/files/:file_id/preview |
A detail page: the best URL for the file’s kind plus every rendition, in one call |
GET P/files/:file_id/content |
A 302 to the URL, for server-side HTTP clients that follow redirects |