Freedam

Image Transformations

Freedam exposes a Cloudflare-style image transformation proxy that lets you build a derivative of any image asset — resized, re-encoded, optionally watermarked — by composing a URL. The URLs are cookieless, ETag-aware, and safe to embed in HTML, RSS feeds, third-party CMSes, or any caching CDN sitting in front of your Freedam instance.

Unlike the /api/v1/... endpoints, transform URLs require no Sanctum token and are not counted against your API rate limit. They have their own per-URL throttle (see Rate limiting below) and are served straight from the tenant host.

There are three transform paths, picked by what kind of access you need:

Path Auth Cache Watermarking Who builds the URL
/i/{options}/{gaid} None Public Never You (the integrator)
/is/{options}/{gaid} Signed + uid Private Per-user download policy Freedam (returned via the API)
/ip/{options}/{gaid} Signed + uid Private Per-user preview policy Freedam (returned via the API)

Heads up. Only /i/ is designed for third-party URL construction. /is/ and /ip/ carry a Laravel signed-URL signature that requires the tenant's secret app key, so external integrators cannot build them — but you will receive them in API payloads and should be ready to render them.

The public endpoint — /i/

GET /i/{options}/{assetGaid}
  • assetGaid — the asset's Global Asset ID (the same identifier used by every /api/v1/assets/{gaid} endpoint).
  • options — a comma-separated list of key=value pairs (described below), or the literal string original to skip any resizing or re-encoding.

A bare GET is all it takes:

curl -o hero.webp \
  "https://acme.freedam.io/i/w=1280,q=80,f=webp/A_01HXYZAB12CDEF"

The response is the binary image, served with:

  • Content-Typeimage/jpeg, image/png, image/webp, or image/avif depending on the resolved format.
  • Cache-Controlpublic, max-age=31536000, s-maxage=31536000, immutable.
  • ETag — strong validator that includes the asset's mutable state. Replays with If-None-Match return 304 Not Modified.
  • Vary: Accept — so format auto-negotiation stays cacheable per client.

Because the response is immutable, CDNs and browsers will cache it for up to a year. If an asset is replaced or re-processed inside Freedam, its ETag rotates and downstream caches refresh on the next conditional request.

Options syntax

Key Meaning Range / values Default
w Target width in pixels 15120 source width (after fit)
h Target height in pixels 15120 source height (after fit)
q Encoder quality 1100 85
f Output format jpeg | jpg | png | webp | avif auto-negotiated from Accept
fit Resize strategy cover | contain | fill contain
dpr Device-pixel-ratio multiplier 13 1

Order does not matter; keys are case-sensitive and short. Examples:

/i/w=800/A_01HXYZ                    # width-only, format auto-negotiated, contain
/i/w=400,h=400,fit=cover/A_01HXYZ    # square crop, 400×400
/i/w=1600,q=70,f=avif/A_01HXYZ       # forced AVIF at lower quality
/i/w=800,dpr=2/A_01HXYZ              # request 1600px image, advertised as 2× of 800 CSS px
/i/original/A_01HXYZ                 # source, no resize/re-encode (format still negotiated)

fit semantics

  • contain (default) — preserve aspect ratio, no crop. The result fits inside w × h and the smaller axis is rounded down to maintain ratio.
  • cover — preserve aspect ratio, crop the overflow so the output exactly fills w × h.
  • fill — stretch to exactly w × h, ignoring aspect ratio.

If only one of w or h is given, the omitted axis is computed from the source aspect ratio.

dpr and the source tier picker

Freedam pre-renders preview tiers at fixed sizes (128, 256, 512, 1024, 2048, 4096, 5120 px on the long edge). The transform endpoint picks the smallest tier ≥ your requested long edge as its source, then resizes down. dpr multiplies the requested edge before picking the tier, so w=800,dpr=2 re-encodes from the 2048 tier (not 1024). This gives crisp Retina output without forcing every request to read the heaviest source.

Format auto-negotiation

If you omit f=, the server inspects the request's Accept header in this order:

  1. image/avif → encodes AVIF (best compression).
  2. image/webp → encodes WebP.
  3. otherwise → encodes JPEG.

If you need a deterministic format (e.g. when emailing a snippet that may render in a client without Accept), set f= explicitly.

Errors

Status Meaning
400 One of the options is out of range or malformed.
403 The request is not in a valid tenant context.
404 The asset does not exist, or no preview tier is available yet.
429 Per-URL transform rate limit hit (see below).
500 The image encoder failed; the request is logged for triage.

Error responses are plain HTTP (no JSON envelope) so they can be rendered by <img> tags without surfacing leaking detail.

Rate limiting

Transform URLs use their own bucket: 10 requests / minute per (ip, asset_gaid, options) triple. This is intentionally generous for cache misses but tight enough to prevent enumeration. Once a CDN or browser caches the response, subsequent requests bypass the origin entirely and never count against the budget.

The /api/v1 token budget documented in Rate Limiting is independent of this throttle.

Tenant context

