Signed URLs and domain allowlists
When an OGKit image URL is public (embedded in HTML that anyone can view), a raw API key in the query string is a credential leak. Signed URLs and domain allowlists let you keep keys server-side while still serving deterministic 1200×630 PNGs to Slack, LinkedIn, Discord, and crawlers.
When you need signing
Use require-signed-urls on a key when the image URL will appear in public HTML, sitemaps, or RSS. Scrapers fetch the URL without cookies; they do not need your key — they need a URL that already includes a valid HMAC signature (or a key that does not require signing).
Domain allowlists are complementary: if a key has allowed domains, pass domain=example.com (or your claim) and OGKit rejects requests whose domain claim is outside the list.
- Public marketing pages and blog posts → prefer signed URLs
- Internal previews / CI → demo=1 or a key without signing is fine
- Multiple brands on one account → one key per domain allowlist
How signing works
Enable “Require signed URLs” on the key in the dashboard. Then every GET to /api/og/{template} (and /api/og/auto) with that key must include sig=…
Canonical string: pathname + ? + sorted query string with sig removed. Sign with HMAC-SHA256 using the full API key (ogk_live_…) as the secret. Digest is lowercase hex.
Important: sort query parameters before hashing. Changing parameter order without re-sorting produces a different signature and returns invalid_signature.
import { createHmac } from "node:crypto";
function signOgUrl(rawUrl: string, apiKey: string) {
const url = new URL(rawUrl);
const params = new URLSearchParams(url.searchParams);
params.delete("sig");
params.sort();
const query = params.toString();
const canonical = query ? `${url.pathname}?${query}` : url.pathname;
const sig = createHmac("sha256", apiKey).update(canonical).digest("hex");
params.set("sig", sig);
url.search = params.toString();
return url.toString();
}
const unsigned = new URL("https://www.webmorp.art/ogkit/api/og/article");
unsigned.searchParams.set("key", process.env.OGKIT_KEY!);
unsigned.searchParams.set("title", "Ship notes");
unsigned.searchParams.set("domain", "example.com");
const imageUrl = signOgUrl(unsigned.toString(), process.env.OGKIT_KEY!);
// → put imageUrl in metadata.openGraph.imagesPython example
import hashlib, hmac
from urllib.parse import urlencode, urlparse, parse_qsl, urlunparse
def sign_og_url(raw_url: str, api_key: str) -> str:
parts = urlparse(raw_url)
params = [(k, v) for k, v in parse_qsl(parts.query, keep_blank_values=True) if k != "sig"]
params.sort(key=lambda kv: kv[0])
query = urlencode(params)
canonical = f"{parts.path}?{query}" if query else parts.path
sig = hmac.new(api_key.encode(), canonical.encode(), hashlib.sha256).hexdigest()
params.append(("sig", sig))
return urlunparse(parts._replace(query=urlencode(params)))Errors
- missing_signature — key requires signing but sig is absent
- invalid_signature — HMAC mismatch (wrong key, unsorted params, or mutated query)
- domain_not_allowed — domain claim not in the key allowlist
Operational tips
Sign on the server at request time (or at build time for static pages). Never ship the raw API key to the browser just to compute sig.
If you change title/subtitle after deploy, regenerate the signature — the signed URL is bound to the exact query string.
During open access, signing is optional for evaluation, but still recommended once URLs are public.
FAQ
Do social networks need my API key?
No. Scrapers only fetch the final HTTPS image URL. Signing lets that URL be public without embedding a usable bare key.
Can I sign demo=1 URLs?
demo=1 skips API key auth, so signatures are not required. Use signing with a real key for production metadata.
What is the domain parameter for?
Optional claim checked against the key’s allowlist. Use it when one account serves multiple properties and each key is scoped to a hostname.
Related
OGKit turns one HTTPS URL into a 1200×630 Open Graph image. Read the API reference, deep guides, the Open Graph SEO guide, try the Playground, or sign in to create API keys.