Signed URLs
How VectorFast Render signed image URLs work, the exact canonicalization and HMAC-SHA-256 rules, Node.js and Python examples, and how to keep the signing secret safe.
Short answer: a signed URL is GET /i/{template}.{format} with the template parameters in the query string and a sig that is an HMAC-SHA-256 of the path plus the sorted query. The server verifies sig before it reads any cache, renders on the first request, and serves the same bytes with a one-year immutable cache header after that.
Current status
Every url returned by POST /v1/renders, the MCP render tool, and GET /v1/templates is already a signed URL. Per-key signing secrets for building URLs on your own server are not issued yet in early access; the algorithm below is what the server verifies today and is what you will use once they are.
Anatomy
https://vectorfast.ai/i/launch-post.png?badge=New&brand=default&h=630&title=Hello&w=1200&sig=Besvpf_bmr9a…| Query key | Meaning |
|---|---|
| template params | Every template parameter, as a string. Arrays (such as changelog.items) are JSON-encoded. |
brand | Brand kit: default, or an account kit pinned to a version such as bk_…@3. An account kit without @n is rejected so the URL can never change pixels. |
w, h | Size; must be one of the template's sizes. Defaults to the template's default size. |
scale | 2 for double-density output; omit for 1. |
v | Reserved for a template major-version pin; it is signed but does not select a version yet. |
k | Public key id of the key that signed the URL. |
sig | The signature. Always last. |
brand, k, v, w, h, scale, and sig are reserved; no template uses them as parameter names.
How the signature is computed
- Take every query pair except
sig. - Decode, then re-encode each key and value with RFC 3986 rules:
encodeURIComponent, plus!,',(,), and*percent-encoded. - Sort the pairs by encoded key, then by encoded value, comparing byte by byte.
- Join them as
key=valuewith&to get the canonical query. - The payload is
/i/{template}.{format}?followed by the canonical query. Signing the path means a URL signed for one template or format cannot be replayed against another. sig = base64url(HMAC-SHA-256(secret, payload))without padding.
Because values are decoded before re-encoding, a=1%20b and a=1+b-style differences in escaping do not change the signature.
Node.js
import { createHmac } from 'node:crypto'
// Server-side only. Never ship the signing secret to a browser, a mobile app or a repo.
const SIGNING_SECRET = process.env.VECTORFAST_SIGNING_SECRET
function encode(value) {
return encodeURIComponent(value).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
}
export function signRenderUrl({ template, format = 'png', params, brand, width, height, keyId }, secret = SIGNING_SECRET) {
const query = {}
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null)
continue
query[key] = typeof value === 'string' ? value : Array.isArray(value) ? JSON.stringify(value) : String(value)
}
if (brand) query.brand = brand
if (keyId) query.k = keyId
if (width) query.w = String(width)
if (height) query.h = String(height)
const canonical = Object.entries(query)
.map(([key, value]) => [encode(key), encode(value)])
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0))
.map(([key, value]) => key + '=' + value)
.join('&')
const path = '/i/' + template + '.' + format
const sig = createHmac('sha256', secret).update(path + '?' + canonical).digest('base64url')
return 'https://vectorfast.ai' + path + '?' + (canonical ? canonical + '&' : '') + 'sig=' + sig
}Python
import base64
import hashlib
import hmac
import json
import os
from urllib.parse import quote
# Server-side only. Never ship the signing secret to a browser, a mobile app or a repo.
SIGNING_SECRET = os.environ.get("VECTORFAST_SIGNING_SECRET", "")
def _encode(value: str) -> str:
return quote(value, safe="-_.~")
def sign_render_url(template, params, fmt="png", brand=None, width=None, height=None, key_id=None, secret=SIGNING_SECRET):
query = {}
for key, value in params.items():
if value is None:
continue
query[key] = json.dumps(value, ensure_ascii=False, separators=(",", ":")) if isinstance(value, list) else str(value)
if brand:
query["brand"] = brand
if key_id:
query["k"] = key_id
if width:
query["w"] = str(width)
if height:
query["h"] = str(height)
pairs = sorted((_encode(k), _encode(v)) for k, v in query.items())
canonical = "&".join(f"{k}={v}" for k, v in pairs)
path = f"/i/{template}.{fmt}"
digest = hmac.new(secret.encode(), f"{path}?{canonical}".encode(), hashlib.sha256).digest()
sig = base64.urlsafe_b64encode(digest).decode().rstrip("=")
return f"https://vectorfast.ai{path}?{canonical + '&' if canonical else ''}sig={sig}"Python's quote with safe="-_.~" encodes exactly the RFC 3986 reserved set, and sorting the encoded tuples sorts by key, then value, as the server does.
Responses
| Situation | Status | Error code |
|---|---|---|
| Valid signature, first request | 200, X-Cache: miss | — |
| Valid signature, repeat | 200, X-Cache: hit-edge or hit-r2 | — |
Missing, malformed, or wrong sig; any changed parameter | 403 | invalid_signature |
| Unknown template | 404 | template_not_found |
| Unknown or deleted brand kit | 404 | brand_kit_not_found |
| Bad format, size, or params; a parameter given twice | 400 | invalid_params |
Successful responses carry Cache-Control: public, max-age=31536000, immutable, X-Cache-Key, and X-Cache. The signature is checked before any cache is read, so a tampered URL never reaches cached bytes.
Keep the secret safe
- Sign on your server or at build time; send only finished URLs to browsers.
- Anyone with a signed URL can load that one image; they cannot change a single parameter without invalidating
sig. - Rotating the signing secret invalidates every URL signed with it. Images already rendered stay in the R2 cache under their cache key and are served again to newly signed URLs with the same inputs.
Updated 2026-10-30 · Markdown version
REST API reference
Every VectorFast Render /v1 endpoint with its parameters, request body, response fields, and status codes, generated from the OpenAPI 3.1 document.
Caching and versions
How VectorFast Render caches renders, what goes into the cache key, how template and brand kit versions work, and how idempotency keys make retries safe.