Skip to content

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.

bash
curl 'https://ogforge.co/api/og/aurora?title=Hello+from+OGForge' \
  -H 'X-API-Key: ogf_live_your_key' -o og.png

List the template catalog:

bash
curl https://ogforge.co/api/templates

Long titles or programmatic params? POST JSON instead:

bash
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.png

Agents 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:

ts
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:

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).

paramrequiredmax lennotes
titleyes160headline text
subtitle240sub-headline
kicker40small uppercase pill label
brand40site/brand name, bottom corner
imagesomeURLhttps://… or /u/{id}
logoURLsmall mark, corner-anchored
paletteenumaurora, ember, forest, ocean, sand, mono, midnight, royal, rose, lime
presetenumog (default), x, square, thumb
fmtenumpng (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.

presetsizeuse
og1200×630og:image default — LinkedIn, Slack, Discord…
x1600×900X/Twitter cards
square1200×1200feeds, Telegram, WhatsApp
thumb600×315thumbnails, 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.

bash
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.

lanelimit
anonymous render60 req/min per IP, burst 40
free session120 req/min
Pro session (web)600 req/min sanity cap
API keymonthly quota + 300 req/min sanity
x40260 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.

bash
curl 'https://ogforge.co/api/og/aurora?title=From+the+API' \
  -H 'X-API-Key: ogf_live_your_key' -o og.png

MCP

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

  1. list_templates — pick a stable id from useCase, optionally filter by category or tag.
  2. describe_template — read paramsSchema, sampleParams, and useCase. Do not invent extra fields.
  3. render_og — mint {url, width, height, preset, fmt}.
  4. GET that URL with an API key or x402 (not as public og: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

toolargumentsreturns
list_templatescategory (optional), tag (optional)templates[] — id, name, description, useCase, category, tags, defaultPalette
describe_templateid (required)schema, sampleParams, useCase. No layout tree. Unknown id → tool error.
render_ogtemplate, params (title required), preset, fmtJSON 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:

bash
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"}}}'
bash
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:

bash
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:

bash
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):

bash
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.

json
{
  "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:

json
{
  "mcpServers": {
    "ogforge": {
      "url": "https://ogforge.co/mcp"
    }
  }
}

Claude Code:

bash
claude mcp add --transport http ogforge https://ogforge.co/mcp

Local stack (make up): point at the Go listener. Vite also proxies /mcp from http://localhost:5173.

json
{
  "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.

flow
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 bytes

402 body (shape):

json
{
  "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-Key skip 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
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 id
  • 422 — invalid params, preset, fmt or image reference
  • 429 — rate limit or quota exhausted (Retry-After set)
  • 402 — x402 payment required (agent lane)