Dynamic OG image API
Hosted OG image API: generate with one GET (session, API key, or x402). Put a
saved /u/{id} URL in og:image.
Works from Next.js, Astro, and any stack. Errors are RFC 9457 application/problem+json.
Quickstart
Generate a dynamic OG image with one authenticated GET (API key, session, or x402).
Same params, same bytes. For og:image, save a /u/{id} file — live query URLs are not public.
curl 'https://ogforge.co/api/og/aurora?title=Hello+from+OGForge' \
-H 'X-API-Key: ogf_live_your_key' -o og.pngList the template catalog:
curl https://ogforge.co/api/templatesLong titles or programmatic params? POST JSON instead:
curl -X POST https://ogforge.co/api/render \
-H 'Content-Type: application/json' \
-H 'X-API-Key: ogf_live_your_key' \
-d '{"template":"aurora","params":{"title":"Hello from OGForge"},"preset":"og","fmt":"png"}' \
-o og.pngAgents also negotiate this page as Markdown: request with Accept: text/markdown. A metered key is Pro; unauthenticated
GETs can pay via x402 — that is the free OG image generator API lane for agents.
Next.js, Astro, and Vercel OG
OGForge is a hosted dynamic OG image API, not an HTML-to-image renderer and not @vercel/og running on the Edge. You design with templates and params; we return PNG/JPEG/WebP. Use it as a Vercel OG alternative when you want the same URL from Next.js, Astro, SvelteKit, or a CMS — any host.
Next.js dynamic OG image
Skip opengraph-image.tsx if you do not want to maintain
JSX cards. Put the OGForge URL in metadata:
export const metadata = {
openGraph: {
images: [{
url: 'https://ogforge.co/u/<saved-id>',
width: 1200,
height: 630
}]
},
twitter: { card: 'summary_large_image' }
};Astro OG image generation
Astro does not execute JS for most social crawlers. Emit the tags in the initial HTML:
---
const og = 'https://ogforge.co/u/<saved-id>';
---
<meta property="og:image" content={og} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />Gatsby and other SSGs work the same way: an absolute og:image URL. We do not screenshot HTML/CSS.
Templates & params
120 templates with stable IDs — never renamed, never reused. Exclusive use-case
category plus overlapping style tags. List them with GET /api/templates or GET /api/categories. Every template accepts the
common params; per-template extras (author, stats, steps…) live in its
JSON-Schema at GET /api/templates/{id} (or the MCP describe_template tool).
| param | required | max len | notes |
|---|---|---|---|
| title | yes | 160 | headline text |
| subtitle | – | 240 | sub-headline |
| kicker | – | 40 | small uppercase pill label |
| brand | – | 40 | site/brand name, bottom corner |
| image | some | URL | https://… or /u/{id} |
| logo | – | URL | small mark, corner-anchored |
| palette | – | enum | aurora, ember, forest, ocean, sand, mono, midnight, royal, rose, lime |
| preset | – | enum | og (default), x, square, thumb |
| fmt | – | enum | png (default), jpeg, webp |
Presets & formats
Four canvas presets cover every network. Templates are authored in 1200-wide design units and scaled exactly — no layout drift between presets.
| preset | size | use |
|---|---|---|
| og | 1200×630 | og:image default — LinkedIn, Slack, Discord… |
| x | 1600×900 | X/Twitter cards |
| square | 1200×1200 | feeds, Telegram, WhatsApp |
| thumb | 600×315 | thumbnails, embeds |
Formats: png (default), jpeg (q90), webp (q90 lossy). Pick with fmt=.
Determinism & caching
Render key = sha256(template@version | canonical params | preset | fmt | engine
version). No time, no randomness in render paths — decorative variation derives from
the inputs themselves. Successful renders respond with an ETag and Cache-Control: public, max-age=31536000, immutable.
curl -sI 'https://ogforge.co/api/og/aurora?title=Hello' \
-H 'X-API-Key: ogf_live_your_key' | grep -i etag
# send it back → 304 Not Modified, zero render cost
curl -H 'X-API-Key: ogf_live_your_key' \
-H 'If-None-Match: "<the-etag>"' \
'https://ogforge.co/api/og/aurora?title=Hello' -o /dev/null -w '%{http_code}\n'Rate limits
Per-IP token buckets (per-user for sessions). Responses carry X-RateLimit-Limit/Remaining/Reset; exhaustion returns
429 with Retry-After.
| lane | limit |
|---|---|
| anonymous render | 60 req/min per IP, burst 40 |
| free session | 120 req/min |
| Pro session (web) | 600 req/min sanity cap |
| API key | monthly quota + 300 req/min sanity |
| x402 | 60 paid renders/min per IP |
API keys
Pro and Enterprise manage keys in the dashboard: format ogf_live_…, stored hashed, shown exactly once, revocable.
Pro includes 1000 API-key renders per UTC month (web renders are unmetered); hitting
the quota returns 429 with X-Quota-Remaining: 0.
curl 'https://ogforge.co/api/og/aurora?title=From+the+API' \
-H 'X-API-Key: ogf_live_your_key' -o og.pngMCP
OGForge speaks Model Context Protocol over streamable HTTP (protocol 2025-03-26) at https://ogforge.co/mcp.
JSON-RPC 2.0. Tools only — no resources, no prompts.
The server never returns image bytes. It returns a canonical GET /api/og/… URL. Fetch that URL to render.
What an agent should do
list_templates— pick a stable id fromuseCase, optionally filter bycategoryortag.describe_template— readparamsSchema,sampleParams, anduseCase. Do not invent extra fields.render_og— mint{url, width, height, preset, fmt}.GETthat URL with an API key or x402 (not as publicog:image— crawlers will not send credentials). For share URLs, save a generated/u/{id}file.
REST equivalents: GET /api/templates, GET /api/templates/{id}, GET /api/og/{id}?…. /mcp is unauthenticated.
Calling render_og does not spend quota and does not charge.
The follow-up GET does.
Tools
| tool | arguments | returns |
|---|---|---|
| list_templates | category (optional), tag (optional) | templates[] —
id, name, description, useCase, category, tags, defaultPalette |
| describe_template | id (required) | schema, sampleParams, useCase. No layout tree. Unknown id → tool error. |
| render_og | template, params (title required), preset, fmt | JSON text: url, width, height, preset, fmt. Invalid params → 422-style tool error. |
Handshake
Streamable HTTP: POST JSON-RPC to /mcp with Accept: application/json, text/event-stream.
Hosted clients do this for you. Raw curl:
curl -s https://ogforge.co/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'curl -s https://ogforge.co/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'Call the tools
List agent-category templates:
curl -s https://ogforge.co/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_templates","arguments":{"category":"agent"}}}'Read Aurora's param contract:
curl -s https://ogforge.co/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"describe_template","arguments":{"id":"aurora"}}}'Mint a render URL (no pixels yet):
curl -s https://ogforge.co/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"render_og","arguments":{"template":"aurora","preset":"og","fmt":"png","params":{"title":"Hello agents","kicker":"MCP"}}}}'render_og content is this JSON. Same inputs ⇒ same URL
⇒ same bytes.
{
"url": "https://ogforge.co/api/og/aurora?title=Hello+agents&kicker=MCP&preset=og&fmt=png",
"width": 1200,
"height": 630,
"preset": "og",
"fmt": "png"
}Connect a client
Cursor — user ~/.cursor/mcp.json or project .cursor/mcp.json:
{
"mcpServers": {
"ogforge": {
"url": "https://ogforge.co/mcp"
}
}
}Claude Code:
claude mcp add --transport http ogforge https://ogforge.co/mcpLocal stack (make up): point at the Go listener.
Vite also proxies /mcp from http://localhost:5173.
{
"mcpServers": {
"ogforge": {
"url": "http://127.0.0.1:8080/mcp"
}
}
}x402 for agents
Agents don't need accounts or API keys. An unauthenticated GET /api/og/… (including a URL minted by render_og) receives HTTP 402 with x402 payment
requirements — $0.01 per render (10000 atomic units) in USDC on Base, exact scheme. Pay, then retry the
same URL with the proof. Replays are deduped by transaction hash.
1. GET /api/og/aurora?title=Hello
→ HTTP 402 + JSON payment requirements
2. Pay $0.01 USDC on Base (exact scheme, 10000 atomic units)
3. Base64-encode the payment payload
4. Retry the same URL with X-PAYMENT: <base64>
→ HTTP 200 + image bytes402 body (shape):
{
"x402Version": 1,
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "10000",
"asset": "<USDC on Base>",
"payTo": "<ogforge wallet>",
"resource": "https://ogforge.co/api/og/aurora?title=Hello",
"mimeType": "image/png",
"description": "OGForge render",
"maxTimeoutSeconds": 60
}]
}- Header:
X-PAYMENT— base64 of the x402 payment payload. - Network: Base mainnet in production; Base Sepolia in development.
- Verify then settle via the facilitator. A replay never double-charges.
- Paid lane: 60 renders/min per IP. Anti-replay is the real gate.
- Pro/Enterprise with
X-API-Keyskip this lane and use the monthly quota instead.
Errors
All errors are RFC 9457 application/problem+json.
Validation failures (422) carry a per-field errors array.
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "title is required",
"errors": [{ "location": "query.title", "message": "required" }]
}404— unknown template id422— invalid params, preset, fmt or image reference429— rate limit or quota exhausted (Retry-After set)402— x402 payment required (agent lane)