Skip to content

Signed URLs

Signing on img.brandcenter.io follows the Uploadcare/imgix posture: optional by default. What a signature buys is durability, not access:

URLRenders?Persisted at origin?
w: / p: (any)Already pre-generated
Unsigned c: / fx:✓ — CDN-cached✗ ephemeral tier
Validly signed c: / fx:✓ durable tier
Present-but-invalid signature✗ rejected

So you can always paste a crop or effect URL into a browser and tweak it — development stays friction-free. The CMS plugins (Sanity, Payload) derive unsigned URLs by design — they run in browsers, where signing keys never live, and unsigned URLs render forever on the ephemeral tier. Signing is the server-side upgrade for URLs whose renders should be persisted, not re-renderable — apply it where a backend mints or post-processes stored URLs.

s:{kid}:{hmac} is always the last op. The HMAC is SHA-256 over the canonical path — the full pathname with the ,s:{kid}:{hmac} op removed:

payload: /i/nova/FILE/REV/c:0.250000:0.250000:0.500000:0.500000,w:800.webp
signed: /i/nova/FILE/REV/c:0.250000:0.250000:0.500000:0.500000,w:800,s:1:9ff297….webp

kid identifies the signing key, so keys rotate without invalidating URLs signed with the previous key.

Signing keys never ship to browsers — signatures are minted server-side. Inside Brandcenter’s stack that is one call:

let signed = await BcImageUrl.signIPath(path, ~kid, ~secret)

For your own backend, it’s standard HMAC-SHA256: hex-encode HMAC_SHA256(secret, canonicalPath) and append ,s:{kid}:{hex} before the extension. A tampered or wrong-key signature is always rejected — tampering never silently degrades to “unsigned”.