imgbt
Guides

Signed URLs

HMAC-SHA256 signed URLs for secure asset access.

Signed URLs

Signed URLs provide time-limited access to assets using HMAC-SHA256 signatures.

Signing alone does not restrict access

A vault is public by default, and a public vault serves its assets to anyone with the URL. Signing a URL for a public vault does not change that: the unsigned URL still works, because there is nothing to enforce.

To make signatures mandatory, set the vault to private:

curl -X PATCH https://api.imgbt.com/api/v1/vaults/$VAULT_ID \
  -H "Content-Type: application/json" \
  -d '{"visibility":"private"}'

A private vault rejects every unsigned request with 403, including requests that carry only transform parameters. Its responses are also sent Cache-Control: private, no-store and are never written to the shared edge cache.

How It Works

  1. Your server generates a signed URL with an expiration time
  2. The URL includes token and expires query parameters
  3. imgbt verifies the signature at the edge before serving the asset
  4. Expired or tampered URLs are rejected with a 403 response
  5. On a private vault, a missing signature is also rejected with 403

Getting your signing secret

Every vault is created with one. Owners and admins can read it:

curl https://api.imgbt.com/api/v1/vaults/$VAULT_ID/signing-secret
# => {"signing_secret":"..."}

It is deliberately not included in the normal vault response, and members cannot read it.

Rotating it

Rotation is how you revoke leaked links — it invalidates every outstanding signed URL for the vault immediately.

curl -X POST https://api.imgbt.com/api/v1/vaults/$VAULT_ID/signing-secret/rotate
# => {"signing_secret":"<new value>"}

Generating Signed URLs

Signing Payload

The signing payload is constructed from three components separated by newlines:

{path}\n{sorted_query_params}\n{expires}
  • path: The URL pathname (e.g., /marketing/blog/hero/photo.jpg)
  • sorted_query_params: Transform parameters sorted alphabetically by key (excluding token and expires)
  • expires: Unix timestamp in seconds

Example (Node.js)

import { createHmac } from 'crypto'

function signUrl(url, secret, expiresAt) {
  const urlObj = new URL(url)
  const path = urlObj.pathname

  // Sort query params (excluding token and expires)
  const params = new URLSearchParams(urlObj.search)
  params.delete('token')
  params.delete('expires')
  params.sort()

  const payload = `${path}\n${params.toString()}\n${expiresAt}`
  const signature = createHmac('sha256', secret).update(payload).digest('base64url')

  urlObj.searchParams.set('expires', String(expiresAt))
  urlObj.searchParams.set('token', signature)
  return urlObj.toString()
}

// Sign a URL that expires in 1 hour
const expiresAt = Math.floor(Date.now() / 1000) + 3600
const signed = signUrl(
  'https://cdn.example.com/photos/album/main/photo.jpg?w=800&format=webp',
  'your-vault-signing-secret',
  expiresAt,
)

Vault Signing Secret

The signing secret is stored in your vault settings. You can view it in the dashboard under Vault Settings. Keep this secret secure — anyone with the secret can generate valid signed URLs.

Caching Behavior

Signed URLs bypass the edge cache to prevent unauthorized access via cached responses. Each request is verified independently.