Signed URLs
Mint short-lived, cross-origin proxy URLs server-side using HMAC-SHA256.
Signed URLs
Signed URLs let you authorize proxy traffic without exposing source origins to an allowlist. The signing secret stays on your backend; the URL carries an HMAC + expiry.
When to use signed mode
- You sign URLs server-side and embed them per-request (most apps).
- Short-lived URLs (e.g. paywalled content, A/B variants).
- You want to proxy sources from origins you don't fully control.
If your sources live on a small set of fixed domains, allowlist mode is simpler — skip this guide.
Enable signing
bashcurl -X PUT https://motioness.com/api/projects/$PROJECT_ID/proxy-config \ -H 'Content-Type: application/json' -b cookie.txt \ -d '{ "enable_signing": true }'
This mints a signing secret on first save. Read it from Project → Settings → Proxy → "Signing secret" — it's shown once. Store it as MOTIONESS_SIGNING_SECRET in your backend env.
To rotate it later:
bashcurl -X POST "https://motioness.com/api/projects/$PROJECT_ID/keys/rotate?which=signing" \ -b cookie.txt
URL shape
texthttps://motioness.com/v1/{projectKey}/{base64url(srcUrl)}.{format} ?sig={hmacSha256Hex}&exp={unixSeconds}
| Component | Notes |
|---|---|
base64url(srcUrl) | URL-safe base64 of the full source URL with scheme. Strip trailing = padding. |
sig | Hex-encoded HMAC-SHA256 of ${projectKey}:${srcUrl}:${exp} keyed with the signing secret. |
exp | Unix seconds at which the URL expires. Server rejects with 403 once the clock passes this. |
Recipes
tsimport crypto from 'node:crypto';import { buildProxyUrl } from '@motioness/proxy-client';function base64UrlEncode(s: string) { return Buffer.from(s).toString('base64url');}function sign(secret: string, data: string) { return crypto.createHmac('sha256', secret).update(data).digest('hex');}export function motionessSignedUrl(srcUrl: string, ttlSeconds = 3600) { const projectKey = process.env.MOTIONESS_PROJECT_KEY!; const secret = process.env.MOTIONESS_SIGNING_SECRET!; const exp = Math.floor(Date.now() / 1000) + ttlSeconds; const sig = sign(secret, `${projectKey}:${srcUrl}:${exp}`); return buildProxyUrl({ origin: 'https://motioness.com', projectKey, src: srcUrl, signature: sig, exp, });}
tsasync function hmacSha256Hex(secret: string, data: string) { const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'], ); const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data)); return [...new Uint8Array(sig)] .map((b) => b.toString(16).padStart(2, '0')) .join('');}export async function motionessSignedUrl(env: Env, srcUrl: string) { const exp = Math.floor(Date.now() / 1000) + 3600; const sig = await hmacSha256Hex(env.MOTIONESS_SIGNING_SECRET, `${env.MOTIONESS_PROJECT_KEY}:${srcUrl}:${exp}`); const b64 = btoa(srcUrl).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); return `https://motioness.com/v1/${env.MOTIONESS_PROJECT_KEY}/${b64}.mp4?sig=${sig}&exp=${exp}`;}
pythonimport base64import hashlibimport hmacimport osimport timefrom urllib.parse import urlencodedef motioness_signed_url(src_url: str, ttl_seconds: int = 3600) -> str: project_key = os.environ['MOTIONESS_PROJECT_KEY'] secret = os.environ['MOTIONESS_SIGNING_SECRET'].encode() exp = int(time.time()) + ttl_seconds msg = f'{project_key}:{src_url}:{exp}'.encode() sig = hmac.new(secret, msg, hashlib.sha256).hexdigest() body = base64.urlsafe_b64encode(src_url.encode()).rstrip(b'=').decode() params = urlencode({'sig': sig, 'exp': exp}) return f'https://motioness.com/v1/{project_key}/{body}.mp4?{params}'
gopackage motionessimport ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "os" "time")func SignedURL(srcURL string, ttl time.Duration) string { key := os.Getenv("MOTIONESS_PROJECT_KEY") secret := []byte(os.Getenv("MOTIONESS_SIGNING_SECRET")) exp := time.Now().Add(ttl).Unix() h := hmac.New(sha256.New, secret) h.Write([]byte(fmt.Sprintf("%s:%s:%d", key, srcURL, exp))) sig := hex.EncodeToString(h.Sum(nil)) body := base64.RawURLEncoding.EncodeToString([]byte(srcURL)) return fmt.Sprintf( "https://motioness.com/v1/%s/%s.mp4?sig=%s&exp=%d", key, body, sig, exp, )}
Verifying client-side
The browser doesn't verify anything — it just hits the URL. The server verifies. Don't put the signing secret in client-side code.
If you embed signed URLs in HTML returned from your CDN, set a short exp and let the cache expire naturally. For pages that update frequently, sign at request time in your edge function or backend.
Common mistakes
Signed mode requires the src URL to be encoded. Allowlist mode uses plain text. Mixing them returns 403.
The signature MUST be over ${projectKey}:${srcUrl}:${exp} exactly. Different separator? Forgot to include exp? URL-encoded srcUrl? Server returns 403 with no detail (security).
To debug, log the exact string you sign and the exact exp; compare against what the worker reconstructs.
The server rejects when now > exp. If your backend's clock is ahead of the worker's, you might over-shorten the TTL. Stick to >= 60s.
+ and / characters break URL parsing. Use base64url (Buffer.toString('base64url') in Node, RawURLEncoding in Go, etc.) and strip = padding.