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 |
How the page is read
Section titled “How the page is read”- 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. TheX-Shotvik-Data-Sourceresponse header says which one was used (jsonld,og, orfallbackfor 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: ShotvikBotgroup 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; aUser-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/httpsURL on port 80 or 443, also after redirects (max 5). Private addresses and URLs on Phishing.Database or our own blocklist are refused with403 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 /v1/og/from-url
Section titled “POST /v1/og/from-url”POST https://api.shotvik.com/v1/og/from-urlContent-Type: application/jsonAuthorization: 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. |
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.pngResponse
Section titled “Response”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 there is no usable data
Section titled “When there is no usable data”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) returns200with a plain card: the domain, the page title or URL path, plus yournameandpriceoverrides.X-Shotvik-Warningscontainsdata_unavailable, orrobots_disallowedfor a robots.txt refusal. This counts as a render.fallback: "error"returns422 target_data_unavailable. This isn’t counted.
response=link
Section titled “response=link”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 /v1/og/from-url (signed URL)
Section titled “GET /v1/og/from-url (signed URL)”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.
Usage and limits
Section titled “Usage and limits”- 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.