OG image API — generate 1200×630 Open Graph images
Generates 1200×630 PNG Open Graph images from an HTML template. Templates are plain HTML and CSS rendered in real Chromium, so CSS grid and web fonts work.
There are two ways to get an image:
| Use | Endpoint | Auth |
|---|---|---|
| Server-side (build step, backend, cron) | POST /v1/og |
API key |
Public link in <meta property="og:image"> |
GET /v1/og?…&sig=… |
Signed URL |
POST /v1/og
Section titled “POST /v1/og”POST https://api.shotvik.com/v1/ogContent-Type: application/jsonAuthorization: Bearer <api_key>Send exactly one of template or html. Unknown fields are rejected.
| Field | Type | Default | Description |
|---|---|---|---|
template |
string | A built-in template (basic, article) or the ID of one of your stored templates (tpl_…). |
|
html |
string | Inline template HTML with {{var}} placeholders, max 1,048,576 bytes. |
|
vars |
object of strings | Template variables: at most 30, each value up to 1,000 characters. Names use A–Z, a–z, 0–9 and _ (max 40 characters). |
|
cache |
boolean | false |
Opt-in cache on our Hetzner servers. Cache hits don’t count toward your quota. |
cache_ttl |
integer, 60–2592000 | 86400 |
Cache lifetime in seconds (max 30 days, provisional). Only with cache: true. |
The output size is always 1200×630 and the format is always PNG. Width, height and format can’t be set here.
curl https://api.shotvik.com/v1/og \ -H "Authorization: Bearer $SHOTVIK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"template": "basic", "vars": {"title": "Hello", "subtitle": "My first OG image", "site": "example.com"}}' \ --fail-with-body --output og.pngResponse: 200 with Content-Type: image/png and the headers listed under Screenshot a URL, except X-Target-Status. Errors: 400, 401, 403, 404 (template_not_found), 413, 429, 500, 503, 504. See Errors.
GET /v1/og (signed URL)
Section titled “GET /v1/og (signed URL)”GET https://api.shotvik.com/v1/og?kid=<key_id>&template=<template>&vars[title]=Hello&sig=<signature>| Query parameter | Required | Description |
|---|---|---|
kid |
yes | Your public key ID (the 12-character ID that is part of your API key). The API key itself is never in the URL. |
template |
yes | A built-in template name or a stored template ID. Inline HTML isn’t possible in a GET URL. |
vars[<name>] |
no | One parameter per template variable, e.g. vars[title]=Hello. Same limits as above. |
exp |
no | Expiry as a Unix timestamp (seconds). After it, the link returns 403 signature_expired. Without exp, the link doesn’t expire. |
cache |
no | true or false. |
cache_ttl |
no | Cache lifetime in seconds, as above. |
sig |
yes | The signature. See Signed URLs. |
Every parameter except sig is covered by the signature, so changing anything (including cache or exp) invalidates the link. A parameter that appears twice is rejected. Revoking or blocking the key, or rotating its signing secret, invalidates all of its signed URLs.
Response: 200 with Content-Type: image/png. Errors:
- 400;
- 401 (
invalid_signature); - 403 (
signature_expired, or the key is suspended); - 404;
- 429, 500, 503, 504.
Caching
Section titled “Caching”Crawlers (Slack, LinkedIn, X, WhatsApp and others) fetch OG images repeatedly. Without caching, every fetch is a new render and counts toward your quota.
With cache=true:
- the image is stored on Shotvik’s Hetzner servers in Germany/Finland for
cache_ttlseconds (max 30 days, provisional), and served from there; there is no CDN in front of the API; - on signed GET links, the response carries
Cache-Control: public, max-age=<cache_ttl>, so browsers and crawlers may keep their own copies, even after you revoke the key; - cache hits return
X-Cache: HITand don’t count toward your quota. They carry the sameX-Shotvik-Warningsas the original render, if any.
Without caching, responses are sent with Cache-Control: private, no-store.
Templates
Section titled “Templates”Syntax: {{name}} inserts a variable, and {{name|fallback}} uses fallback when the variable isn’t set. Values are always HTML-escaped. There is no logic (no loops or conditions) and no way to output raw HTML. For custom fonts, use @font-face in your template HTML; fonts are loaded through the same safety checks as other resources. If a font or image can’t be loaded, the image is still rendered and the response lists it in X-Shotvik-Warnings (for example font:blocked:1).
Built-in templates:
| Name | Description | Variables |
|---|---|---|
basic |
Title and subtitle on a gradient, site name in the footer. | title, subtitle, site, accent |
article |
Blog/article card: category tag, title, author and date, CSS grid layout. | category, title, author, date, site |
Stored templates (API key required):
| Method and path | Body | Response |
|---|---|---|
POST /v1/templates |
{"name": "...", "html": "..."}: name 1–100 characters, HTML max 1,048,576 bytes |
201 {"id": "tpl_…", "name": "..."} |
GET /v1/templates |
200 {"builtin": [{name, description, vars}], "templates": [{id, name, created_at}]} |
|
DELETE /v1/templates/{id} |
204, or 404 template_not_found |
Stored templates are kept until you delete them or your account.
Shotvik is in private beta. Request access: support@shotvik.com · Privacy