Authentication — API keys and signed URLs
Shotvik uses two ways to authenticate a request.
| Use case | Method |
|---|---|
| Server-side calls (your backend, scripts, CI) | API key in a request header |
Public image links, e.g. <meta property="og:image"> |
Signed URL for GET /v1/og (no API key in the URL) |
API keys
Section titled “API keys”Send the key in one of these headers:
Authorization: Bearer <api_key> (preferred)X-API-Key: <api_key>curl https://api.shotvik.com/v1/usage -H "Authorization: Bearer $SHOTVIK_API_KEY"- Format:
shotvik_<key_id>_<secret>. Thekey_id(12 characters) is public; you use it askidin signed URLs. The rest is secret. - Shown once: when a key is created, you see the key and its signing secret once. Shotvik stores only a hash of the key, so it can’t show an existing key again. If you lose a key, contact support@shotvik.com to revoke it and get a new one.
- Never in the URL: requests with a key in the query string (
api_key,key,apikey,access_keyortoken) are refused with400 api_key_in_url. - Treat keys like passwords. Never put a key in front-end code, HTML or a public repository. If a key may have leaked, contact support@shotvik.com right away so it can be revoked. Revoking takes effect immediately.
Authentication errors: 401 unauthorized (no key), 401 invalid_api_key, 401 api_key_revoked, 403 key_suspended, 403 account_frozen. See Errors.
Signed URLs for og:image links
Section titled “Signed URLs for og:image links”Social networks and chat apps fetch your og:image URL directly, so you can’t send a header. A signed URL proves the link was created by you without exposing your API key. Changing any parameter invalidates the signature, so nobody can reuse the link to render something else on your quota.
You need:
- your key ID (
kid); - your key’s signing secret, a separate secret that belongs to the key. Keep it on your server only.
Your signing secret
Section titled “Your signing secret”- Shown once, at creation. The signing secret is shown together with the API key when the key is created, and only then. Store it right away, for example in your server’s environment as
SHOTVIK_SIGNING_SECRET. - Rotate it if you lost it or it may have leaked. Rotating returns a new secret, once, and the old one stops working immediately, so every link signed with it fails until you re-sign. Call the API with any key of the same account:
curl -X POST https://api.shotvik.com/v1/keys/<key_id>/rotate-signing-secret \ -H "Authorization: Bearer $SHOTVIK_API_KEY"# → 200 {"key_id": "<key_id>", "signing_secret": "sig_…"}A key ID that isn’t in your account returns 404 not_found; a revoked key returns 400 invalid_request.
How the signature works
Section titled “How the signature works”- Collect all query parameters except
sig:kid,template, anyvars[<name>], and optionallyexp,cacheandcache_ttl. Each name may appear only once. - Encode each name and value with
encodeURIComponentrules, and join them asname=value. - Sort the pairs by encoded name (byte order), and join them with
&. This is the canonical query. - Compute
sig = base64url( HMAC-SHA256( signing_secret, "GET\n/v1/og\n" + canonical_query ) ). Use base64url without=padding. - The URL is
https://api.shotvik.com/v1/og?+ canonical query +&sig=+ sig.
Add exp (a Unix timestamp in seconds) to make the link expire; after that it returns 403 signature_expired. Revoking or blocking the key, or rotating its signing secret, invalidates all of its signed URLs.
Examples
Section titled “Examples”JavaScript / TypeScript (Node.js):
import { createHmac } from 'node:crypto';
function signedOgUrl(kid, signingSecret, template, vars = {}, extra = {}) { const params = [['kid', kid], ['template', template], ...Object.entries(extra)]; for (const [name, value] of Object.entries(vars)) params.push([`vars[${name}]`, String(value)]); const canonical = params .map(([k, v]) => [encodeURIComponent(k), encodeURIComponent(v)]) .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)) .map(([k, v]) => `${k}=${v}`) .join('&'); const sig = createHmac('sha256', signingSecret).update(`GET\n/v1/og\n${canonical}`).digest('base64url'); return `https://api.shotvik.com/v1/og?${canonical}&sig=${sig}`;}
const url = signedOgUrl(process.env.SHOTVIK_KEY_ID, process.env.SHOTVIK_SIGNING_SECRET, 'basic', { title: 'Hello world', site: 'example.com' }, { cache: 'true', cache_ttl: '604800' });Python:
import base64import hashlibimport hmacimport osfrom urllib.parse import quote
def _enc(s: str) -> str: # Same characters as JavaScript's encodeURIComponent leaves unescaped return quote(s, safe="-_.!~*'()")
def signed_og_url(kid, signing_secret, template, variables=None, extra=None): params = [("kid", kid), ("template", template)] + list((extra or {}).items()) params += [(f"vars[{name}]", str(value)) for name, value in (variables or {}).items()] pairs = sorted((_enc(k), _enc(v)) for k, v in params) canonical = "&".join(f"{k}={v}" for k, v in pairs) digest = hmac.new(signing_secret.encode(), f"GET\n/v1/og\n{canonical}".encode(), hashlib.sha256).digest() sig = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() return f"https://api.shotvik.com/v1/og?{canonical}&sig={sig}"
url = signed_og_url(os.environ["SHOTVIK_KEY_ID"], os.environ["SHOTVIK_SIGNING_SECRET"], "basic", {"title": "Hello world", "site": "example.com"}, {"cache": "true", "cache_ttl": "604800"})PHP:
<?phpfunction shotvik_enc(string $s): string { // rawurlencode plus the characters encodeURIComponent leaves unescaped return strtr(rawurlencode($s), ['%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')']);}
function shotvik_signed_og_url(string $kid, string $secret, string $template, array $vars = [], array $extra = []): string { $params = [['kid', $kid], ['template', $template]]; foreach ($extra as $k => $v) { $params[] = [(string) $k, (string) $v]; } foreach ($vars as $k => $v) { $params[] = ["vars[$k]", (string) $v]; } $pairs = array_map(fn($p) => [shotvik_enc($p[0]), shotvik_enc($p[1])], $params); usort($pairs, fn($a, $b) => strcmp($a[0], $b[0])); $canonical = implode('&', array_map(fn($p) => $p[0] . '=' . $p[1], $pairs)); $raw = hash_hmac('sha256', "GET\n/v1/og\n" . $canonical, $secret, true); $sig = rtrim(strtr(base64_encode($raw), '+/', '-_'), '='); return 'https://api.shotvik.com/v1/og?' . $canonical . '&sig=' . $sig;}
$url = shotvik_signed_og_url(getenv('SHOTVIK_KEY_ID'), getenv('SHOTVIK_SIGNING_SECRET'), 'basic', ['title' => 'Hello world', 'site' => 'example.com'], ['cache' => 'true', 'cache_ttl' => '604800']);Then put the URL in your page. Escape & as & inside HTML attributes:
<meta property="og:image" content="https://api.shotvik.com/v1/og?cache=true&cache_ttl=604800&kid=…&template=basic&vars%5Bsite%5D=example.com&vars%5Btitle%5D=Hello%20world&sig=…" />Shotvik is in private beta. Request access: support@shotvik.com · Privacy