Authenticate every request with your API key in the X-API-Key header. Every render is a single GET — the URL is the image.
curl "https://getcardforge.dev/api/v1/card?template=og-gradient&title=Launch%20day&subtitle=We%20are%20live&accent=%23ff5533" -H "X-API-Key: cfg_your_key_here" -o card.pngWorks in Node 18+ and the browser. Build the query, send your key in the X-API-Key header, and read the response as a Blob/Buffer.
const params = new URLSearchParams({
template: "og-gradient",
title: "Launch day",
subtitle: "We are live",
accent: "#ff5533",
format: "webp", // png (default) | jpeg | webp
size: "twitter", // og | twitter | linkedin | instagram | square
});
const res = await fetch(
`https://getcardforge.dev/api/v1/card?${params}`,
{ headers: { "X-API-Key": "cfg_your_key_here" } },
);
if (!res.ok) throw new Error(await res.text());
const bytes = Buffer.from(await res.arrayBuffer());
await require("node:fs/promises").writeFile("card.webp", bytes);import requests
params = {
"template": "stat-card",
"title": "Monthly active users",
"value": "+128%",
"trend": "up",
"format": "jpeg", # png (default) | jpeg | webp
"size": "linkedin", # og | twitter | linkedin | instagram | square
}
res = requests.get(
"https://getcardforge.dev/api/v1/card",
params=params,
headers={"X-API-Key": "cfg_your_key_here"},
timeout=20,
)
res.raise_for_status()
with open("card.jpg", "wb") as f:
f.write(res.content)The /api/demo endpoint needs no key, works in any img tag and adds a small cardforge.dev watermark. Great for trying templates before you redeem.
<img
src="https://getcardforge.dev/api/demo?template=quote-card&title=Stay%20hungry&author=Steve"
width="540" alt="Quote card" />| Template | Output |
|---|---|
| og-gradient | 1200 x 630 PNG |
| og-minimal | 1200 x 630 PNG |
| quote-card | 1080 x 1080 (square) PNG |
| blog-cover | 1200 x 630 PNG |
| stat-card | 1200 x 630 PNG |
Add size= to force a platform dimension on any template — handy for the same card at Twitter, LinkedIn and square sizes.
| size | Output |
|---|---|
| auto | Template-natural (default) |
| og | 1200 x 630 |
| 1200 x 675 | |
| 1200 x 627 | |
| 1080 x 1080 | |
| square | 1080 x 1080 |
curl "https://getcardforge.dev/api/v1/card?template=og-minimal&title=We%20are%20hiring&size=linkedin&logo=https%3A%2F%2Fcdn.example.com%2Flogo.png" -H "X-API-Key: cfg_your_key_here" -o linkedin.pngAdd format= to choose the encoding. PNG is the lossless default; JPEG and WebP are smaller and ideal for crawler-facing images.
| format | Output |
|---|---|
| png | image/png — default, lossless |
| jpeg | image/jpeg — smaller, jpg is an alias |
| webp | image/webp — smallest, modern crawlers |
Social crawlers fetch an og:image URL many times and cannot send headers. A signed URL carries an HMAC-SHA256 sig that proves it was minted by your account, so it renders withoutan API key and is always served from Vercel's edge CDN on repeat hits — the first render is metered once, every crawler re-fetch is free. Tampering with any parameter invalidates the signature.
// Mint a signed URL server-side (Node) with your signing secret.
import { createHmac } from "node:crypto";
function signCardUrl(params, secret) {
const sp = new URLSearchParams(params);
sp.delete("sig");
// Canonical string: path + params sorted by key, URL-encoded.
const sorted = [...sp.entries()].sort(([a],[b]) => a < b ? -1 : a > b ? 1 : 0);
const query = sorted.map(([k,v]) =>
`${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
const sig = createHmac("sha256", secret)
.update(`/api/v1/card?${query}`).digest("hex");
sp.set("sig", sig);
return `https://getcardforge.dev/api/v1/card?${sp}`;
}
// <meta property="og:image" content={signCardUrl({ template:"og-gradient",
// title:"Launch day" }, process.env.CARDFORGE_SIGNING_SECRET)} />POST /api/v1/cards/bulk takes an array of card configs (max 50) and returns a ready-to-use signed URL for each — perfect for pre-generating OG images across a catalogue. Generation is bounded (5 in parallel) to stay within the request timeout; each returned URL is metered when it is first fetched.
curl -X POST "https://getcardforge.dev/api/v1/cards/bulk" -H "X-API-Key: cfg_your_key_here" -H "Content-Type: application/json" -d '{ "cards": [
{ "template": "og-gradient", "title": "Post one", "format": "webp" },
{ "template": "quote-card", "title": "Post two", "author": "Ada" }
] }'
# → { "count": 2, "cards": [
# { "index": 0, "url": "https://getcardforge.dev/api/v1/card?...&sig=...",
# "width": 1200, "height": 630, "format": "webp" }, ... ] }Pull your recent API requests as CSV or JSON for reporting — the same data the dashboard shows, downloadable with the format query.
curl "https://getcardforge.dev/api/dashboard/requests?format=csv" -H "X-API-Key: cfg_your_key_here" -o requests.csvAll parameters are query-string values. Unknown values never error — they fall back to safe defaults.
| Param | Applies to | Notes |
|---|---|---|
| template | all | og-gradient (default), og-minimal, quote-card, blog-cover, stat-card |
| title | all | Main text. Max 120 chars. Quote text on quote-card, label on stat-card. |
| subtitle | og-gradient, blog-cover, quote-card | Subtitle / category pill / handle. Max 160 chars. |
| author | og-minimal, quote-card, blog-cover | Author line. Max 60 chars. |
| brand | og-gradient, og-minimal, blog-cover, stat-card | Brand pill / logo text / footer. Max 40 chars. |
| theme | og-minimal, quote-card, stat-card | dark (default) or light. |
| accent | all | Hex color, URL-encode the hash: accent=%23ff5533. Invalid values fall back. |
| bg, bg2 | og-gradient | Gradient start / end hex colors. |
| value | stat-card | The big number, e.g. +128%. Max 24 chars. |
| trend | stat-card | up (green arrow), down (red arrow) or none. |
| date | blog-cover | Free date text, e.g. Jun 2026. Max 40 chars. |
| logo | all | Public https:// URL of a logo image (png/jpeg/webp/gif/svg). Embedded into the card. Internal/private hosts are rejected. |
| size | all | Output size preset: auto (default), og (1200x630), twitter (1200x675), linkedin (1200x627), instagram (1080x1080), square (1080x1080). Overrides the template-natural size. |
| format | all | Output image format: png (default), jpeg (alias jpg), webp. JPEG/WebP are smaller and ideal for crawlers. |
| cache | all | Set cache=edge to allow CDN caching of a public card (served from the edge on repeat hits). Default is no-store. |
| sig | all | HMAC signature for a key-less signed URL (see Signed URLs below). A valid sig renders without an API key and is always edge-cached. |
Success returns image/png. Errors return JSON:
{
"error": {
"code": "UNAUTHORIZED | QUOTA_EXCEEDED | RATE_LIMITED | RENDER_FAILED",
"message": "Human-readable explanation"
}
}Every response carries an X-Request-Id header — include it when contacting support so we can find your exact request in the logs.
The API is versioned in the path (/api/v1/…) and every response advertises the running version in the X-API-Version header (currently v1). We add fields backward-compatibly within a version. A breaking change ships under a new path (/api/v2); when that happens, v1 responses begin sending a Deprecation: true header and a Sunset date at least 6 months out, so you have a clear, monitored window to migrate.
Mutating endpoints (signup, redeem, checkout) accept an optional Idempotency-Key header. Retrying with the same key replays the original result instead of acting twice — safe for network retries and double-submits.
curl -X POST "https://getcardforge.dev/api/redeem" -H "Content-Type: application/json" -H "Idempotency-Key: redeem-9f2c14a8" -d '{ "code": "CFG-DM-XXXX", "email": "you@company.com" }'GET /api/health is a public, secret-free liveness probe for uptime monitors. It returns 200 with the running version and commit.
curl "https://getcardforge.dev/api/health"
# → { "status": "ok", "service": "cardforge", "apiVersion": "v1", "commit": "…" }