Shotkiln Get a free key
Docs

Shotkiln API documentation

The Shotkiln API has three render endpoints: /v1/screenshot, /v1/pdf and /v1/og. Each accepts GET with query parameters or POST with a JSON body, and returns the file with the matching Content-Type. Base URL: https://shotkiln.com. The machine-readable spec is at /openapi.json.

Quickstart

shell
# 1. Create a free key (shown once)
curl -X POST https://shotkiln.com/v1/keys

# 2. Screenshot a page
curl "https://shotkiln.com/v1/screenshot?url=https://example.com&width=1440&height=900" \
  -H "x-api-key: $SHOTKILN_KEY" -o shot.png

Authentication

Send the key as x-api-key: shk_live_… or Authorization: Bearer shk_live_…. Test-mode keys start with shk_test_. Without a key, requests run as an anonymous demo: 3 per minute, 10 per day per network, watermarked.

You may pass api_key= in the query string for quick tests, but a key in a public page or <img src> can be copied and used against your quota. In production, call the API from your server or build step and serve the file yourself.

Screenshot

GETPOSThttps://shotkiln.com/v1/screenshot

Returns image/png, image/jpeg or image/webp.

ParameterTypeDefaultDescription
urlstringrequiredPublic http(s) URL. Ports 80, 443, 8080, 8443.
widthinteger1280Viewport width, 320–3840 (capped by plan).
heightinteger800Viewport height, 200–3840 (capped by plan).
full_pagebooleanfalseCapture the whole scroll height, max 16,384 px.
formatstringpngpng, jpeg or webp.
qualityinteger–1–100, jpeg and webp only.
delayinteger0Extra wait after load in ms, max 10,000.
device_scale_factornumber11–3. Use 2 for retina.
dark_modebooleanfalseEmulates prefers-color-scheme: dark.
cache_ttlinteger3600Seconds, 0–2,592,000. 0 always renders fresh.

PDF

GETPOSThttps://shotkiln.com/v1/pdf

Returns application/pdf. Raw HTML is cached per key, never shared.

ParameterTypeDefaultDescription
urlstring–Page to print. Either url or html.
htmlstring–Up to 1 MB, POST only. External assets must be public.
formatstringA4A3, A4, A5, Letter, Legal, Tabloid.
landscapebooleanfalse
print_backgroundbooleantrueInclude CSS backgrounds.
marginstring10mmAll sides, or margin_top, margin_right, margin_bottom, margin_left. Units px, mm, cm, in.
delay, cache_ttlinteger0, 3600As for screenshots.
shell
curl -X POST https://shotkiln.com/v1/pdf -H "x-api-key: $SHOTKILN_KEY" -H "content-type: application/json" \
  -d '{"html":"<h1>Invoice 42</h1>","format":"A4","margin":"15mm"}' -o invoice.pdf

OG image

GETPOSThttps://shotkiln.com/v1/og

Returns a 1200 × 630 image/png. Without a key the card carries a small label; with a paid key it does not. To link the endpoint from an og:image tag, use a signed URL so your key stays private, or generate cards at build time and host the PNG.

ParameterTypeDefaultDescription
titlestringrequiredUp to 200 characters. Font size adapts to length.
subtitlestring–Up to 300 characters.
templatestringdefaultdefault, split or gradient.
themestringdarklight or dark.
accentstring#ff5a1fHex colour for the bar or dot.
sitestring–Your name or domain, up to 60 characters.
cache_ttlinteger604800Seconds; 7 days by default.

Signed OG URLs

A signed URL lets you put /v1/og straight into an og:image tag without exposing your API key. The URL carries your key's public id (kid) and an HMAC-SHA256 signature (sig) of its parameters, made with the key's signing secret. Changing any parameter breaks the signature, so nobody can reuse the URL to draw other cards on your quota. Renders count against the key's plan; free keys produce watermarked cards.

  1. Get the signing secret once: POST /v1/keys/signing-secret with your key returns kid and signing_secret. Keep the secret on your server.
  2. Build the query string with the card parameters plus kid (and optionally exp, a Unix time in seconds after which the URL stops working).
  3. Sort the parameters by name, then value. Percent-encode each name and value with encodeURIComponent rules, join them as name=value with &, and prefix /v1/og?.
  4. Append sig: the lowercase hex HMAC-SHA256 of that string.
