Media uploads never pass through the API. You request a signed URL, upload directly to storage, then confirm — which keeps large files off the API's request path entirely.
The upload flow
// 1. Ask for a signed destination
const { uploadUrl, assetId } = await api.post("/v1/media/uploads", {
filename: file.name,
contentType: file.type,
bytes: file.size,
});
// 2. Upload straight to storage
await fetch(uploadUrl, { method: "PUT", body: file });
// 3. Confirm, which triggers processing
await api.post(`/v1/media/${assetId}/confirm`);
Signed URLs expire after 15 minutes. An unconfirmed asset is garbage-collected after 24 hours.
Transformations
Derivatives are generated on first request and cached at the edge:
https://cdn.cymbiote.com/<asset-id>/w_1200,f_auto,q_auto/photo.jpg
| Parameter | Meaning |
|---|---|
w_ / h_ | Target width / height in pixels |
f_auto | Negotiate AVIF or WebP from Accept |
q_auto | Quality chosen from image content |
c_fill | Crop to fill, preserving the focal point |
Always send f_auto and q_auto. Hard-coding f_jpg costs your users roughly 40% more bytes for no benefit.
Limits
Maximum 100MB per asset, 2GB per project per day. Supported: JPEG, PNG, WebP, AVIF, SVG, MP4, WebM and PDF. SVGs are sanitised on upload — embedded scripts are stripped, not rejected.