Skip to content

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)

Send the key in one of these headers:

Authorization: Bearer <api_key> (preferred)
X-API-Key: <api_key>
Terminal window
curl https://api.shotvik.com/v1/usage -H "Authorization: Bearer $SHOTVIK_API_KEY"
  • Format: shotvik_<key_id>_<secret>. The key_id (12 characters) is public; you use it as kid in 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_key or token) are refused with 400 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.

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.
  • 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:
Terminal window
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.

  1. Collect all query parameters except sig: kid, template, any vars[<name>], and optionally exp, cache and cache_ttl. Each name may appear only once.
  2. Encode each name and value with encodeURIComponent rules, and join them as name=value.
  3. Sort the pairs by encoded name (byte order), and join them with &. This is the canonical query.
  4. Compute sig = base64url( HMAC-SHA256( signing_secret, "GET\n/v1/og\n" + canonical_query ) ). Use base64url without = padding.
  5. 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.

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 base64
import hashlib
import hmac
import os
from 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:

<?php
function 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 &amp; inside HTML attributes:

<meta property="og:image" content="https://api.shotvik.com/v1/og?cache=true&amp;cache_ttl=604800&amp;kid=…&amp;template=basic&amp;vars%5Bsite%5D=example.com&amp;vars%5Btitle%5D=Hello%20world&amp;sig=…" />

Shotvik is in private beta. Request access: support@shotvik.com · Privacy