Skip to content

Product URL to OG image API — a 1200×630 card from a product page

Send the URL of a public product page and get a 1200×630 PNG product card back. Shotvik reads the product name, price, brand and image from the page and renders them in a built-in card or in one of your stored templates. You don’t need to write a template.

Use Endpoint Auth
Server-side (build step, backend, cron) POST /v1/og/from-url API key
Public link in <meta property="og:image"> GET /v1/og/from-url?…&sig=… Signed URL
  • Shotvik fetches the page’s static HTML once. It doesn’t use a browser and doesn’t run JavaScript on the page, so the product data must be in the HTML itself.
  • It uses the first source that has data: JSON-LD (Product), then Open Graph / product meta tags. A page that only has a <title> isn’t usable product data; it gets the fallback. The X-Shotvik-Data-Source response header says which one was used (jsonld, og, or fallback for the fallback card).
  • The page, the product image and an optional logo are fetched with only the User-Agent ShotvikBot/0.1 (+https://shotvik.com/bot): no cookies, no other User-Agent, no stealth, no captcha or bot-detection bypass and no proxies.
  • robots.txt is honoured. A shop can refuse ShotvikBot with a User-agent: ShotvikBot group in robots.txt or in its own user-agent rules, and that refusal is never bypassed. By default only a group that names ShotvikBot applies; a User-agent: * group on its own doesn’t. When the page is disallowed, it isn’t fetched and you get the fallback. More for site owners: ShotvikBot.
  • URL checks. The page must be a public http/https URL on port 80 or 443, also after redirects (max 5). Private addresses and URLs on Phishing.Database or our own blocklist are refused with 403 url_blocked, which isn’t counted. The same checks apply to every fetch the request makes, including redirects, robots.txt and images. See block reasons.
  • Brand names, product images and prices are your responsibility. The price is a snapshot taken at fetched_at; it isn’t guaranteed to still be the shop’s price. Report misuse to abuse@shotvik.com.
POST https://api.shotvik.com/v1/og/from-url
Content-Type: application/json
Authorization: Bearer <api_key>

Only url is required. Unknown fields are rejected.

Field Type Default Description
url string, max 2048 The product page (http/https, ports 80 and 443 only).
template string product product, product-compact, or the ID of one of your stored templates (tpl_…). A stored template receives the HTML-escaped variables {{name}}, {{price}}, {{currency}}, {{brand}}, {{domain}}, {{image}} and {{logo}}; {{image}} and {{logo}} are data URIs (or empty).
bg #RRGGBB Background colour. When omitted: the page’s theme-color meta tag, then the dominant colour of a PNG logo, then #0f172a.
fg #RRGGBB Text colour. Replaced with black or white when its contrast against the background is below WCAG AA (4.5:1).
accent #RRGGBB Accent colour, used as text on the white price chip. Darkened when needed to meet WCAG AA.
logo_url string, max 2048 Optional http(s) logo: PNG, JPEG or WebP, max 1 MB and 4000×4000 px. SVG is rejected (400). Without a usable logo, the brand or domain is shown as text.
show_price boolean true Show the price on the card.
locale string nl-NL Number format of the price. One of nl-NL, nl-BE, en-GB, en-US, de-DE, de-AT, de-CH, fr-FR, fr-BE, es-ES, it-IT, pl-PL, pt-PT, sv-SE, da-DK, nb-NO, fi-FI. When the page has a price but no currency, euros are assumed.
name string, max 1000 Override for the product name. The card shows at most 120 characters (warning truncated).
price string (max 32) or number Override for the price: a plain number, or a decimal with . or ,. No thousands separators.
fallback card or error card What happens when there’s no usable data. See below.
response png or link png png streams the image. link returns JSON with a signed GET URL instead. See response=link.
cache boolean false Opt-in cache on our Hetzner servers. Nothing is stored without it. Cache hits don’t count toward your quota.
cache_ttl integer, 60–2592000 86400 Cache lifetime in seconds (max 30 days). Only with cache: true.
data_max_age integer, 0–86400 3600 How old a product snapshot fetched earlier for your account may be, in seconds. Snapshots aren’t shared between accounts. The image cache (cache) is separate and can outlive data_max_age.
Terminal window
curl https://api.shotvik.com/v1/og/from-url \
-H "Authorization: Bearer $SHOTVIK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://vitalbuddies.com/products/jeuk-vrij"}' \
--fail-with-body --output product-og.png

