Cheaper than Bannerbear · Simpler than Placid · No template lock-in

Bring your own HTML.
Get back a perfect OG image.

The simplest open graph image API on the internet. No templates. No drag-and-drop editors. No Puppeteer servers to maintain. POST your HTML, receive a 1200×630 PNG. That's it.

Get free API key → See it render live ↓

Every OG image solution is broken in some way.

You shouldn't need a PhD in headless Chrome to add og:image tags to your app.

🔧

Option A: Build it yourself

Set up a Puppeteer/Playwright server. Fight memory leaks. Handle font loading failures. Manage cold starts. Maintain it forever. It'll break with every Node.js update.

💸

Option B: Use Bannerbear or Placid

Set up their template editor. Learn their variable syntax. Pay $49/mo for 1,000 images. Discover your design doesn't fit their template system. Start over.

🔒

Option C: Vercel OG

Only works if you're on Vercel. Requires React JSX syntax. Not callable from Python, Ruby, or PHP.

The solution is obvious: just let me send HTML.

That's what RawOG does.

One POST. One image.

Three steps. No templates, no editors, no BS.

1

Design your template in HTML + CSS

Write any HTML you want. Use any CSS. Load Google Fonts. It renders exactly as a browser would.

template.html
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      width: 1200px; height: 630px; margin: 0;
      background: linear-gradient(135deg, #0f0f0f, #1a1a2e);
      display: flex; align-items: center; justify-content: center;
      font-family: 'Inter', sans-serif; color: white;
    }
    .card { text-align: center; padding: 60px; }
    .title { font-size: 72px; font-weight: 800; line-height: 1.1; }
    .sub { font-size: 28px; color: #888; margin-top: 20px; }
    .badge { background: #22c55e; color: #000; padding: 8px 24px;
             border-radius: 100px; font-size: 18px; font-weight: 600;
             margin-top: 32px; display: inline-block; }
  </style>
</head>
<body>
  <div class="card">
    <div class="title">How I grew to<br/>10,000 subscribers</div>
    <div class="sub">The complete playbook — free</div>
    <div class="badge">Read now →</div>
  </div>
</body>
</html>
2

POST it to RawOG

terminal
curl -X POST https://rawog.dev/v1/og \
  -H "X-API-Key: rawog_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"html": "<your html here>"}' \
  --output og-image.png
3

Get back a perfect 1200×630 PNG

The response is raw PNG bytes. Save it, upload to your CDN, or use the GET endpoint for a stable og:image URL.

og:image meta tag (GET endpoint)
<meta property="og:image"
  content="https://rawog.dev/v1/og?api_key=rawog_...&html=YOUR_ENCODED_HTML">

Works from every language. No SDK required.

It's just HTTP. If your language can make a POST request, it works with RawOG.

JavaScript / Node.js
const res = await fetch('https://rawog.dev/v1/og', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': process.env.RAWOG_KEY,
  },
  body: JSON.stringify({ html: myTemplate }),
});

const buffer = await res.arrayBuffer();
await fs.writeFile('og.png', Buffer.from(buffer));
Python
import requests, os

res = requests.post(
    'https://rawog.dev/v1/og',
    headers={'X-API-Key': os.environ['RAWOG_KEY']},
    json={'html': my_template}
)

with open('og.png', 'wb') as f:
    f.write(res.content)
PHP
$ch = curl_init('https://rawog.dev/v1/og');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'X-API-Key: rawog_your_key',
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode(['html' => $tmpl]),
  CURLOPT_RETURNTRANSFER => true,
]);
file_put_contents('og.png', curl_exec($ch));
Ruby
require 'net/http'
require 'json'

uri = URI('https://rawog.dev/v1/og')
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-API-Key'] = ENV['RAWOG_KEY']
req.body = { html: my_template }.to_json

res = Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }
File.write('og.png', res.body)

3× cheaper than Bannerbear. Simpler than everything else.

No credit card required on the free tier.

Free
$0/mo
100 images/month
  • 1200×630 PNG output
  • Custom HTML + CSS
  • Google Fonts
  • No watermark
  • No credit card required
Get free key →
Best Value
Starter
$9/mo
500 images/month · $0.018/image
  • Everything in Free
  • PNG + JPEG output
  • Custom font URLs
  • Custom dimensions (up to 2400×1260)
  • 24-hour CDN caching
Start Starter →
Pro
$29/mo
5,000 images/month · $0.0058/image
  • Everything in Starter
  • PNG + JPEG output
  • Custom dimensions up to 4000×4000
  • Batch endpoint (10 per call)
  • Priority rendering queue
Start Pro →
RawOG Starter Bannerbear Placid
Monthly price $9 $49 $19
Images included 500 1,000 500
Price per image $0.018 $0.049 $0.038
Raw HTML input ✓ Yes ✗ Templates only ✗ Templates only
No lock-in

Overage: $0.02/image above plan limit — still cheaper than Bannerbear's base rate.

Quick answers.

Do I need to send a full HTML document or just a snippet?

Both work. You can send a full <!DOCTYPE html> document or just a fragment. If you send a fragment, we wrap it in a minimal HTML shell. For best control, send a full document with explicit width/height on the body.

Can I use Google Fonts or custom fonts?

Yes. Include a Google Fonts <link> tag in your HTML <head> and they'll load correctly. For custom/self-hosted fonts, use @font-face with a publicly accessible URL (Starter tier+).

How is this different from Vercel OG?

Vercel OG is free but locked to the Vercel platform and requires React JSX syntax. RawOG is language-agnostic — call it from Python, PHP, Ruby, or anything that can make an HTTP request. No framework required, no deployment constraint.

What's the rendering quality like?

We render using a real headless browser (same engine as Chrome). If it looks right in a browser, it'll look right in RawOG. CSS Grid, Flexbox, gradients, box shadows, border-radius — all render correctly.

Can I use the GET endpoint for og:image tags?

Yes — that's exactly what the GET endpoint is for. Set your og:image to https://rawog.dev/v1/og?api_key=...&html=... with URL-encoded HTML. Use encodeURIComponent() to encode the HTML.

What happens if my HTML fails to render?

If the HTML is malformed but parseable, we render whatever the browser produces. If rendering fails or times out (>10 seconds), we return HTTP 502 with {"error": "render_failed"}. We don't charge for failed renders.

Get your free API key

100 renders/month. No credit card. Ready in seconds.