API Documentation

Generate 1200×630 Open Graph images via a single API call. Returns raw PNG bytes or a hosted CDN URL you can use directly in <meta property="og:image">.

Base URL: https://previewcard.dev

Auth: API key in request body or query string

Formats: image/png bytes · application/json with CDN URL

Authentication

Pass your API key in the POST body or as a GET query parameter. Keys are 40-character hex strings available in your dashboard.

# In POST body
{"api_key": "a1b2c3d4e5...", ...}

# As GET parameter
?api_key=a1b2c3d4e5...&template_id=1&title=Hello

POST /v1/generate

Template-based generation. Recommended for production.

FieldTypeReqDescription
api_key string Your API key
template_id integer ID of a saved template
data object Key-value pairs matching your template placeholders
return string "binary" (default, PNG bytes) or "url" (JSON with CDN URL)
fresh boolean true forces re-render, bypassing cache. Default false.
width integer Output width. Default 1200.
height integer Output height. Default 630.
curl -X POST https://previewcard.dev/v1/generate \
  -H "Content-Type: application/json" \
  -d '{
    "api_key":     "YOUR_KEY",
    "template_id": 1,
    "data":        {"title":"How I Built a $10K MRR API","author":"Jane","tag":"SaaS"},
    "return":      "url"
  }'

GET /v1/generate

Convenience shorthand. Use POST for any data containing long text, emojis, or non-ASCII. Max recommended query string: 500 chars.

https://previewcard.dev/v1/generate?api_key=YOUR_KEY&template_id=1&title=Hello+World&author=Jane&return=url

POST /v1/generate/html

Render arbitrary HTML/CSS. Paid plans only.

FieldTypeReqDescription
api_key string Paid plan API key required
html string Full HTML string. Max 1MB.
width integer Default 1200
height integer Default 630
js boolean Enable JavaScript. Default false.
return string "binary" or "url"

Response format

URL mode

{
  "url":               "https://cdn.previewcard.dev/og/abc123.png",
  "cached":            false,
  "render_ms":         287,
  "credits_used":      1,
  "credits_remaining": 1847,
  "expires_at":        "2026-05-23T14:00:00Z"
}

Binary mode (default)

Returns raw image/png bytes. On cache hit returns a 302 redirect to the CDN URL.

Error codes

HTTPCodeMeaning
401 MISSING_KEY api_key not provided
401 INVALID_KEY Key not found or revoked
429 RATE_LIMITED Exceeded per-minute rate limit for your plan
402 QUOTA_EXCEEDED Monthly image quota reached. Upgrade to continue.
404 TEMPLATE_NOT_FOUND Template not found or belongs to another account
403 PLAN_REQUIRED Raw HTML endpoint requires a paid plan
400 MISSING_HTML html field required for /generate/html
400 HTML_TOO_LARGE html exceeds 1MB
503 RENDER_FAILED Render service unavailable. Retry once after 1s.
504 RENDER_TIMEOUT Render exceeded 10s. Simplify your template.
{"error":true,"code":"QUOTA_EXCEEDED","message":"...","upgrade_url":"https://previewcard.dev/billing"}

Rate limits

Per API key, per minute. Cache hits count toward the rate limit but not your monthly quota.

PlanRequests/min
Free5
Starter30
Growth60
Scale120

Caching

Identical requests return cached images instantly. Cached responses do not count against your quota.

Cache key: SHA256(template_id | template_updated_at | sorted_params). Editing a template auto-invalidates its cache.

Pass "fresh":true to force a new render (counts toward quota).

PlanCDN TTL
Free7 days
Starter30 days
Growth90 days
Scale180 days

Code examples

Replace YOUR_KEY and TEMPLATE_ID with your values from the dashboard.

cURL

curl -X POST https://previewcard.dev/v1/generate \
  -H "Content-Type: application/json" \
  -d '{"api_key":"YOUR_KEY","template_id":TEMPLATE_ID,"data":{"title":"My Blog Post","author":"Jane","tag":"SaaS"},"return":"url"}'

PHP

<?php
$r = file_get_contents('https://previewcard.dev/v1/generate', false,
  stream_context_create(['http' => [
    'method'  => 'POST',
    'header'  => 'Content-Type: application/json',
    'content' => json_encode([
      'api_key' => 'YOUR_KEY', 'template_id' => TEMPLATE_ID,
      'data'    => ['title' => 'My Blog Post', 'author' => 'Jane', 'tag' => 'SaaS'],
      'return'  => 'url',
    ]),
  ]])
);
echo json_decode($r, true)['url'];

Python

import requests

res = requests.post('https://previewcard.dev/v1/generate', json={
    'api_key':     'YOUR_KEY',
    'template_id': TEMPLATE_ID,
    'data':        {'title': 'My Blog Post', 'author': 'Jane', 'tag': 'SaaS'},
    'return':      'url',
})
print(res.json()['url'])

Node.js

const res = await fetch('https://previewcard.dev/v1/generate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    api_key:     'YOUR_KEY',
    template_id: TEMPLATE_ID,
    data:        { title: 'My Blog Post', author: 'Jane', tag: 'SaaS' },
    return:      'url',
  }),
});
const { url } = await res.json();
console.log(url);

Ruby

require 'net/http'
require 'json'

uri  = URI('https://previewcard.dev/v1/generate')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
req.body = JSON.generate(
  api_key: 'YOUR_KEY', template_id: TEMPLATE_ID,
  data: { title: 'My Blog Post', author: 'Jane', tag: 'SaaS' },
  return: 'url'
)
puts JSON.parse(http.request(req).body)['url']

Go

package main

import (
  "bytes"; "encoding/json"; "fmt"; "io"; "net/http"
)

func main() {
  body, _ := json.Marshal(map[string]any{
    "api_key": "YOUR_KEY", "template_id": TEMPLATE_ID,
    "data":   map[string]string{"title": "My Blog Post", "author": "Jane", "tag": "SaaS"},
    "return": "url",
  })
  resp, _ := http.Post("https://previewcard.dev/v1/generate", "application/json", bytes.NewReader(body))
  defer resp.Body.Close()
  raw, _ := io.ReadAll(resp.Body)
  var result map[string]any
  json.Unmarshal(raw, &result)
  fmt.Println(result["url"])
}