Quickstart

In this guide, you'll sign your first OG image URL and render it. Everything is free under 500 renders/month.

Step 1: Get an API Key

Visit your RenderOG dashboard and click "Create API Key". A key like sk_live_a1b2c3d4e5f6... (40 hex chars) will appear exactly once.

Store it safely — you'll need it to view usage and rotate keys. Local dev? Create a .env file:

SIGNING_SECRET=your_signing_secret_here

Step 2: Understand the Signing Algorithm

All GET /og requests are HMAC-SHA256 signed to prevent tampering.

Steps:

  1. Collect all parameters except sig: template=basic&title=Hello%20World&theme=dark&w=1200&h=630
  2. Sort alphabetically by key (already sorted in that example)
  3. Compute sig = HMAC-SHA256(SIGNING_SECRET, canonical) as hex
  4. Append to the URL: ...&sig=<hex>

Example (JavaScript):

const crypto = require('crypto');

const params = {
  template: 'basic',
  title: 'My First OG Image',
  theme: 'dark',
  w: '1200',
  h: '630',
};

// Sort and encode
const sorted = Object.keys(params)
  .sort()
  .map(k => `${k}=${encodeURIComponent(params[k])}`)
  .join('&');

const secret = process.env.SIGNING_SECRET;
const sig = crypto
  .createHmac('sha256', secret)
  .update(sorted)
  .digest('hex');

const url = `https://api.renderog.app/og?${sorted}&sig=${sig}`;
console.log(url);

Step 3: Render with curl

Copy the signed URL from above and fetch it:

curl -s "https://api.renderog.app/og?template=basic&title=My%20First%20OG%20Image&theme=dark&w=1200&h=630&sig=abc123..." \
  -o image.png

Open image.png in your browser. You've rendered an OG image at the edge.

Check performance headers:

curl -i "https://api.renderog.app/og?template=basic&title=My%20First%20OG%20Image&theme=dark&w=1200&h=630&sig=abc123..." | head -20

Look for:

  • X-Render-Ms: 45 — milliseconds to render (first time) or near-zero (cache hit)
  • X-Cache: MISS (first render) or HIT (cached)
  • Cache-Control: public, s-maxage=31536000, immutable — cached for 1 year at the edge

Step 4: Render with Fetch (JavaScript)

Modern Node or browser:

async function renderOG() {
  const params = {
    template: 'basic',
    title: 'My First OG Image',
    theme: 'dark',
  };

  // Sign (use the crypto snippet from Step 2)
  const sorted = Object.keys(params)
    .sort()
    .map(k => `${k}=${encodeURIComponent(params[k])}`)
    .join('&');

  const sig = crypto
    .createHmac('sha256', process.env.SIGNING_SECRET)
    .update(sorted)
    .digest('hex');

  const url = `https://api.renderog.app/og?${sorted}&sig=${sig}`;

  const res = await fetch(url);
  const buffer = await res.arrayBuffer();
  const ms = res.headers.get('x-render-ms');
  const cached = res.headers.get('x-cache');

  console.log(`Rendered in ${ms}ms (${cached})`);
  return buffer; // PNG bytes
}

renderOG();

Step 5: Use in Next.js Metadata

Generate a signed URL at build time (or on-the-fly in a server function) and use it in your metadata:

// app/page.tsx
import crypto from 'crypto';

function signOG(params) {
  const sorted = Object.keys(params)
    .sort()
    .map(k => `${k}=${encodeURIComponent(params[k])}`)
    .join('&');

  const sig = crypto
    .createHmac('sha256', process.env.SIGNING_SECRET)
    .update(sorted)
    .digest('hex');

  return `https://api.renderog.app/og?${sorted}&sig=${sig}`;
}

export const metadata = {
  title: 'My Blog Post',
  openGraph: {
    images: [
      {
        url: signOG({
          template: 'blog',
          title: 'My Blog Post',
          subtitle: 'Published today',
          theme: 'dark',
        }),
        width: 1200,
        height: 630,
      },
    ],
  },
};

export default function Page() {
  return <h1>My Blog Post</h1>;
}

What's Next?

Debugging

403 invalid_signature — Double-check that SIGNING_SECRET matches your key, and that all parameter values are URL-encoded identically during signing and in the final URL.

400 invalid_params — Check that title is ≤200 chars, w/h are within limits (max 2400×1260), and template is one of: basic, gradient, blog, product.

422 render_failed — The rendering engine encountered an error (usually bad CSS or invalid template HTML). Check the browser console if using the playground; test with simple templates first.

429 rate_limited — You've exceeded the rate limit (60/min free, 600/min paid). Check the Retry-After header.