node
import { createHmac } from "node:crypto";
const enc = encodeURIComponent;
function signedOg(params, kid, secret) {
  const pairs = Object.entries({ ...params, kid }).map(([k, v]) => [k, String(v)]);
  pairs.sort(([a, x], [b, y]) => (a < b ? -1 : a > b ? 1 : x < y ? -1 : x > y ? 1 : 0));
  const qs = pairs.map(([k, v]) => enc(k) + "=" + enc(v)).join("&");
  const sig = createHmac("sha256", secret).update("/v1/og?" + qs).digest("hex");
  return "https://shotkiln.com/v1/og?" + qs + "&sig=" + sig;
}
python
import hmac, hashlib
from urllib.parse import quote

def signed_og(params, kid, secret):
    enc = lambda s: quote(str(s), safe="-_.!~*'()")
    pairs = sorted((k, str(v)) for k, v in {**params, "kid": kid}.items())
    qs = "&".join(enc(k) + "=" + enc(v) for k, v in pairs)
    sig = hmac.new(secret.encode(), ("/v1/og?" + qs).encode(), hashlib.sha256).hexdigest()
    return "https://shotkiln.com/v1/og?" + qs + "&sig=" + sig

To skip the code, POST /v1/og/sign with your key and the card parameters as JSON returns the signed url. POST /v1/keys/signing-secret/rotate issues a new secret and invalidates every URL signed with the old one; rotating the API key does the same. A wrong signature returns 403 invalid_signature, an expired one 403 signature_expired. Cache hits use no quota but count toward the per-minute limit.

Limits & headers

Every render response carries these headers, so a client can back off without calling a separate usage endpoint. GET /v1/usage returns the same numbers as JSON, plus resets_at (start of next month, UTC) and billing (whether the key has a Stripe billing portal). The usage dashboard shows them for any key.

HeaderMeaning
x-ratelimit-limit, -remaining, -resetPer-minute window (cache hits included)
x-quota-limit, -used, -remaining, -resetMonthly render quota, resets on the 1st (UTC)
x-cacheHIT, MISS or BYPASS (cache_ttl=0)
x-render-msBrowser time for this render, on misses
x-watermarktrue on anonymous and free renders
x-target-statusHTTP status of the page you captured

Pages may load at most 25 MB of resources and 300 requests. The render budget is 15 s anonymous, 20 s free and 30 s on paid plans.

Caching

Speed reference: median 2.5 s for uncached 1280 × 800 screenshots of 10 public sites (18 October 2026); cache hits 0.2–0.6 s.

Identical requests (same parameters, same watermark state) are served from cache for cache_ttl seconds and do not use quota. Set cache_ttl=0 for a fresh render every time. Public URLs and OG cards share a global cache; HTML you POST to /v1/pdf is cached only for your key.

Errors

Errors are JSON: {"error":{"code":"url_blocked","message":"…"}}. Failed renders are not charged.

CodeStatusWhat to do
invalid_parameter, invalid_url400Fix the request; the message names the parameter.
url_blocked, blocked_url400Target is private, uses a refused port or is on the denylist.
invalid_api_key401Key is malformed, revoked or rotated.
invalid_signature, signature_expired403Signed OG URL was changed, made with an old secret, or is past its exp.
page_too_large413The page exceeded 25 MB or 300 requests.
rate_limited, daily_limit, monthly_quota429Wait for Retry-After seconds, or upgrade.
navigation_failed502The target site did not load. Check it in a browser.
browser_unavailable503Retry after Retry-After (10 s).
timeout504Page took longer than the plan's render budget.

MCP

Setup for specific clients is on the MCP screenshot server page. Remote MCP server over Streamable HTTP at https://shotkiln.com/mcp, with tools take_screenshot, render_pdf and generate_og_image. Images come back as MCP image content; PDFs as embedded resources.

mcp.json
{
  "mcpServers": {
    "shotkiln": {
      "url": "https://shotkiln.com/mcp",
      "headers": { "x-api-key": "YOUR_KEY" }
    }
  }
}

The machine-readable spec is at /openapi.json (OpenAPI 3.1), and a short summary for LLMs at /llms.txt.