Image Transformations
Freedam serves resized and re-encoded renditions of image assets through an image transformation proxy. Every transform URL is signed by Freedam for one user: you receive these URLs in API responses and use them unchanged. You cannot build or modify a transform URL yourself, because the signature covers the whole URL, including the options.
Unlike the /api/v1/... endpoints, transform URLs take no API token and are not counted against your API rate limit. The signature in the URL is the credential. They have their own per-URL throttle (see Rate limiting below) and are served from the tenant host.
There are two transform paths:
| Path | Auth | Cache | Watermarking | Where you get the URL |
|---|---|---|---|---|
/ip/{options}/{gaid} |
Signed + uid |
Private | Per-user preview policy | media_urls.thumbnails of every asset in the API |
/is/{options}/{gaid} |
Signed + uid |
Private | Per-user download policy | Issued inside the Freedam application, not by /api/v1 |
No public transform URL. The unsigned
/i/{options}/{gaid}path no longer exists and answers404. It served images without checking who asked. Use the signed URLs frommedia_urlsinstead.
Getting transform URLs from the API
GET /v1/assets and GET /v1/assets/{gaid} return a media_urls object for each asset (see API Endpoints). Its thumbnails value holds one signed /ip/ URL per rendition tier:
{
"media_urls": {
"thumbnails": {
"w_5120": "https://{workspace}.freedam.io/ip/w=5120/IM-20260309-UDXZH8PP6VPW?expires=…&uid=42&v=…&signature=…",
"w_4096": "https://{workspace}.freedam.io/ip/w=4096/IM-20260309-UDXZH8PP6VPW?…",
"w_2048": "https://{workspace}.freedam.io/ip/w=2048/IM-20260309-UDXZH8PP6VPW?…",
"w_1024": "https://{workspace}.freedam.io/ip/w=1024/IM-20260309-UDXZH8PP6VPW?…",
"w_512": "https://{workspace}.freedam.io/ip/w=512/IM-20260309-UDXZH8PP6VPW?…",
"w_256": "https://{workspace}.freedam.io/ip/w=256/IM-20260309-UDXZH8PP6VPW?…",
"w_128": "https://{workspace}.freedam.io/ip/w=128/IM-20260309-UDXZH8PP6VPW?…"
},
"base_preview": "https://{workspace}.freedam.io/assets/preview/IM-20260309-UDXZH8PP6VPW/base_preview?…",
"original": "…",
"hls_master": null,
"converted_pdf": null,
"download": "…"
}
}
Throughout this page {workspace} stands for your own workspace subdomain.
thumbnailsisnullfor audio and for any asset that has no rendition: an asset still being processed, one whose preview generation failed, a file type with no preview, or an animated image that skips preview generation.base_previewisnullin the same cases. Show a placeholder, or useoriginalordownload.- Each URL is valid for 1 to 2 hours. The expiry is rounded to the hour, so the URL of a tier stays the same between two requests made within the same hour, which lets browsers cache the image. After expiry the URL answers
403. Read the asset again to get new URLs. Do not store them. - Each URL is bound to the token's user through
uid. That user's access to the asset, preview policy and watermark policy are checked again on every request, so a URL stops working when the user loses access. vchanges when the asset is updated, so a new rendition gets a new URL.
A bare GET is all it takes:
curl -o thumbnail.webp -H "Accept: image/webp" \
"https://{workspace}.freedam.io/ip/w=512/IM-20260309-UDXZH8PP6VPW?expires=…&uid=42&v=…&signature=…"
Response
The response is the binary image, served with:
Content-Type:image/jpeg,image/png,image/webp, orimage/avif, depending on the format served.Cache-Control:private, max-age=3600. Shared caches and CDNs must not reuse the response across users.ETag: a validator that changes with the asset, the options, the format and the watermark policy. A request with a matchingIf-None-Matchreturns304 Not Modified.Vary: Accept, because the format depends on theAcceptheader.X-Transform-Cache: hit|misson/ip/, telling whether the stored copy of the rendition served the request.
/ip/ never enlarges an image: an original smaller than the requested width is served at its real size.
Format negotiation
The URLs returned by the API carry no f= option, so the server reads the request's Accept header in this order:
image/avif: encodes AVIF.image/webp: encodes WebP.- otherwise: encodes JPEG.
Send the Accept header that matches what your client can display. A browser <img> tag does this by itself.
When an AVIF rendition is not ready yet, /ip/ serves a WebP image at once and encodes the AVIF in the background. That response has Content-Type: image/webp, X-Image-Fallback: 1 and Cache-Control: private, max-age=5, must-revalidate, so the next request gets the AVIF.
Options syntax
options is a comma-separated list of key=value pairs. The API only issues w=… URLs. The other keys are listed so you can read the URLs Freedam issues elsewhere. Changing any of them invalidates the signature.
| Key | Meaning | Range / values | Default |
|---|---|---|---|
w |
Target width in pixels | 1–5120 |
source width (after fit) |
h |
Target height in pixels | 1–5120 |
source height (after fit) |
q |
Encoder quality | 1–100 |
85 |
f |
Output format | jpeg | jpg | png | webp | avif |
negotiated from Accept |
fit |
Resize strategy | cover | contain | fill |
contain |
dpr |
Device-pixel-ratio multiplier | 1–3 |
1 |
fit semantics
contain(default): preserve aspect ratio, no crop. The result fits insidew × h.cover: preserve aspect ratio, crop the overflow so the output exactly fillsw × h.fill: stretch to exactlyw × h, ignoring aspect ratio.
If only one of w or h is given, the omitted axis is computed from the source aspect ratio.
Rendition tiers
Freedam stores renditions at fixed sizes (128, 256, 512, 1024, 2048, 4096, 5120 px). A transform reads the smallest tier at least as large as the requested long edge, then resizes down. dpr multiplies the requested edge before the tier is picked, so w=800,dpr=2 reads the 2048 tier, not the 1024 one.
Errors
| Status | Meaning |
|---|---|
400 |
One of the options is out of range or malformed. |
403 |
The signature is invalid or expired, the URL was changed, the request is not on the tenant host, or the user in uid may no longer preview the asset or needs a different watermark. |
404 |
The asset does not exist, or it has no rendition. |
429 |
Per-URL transform rate limit hit (see below). |
500 |
The image encoder failed. The request is logged. |
A 403 usually means the URL has expired or the user's access changed after the URL was issued. Read the asset again through the API to get a new URL.
Rate limiting
Transform URLs use their own limit: 10 requests per minute for each combination of client IP address, asset GAID and options. A browser that has cached the response does not send the request again, so it does not count against the limit.
The /api/v1 token budget documented in Rate Limiting is independent of this throttle.
Tenant context
Transform URLs must be requested on the tenant host that owns the asset (for example https://{workspace}.freedam.io/ip/..., not the central or marketing domain). The URLs returned by the API already use that host.
Signed transforms: /is/ and /ip/
/ip/{options}/{gaid}: preview-class signed transform
GET /ip/w=1024/IM-20260309-UDXZH8PP6VPW?expires=...&uid=42&v=...&signature=...
signaturecovers the whole URL. Changing the path or any query parameter invalidates it.uididentifies the user the URL was issued for. That user must be able to view the asset, and their access rules must allow a preview. Both are checked on every request. An asset in the Trash is only served to users who can open the Trash.wm(optional) names the watermark the user's preview policy applies. If the policy requires a watermark and the URL carries none, or carries a different one, the request returns403.- A URL without
uidbelongs to a Public Portal guest and only works while the Public Portal is enabled.
/is/{options}/{gaid}: download-class signed transform
GET /is/w=1600,q=85/IM-20260309-UDXZH8PP6VPW?expires=...&uid=42&wm=7&signature=...
Same options grammar, signature, throttle and Cache-Control: private, max-age=3600 as /ip/, but it checks the user's download policy: access to the asset, access to the resolution tier the request reads, and the download watermark. It can enlarge an image to the requested size. The /api/v1 endpoints do not return /is/ URLs.
If the user's watermark policy changed after the URL was issued, both paths return 403. Read the asset again to get a new URL.
Other asset delivery URLs
media_urls also holds signed URLs that are not transforms. You don't construct these; you just follow them.
| URL shape | Purpose | Notes |
|---|---|---|
/assets/preview/{gaid}/{resolution} |
Serve a stored rendition inline. | Signed, valid 1 hour. Returned as media_urls.base_preview (the largest stored preview image), media_urls.converted_pdf (the PDF rendition of an Office document) and, for files other than video, audio and PDF, media_urls.original. |
/assets/download/{gaid}/{resolution} |
Force a download with Content-Disposition: attachment. |
Signed, valid 1 hour. Returned as media_urls.download, with resolution=original. The user's download rights and watermark policy are checked when the URL is followed. |
/assets/download-prepare/{gaid}/{resolution} |
Landing page that starts the download with JavaScript. | Signed. Used in the emails Freedam sends, so the recipient sees a "Your download has started" page. Not returned by the API. |
/assets/stream/{gaid}/master.m3u8 |
HLS master playlist for video assets. | Signed, valid 6 hours. Returned as media_urls.hls_master when the video has an HLS rendition, otherwise null. Variant playlists and segments are signed separately, also for 6 hours. |
/assets/stream/{gaid}/subtitles/{language}.m3u8 |
HLS subtitle playlist for a given language tag. | Signed. Referenced by the master playlist. The language tag is 1 to 16 characters of [A-Za-z0-9_-]. |
/embed/{shareToken}/{gaid} |
Public video embed for <iframe>. |
Tokens are created with /api/v1/assets/{gaid}/embed. |
For video, audio and PDF assets, media_urls.original is a direct URL to the stored file, so it can be used as the source of a <video>, <audio> or <embed> tag. On workspaces with remote storage it is a presigned storage URL valid for 1 hour.
Pass every signed URL through to your <img>, <a>, or <video> tag unchanged. Do not strip or reorder query parameters: doing so invalidates the signature.
Using transform URLs in your own backend
Because the URLs are bound to the token's user and expire, treat them as short-lived values: read the asset when you render the page, or proxy and cache the image bytes on your side. With the TypeScript SDK:
const asset = await client.assets.get('IM-20260309-UDXZH8PP6VPW');
const thumbnails = asset.media_urls.thumbnails;
if (thumbnails === null) {
// No rendition: show a placeholder, or link to asset.media_urls.download
} else {
const response = await fetch(thumbnails.w_1024, {
headers: { Accept: 'image/webp' },
});
const bytes = await response.arrayBuffer();
}
Worked examples
Responsive <img> srcset. Use the tiers from media_urls.thumbnails and let the browser pick one. The format is negotiated from the browser's Accept header:
<img
src="{thumbnails.w_1024}"
srcset="
{thumbnails.w_512} 512w,
{thumbnails.w_1024} 1024w,
{thumbnails.w_2048} 2048w
"
sizes="(max-width: 600px) 100vw, 800px"
alt="…"
/>
Replace each {thumbnails.…} placeholder with the matching URL from the API response when you render the page.
Small thumbnail for a card grid. Use thumbnails.w_256 or thumbnails.w_512 and size the image with CSS (object-fit: cover for a square crop).
Full-size image. Use thumbnails.w_5120 for the largest rendition, or media_urls.download for the original file.