Transform URLs must be hit on the tenant host that owns the asset (e.g. https://acme.freedam.io/i/..., not the central / marketing domain). Cross-tenant URLs return 403. The host header is the only thing identifying the tenant — there is no token to fall back on.

Signed transforms — /is/ and /ip/

The two signed variants are documented here for completeness, but you will not construct them yourself. They appear in API responses (asset detail, gallery cards, share payloads) when Freedam needs to enforce per-user policy on a transformed image.

/is/{options}/{gaid} — download-class signed transform

GET /is/w=1600,q=85/A_01HXYZ?signature=...&uid=42&wm=7
  • Signature is a Laravel signed-URL signature; tampering invalidates it.
  • uid identifies the user the URL was minted for; their download policy and resolution access are re-checked on every request.
  • wm (optional) pins a specific watermark id. If the user's policy mandates a watermark, the request must carry the matching id or it is rejected.
  • Same options grammar as /i/, same throttle bucket.
  • Responses are Cache-Control: private, max-age=3600 so shared caches won't reuse them across users.

/ip/{options}/{gaid} — preview-class signed transform

Identical to /is/ but enforces the preview ACL (which is laxer than download in many tenancies — e.g. trash-bin viewers can still preview a deleted asset for audit reasons but cannot download it). /ip/ responses include an X-Transform-Cache: hit|miss header indicating whether the persistent watermarked-bytes cache served the request.

If a watermark policy mismatch is detected (e.g. the user's policy was changed after the URL was minted), the request returns 403 with no body. Refresh the asset detail payload to mint a new URL.

Other asset delivery URLs

Beyond the transform endpoints, the API may hand you a handful of other signed URLs. You don't construct these; you just follow them.

URL shape Purpose Notes
/assets/preview/{gaid}/{resolution} Serve a non-watermarked preview at a fixed tier (w_256, w_2048, …). Signed URL, short-lived. Use for <img> src when a pre-rendered tier is enough and no on-the-fly resize needed.
/assets/download/{gaid}/{resolution} Force a download with Content-Disposition: attachment. Signed. resolution=original returns the source file.
/assets/download-prepare/{gaid}/{resolution} Public-facing landing page that auto-triggers the download via JS. Use this in email links so users see a "Your download has started" page rather than a blank tab.
/assets/stream/{gaid}/master.m3u8 HLS master playlist for video assets. Signed. Variant playlists and .m4s segments are independently signed with a 6-hour TTL.
/assets/stream/{gaid}/subtitles/{language}.m3u8 HLS subtitle playlist for a given language tag. Signed. Language tag is a 1–16 char [A-Za-z0-9_-]+.
/embed/{shareToken}/{gaid} Public video embed for <iframe>. Tokens are minted via /api/v1/assets/{gaid}/embed.

Signed URLs returned in API responses include the signature directly — pass them through to your <img>, <a>, or <video> tag unchanged. Do not strip query parameters; doing so will invalidate the signature.

Building transform URLs in your own backend

If you are proxying Freedam assets through your own service, the public /i/ URL is just string concatenation:

// TypeScript
function freedamImageUrl(
    gaid: string,
    opts: {
        width?: number;
        height?: number;
        quality?: number;
        format?: 'jpeg' | 'png' | 'webp' | 'avif';
        fit?: 'cover' | 'contain' | 'fill';
        dpr?: 1 | 2 | 3;
    },
): string {
    const parts: string[] = [];
    if (opts.width) parts.push(`w=${opts.width}`);
    if (opts.height) parts.push(`h=${opts.height}`);
    if (opts.quality) parts.push(`q=${opts.quality}`);
    if (opts.format) parts.push(`f=${opts.format}`);
    if (opts.fit) parts.push(`fit=${opts.fit}`);
    if (opts.dpr) parts.push(`dpr=${opts.dpr}`);
    const options = parts.length ? parts.join(',') : 'original';
    return `https://acme.freedam.io/i/${options}/${gaid}`;
}
# Python
def freedam_image_url(gaid: str, **opts) -> str:
    short = {"width": "w", "height": "h", "quality": "q",
             "format": "f", "fit": "fit", "dpr": "dpr"}
    parts = [f"{short[k]}={v}" for k, v in opts.items() if v is not None]
    options = ",".join(parts) if parts else "original"
    return f"https://acme.freedam.io/i/{options}/{gaid}"

There is no need to URL-encode the options segment — the grammar only uses ASCII identifiers, digits, and the , / = separators that Laravel routes through verbatim.

Worked examples

Responsive <img> srcset. Let the browser pick the right pixel density, with format auto-negotiation:

<img
    src="https://acme.freedam.io/i/w=800/A_01HXYZ"
    srcset="
        https://acme.freedam.io/i/w=400/A_01HXYZ   400w,
        https://acme.freedam.io/i/w=800/A_01HXYZ   800w,
        https://acme.freedam.io/i/w=1600/A_01HXYZ 1600w
    "
    sizes="(max-width: 600px) 100vw, 800px"
    alt="…"
/>

Square thumbnail for a card grid.

https://acme.freedam.io/i/w=300,h=300,fit=cover,q=80/A_01HXYZ

Forced AVIF poster. When you need deterministic AVIF (e.g. for OpenGraph tags consumed by crawlers that do not advertise Accept: image/avif):

https://acme.freedam.io/i/w=1200,h=630,fit=cover,f=avif/A_01HXYZ

Skip resizing, just re-encode. Use original to request the largest preview tier in the negotiated format:

https://acme.freedam.io/i/original/A_01HXYZ

show.relatedDocs.heading

show.relatedDocs.subheading