200 with Content-Type: image/png. Besides the usage and rate-limit headers listed under Screenshot a URL, the response carries:

Header Description
X-Shotvik-Data-Source Where the product data came from: jsonld, og or fallback.
X-Shotvik-Data-Fetched-At When the product data was fetched (ISO 8601). A later cache hit repeats this time.
X-Shotvik-Warnings Optional. On this endpoint it lists short tokens, for example truncated, data_unavailable, robots_disallowed, image_unavailable, html_too_large or contrast_adjusted. Omitted when there’s nothing to report. At most 256 bytes; a value that doesn’t fit ends with truncated:N.

Errors: 400, 401, 403, 404 (template_not_found), 413, 422 (target_data_unavailable, only with fallback: "error"), 429, 500, 502, 503, 504. See Errors.

When the page answers 401, 403 or 429, shows a challenge page, times out, has no usable product data (for example only a <title>, without JSON-LD or Open Graph product fields), or robots.txt disallows it:

  • fallback: "card" (default) returns 200 with a plain card: the domain, the page title or URL path, plus your name and price overrides. X-Shotvik-Warnings contains data_unavailable, or robots_disallowed for a robots.txt refusal. This counts as a render.
  • fallback: "error" returns 422 target_data_unavailable. This isn’t counted.

With "response": "link", you get JSON instead of the image:

{
"url": "https://api.shotvik.com/v1/og/from-url?…&sig=…",
"expires_at": "…",
"data": { "name": "…", "price": "…", "currency": "…", "brand": "…", "image": "…", "domain": "…", "source": "jsonld", "fetched_at": "…" }
}

url is a signed GET link without your API key. It lives for max(60, data_max_age) seconds (expires_at). price, currency, brand and image can be null. The link call itself isn’t counted, but it goes through the same monthly quota check: when your monthly limit is already reached, it returns 429 monthly_quota_exceeded. The later GET counts when it returns a PNG.

GET https://api.shotvik.com/v1/og/from-url?kid=<key_id>&url=<product_url>&sig=<signature>

Returns the same card as the POST, for use in og:image. Parameters: kid (your public key ID, required), url (required), any of the POST fields above as strings except response, an optional exp (Unix seconds) and sig (required).

Signing works as for GET /v1/og, with this path in the string to sign:

sig = base64url( HMAC-SHA256( signing_secret, "GET\n/v1/og/from-url\n" + canonical_query ) )

canonical_query is every parameter except sig, each as encodeURIComponent(name)=encodeURIComponent(value), sorted by encoded name and joined with &. An expired link returns 403 signature_expired; a wrong signature or a repeated parameter returns 401 invalid_signature. Rotating your signing secret invalidates earlier links immediately.

Errors: 400, 401, 403, 404, 422, 429, 500, 503, 504.

  • A PNG response counts as one render, including the fallback card. response=link, fallback: "error" (422), blocks (403), timeouts and cache hits don’t count.
  • Limits per request: page HTML max 2 MB and 10 s; images max 5 MB, 4000×4000 px and 5 s each; the whole call max 20 s.
  • Shotvik uses at most 2 fetch slots per second per shop domain from each server. The page, robots.txt, product image and logo of one request share one slot when they’re on the same domain; an image on another domain takes a slot on that domain.
  • Your plan’s per-minute, concurrency and monthly limits apply as for other renders. See Rate limits and plans.