OG Image API: Dynamic Open Graph Images from HTML

Turn any HTML page into an og:image with one signed URL. og_card and twitter_card presets, a public X-Cache-URL, 30-day caching and SDKs in six languages.

By Simon Chiu · Updated

RenderScreenshot is an OG image API that renders your own HTML in Chromium. Design the card as a page on your site, 1200×630, with the CSS and fonts you already use. Then point og:image at one signed URL with preset=og_card. When a crawler fetches it, the API loads your page, captures it and returns the PNG in the same response.

curl -G https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  --data-urlencode "url=https://example.com/og/hello-world?v=3" \
  -d preset=og_card \
  -o card.png

That is the test from a terminal. In production the same request becomes a signed URL in your <head>, so no key ever ships to the browser. The step-by-step OG image tutorial builds the whole setup with a Next.js App Router example. On WordPress, the Yoast and Rank Math plugin file does the same in PHP.

Why a screenshot API for OG images

The other ways to generate social cards each ask you to learn a second layout system:

  • JSX-to-SVG engines such as Satori, the engine inside @vercel/og, support flexbox and a subset of CSS. In our tests, CSS grid and backdrop-filter rendered without an error but with a different result than Chromium. See the Vercel OG alternative for the test.
  • Template editors such as Bannerbear and Placid keep the design in their editor and fill named layers through an API. Good for marketing teams, but your cards no longer live in your repo. See the Bannerbear alternative.
  • Self-hosted Puppeteer gives full CSS, and also a Chromium process to keep alive, fonts to install and a queue to run.

With an HTML template rendered by Chromium, the card is just another page. It shares your design tokens, your web fonts and your components. Designers can change it with the same pull request as the site.

What the API gives you for social cards

Presets for each card size. You do not need to remember dimensions:

Preset Viewport Format Default cache Use it for
og_card 1200×630 PNG 24 hours og:image for Facebook, LinkedIn, Slack, Discord
twitter_card 1200×600 PNG 24 hours X summary_large_image, exact 2:1
twitter_card_large 1200×628 PNG 24 hours X large cards close to 1.91:1
link_preview 800×600 PNG default Chat-style previews with page metadata

Any preset value can be overridden in the same request, for example format=jpeg for a photographic background. The presets reference lists every setting.

Signed URLs that go straight into the tag. A signed URL is a plain GET that returns image/png, so the tag points at the image itself, with no redirect route in your app. Signed URLs work for GET and HEAD only, the expiry is at most 30 days ahead, and changing any parameter after signing returns invalid_signature. Nobody can reuse your URL to render a different page on your account.

A public CDN link on every response. Each image response carries X-Cache-URL, a link to the stored file on our CDN that needs no authentication. If you prefer to render cards on your server when a post is published, store that link and use it as og:image until X-Cache-Expires. On Ghost, an Admin API script renders each card once and uploads it to the post, so it does not expire.

Zero-credit repeats. Crawlers fetch the same image many times. Sign cache_available=true (and cache_ttl up to 2592000 seconds, 30 days) into the URL, and each repeat fetch inside the TTL reuses the first render for 0 credits. The caching reference explains the cache key.

Page metadata for free. Responses include X-Screenshot-Title, X-Screenshot-Description and X-Screenshot-Favicon, read from the rendered page. That is handy when you build link previews of other people's URLs, not only cards for your own.

How a request flows

flowchart LR
    A[Crawler reads og:image] --> B[GET /v1/screenshot signed URL]
    B --> C{Signature valid and not expired?}
    C -- no --> E[401 JSON error]
    C -- yes --> D{cache_available and same card inside TTL?}
    D -- yes --> F[Stored PNG, 0 credits]
    D -- no --> G[Chromium on Cloudflare loads your template]
    G --> H[PNG stored in R2 and served from the CDN]
    H --> I[PNG returned, 1 credit, X-Cache-URL header]

We built the renderer on Cloudflare Browser Rendering, with images stored in R2. Rails checks the signature, the cache and the credits before a browser starts, so a bad URL fails fast and is never billed.

Minimal integration in six languages

Each SDK signs URLs with your public key ID (rs_pub_...) and secret key (rs_secret_...). The secret stays on your server. These calls follow the SDK reference pages; expiry must be within 30 days. Without an SDK, the SvelteKit example signs with Web Crypto.

// Node.js
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY, {
  signingKey: process.env.RS_SECRET_KEY,
  publicKeyId: process.env.RS_PUBLIC_KEY_ID,
});
const ogImage = client.generateUrl(
  TakeOptions.url('https://example.com/og/hello-world?v=3').preset('og_card'),
  new Date(Date.now() + 21 * 24 * 3600 * 1000),
);
# Python
from datetime import datetime, timedelta, timezone
from renderscreenshot import Client, TakeOptions

client = Client(api_key, signing_key=secret_key, public_key_id=public_key_id)
og_image = client.generate_url(
    TakeOptions.url('https://example.com/og/hello-world?v=3').preset('og_card'),
    datetime.now(timezone.utc) + timedelta(days=21),
)
# Ruby
client = RenderScreenshot::Client.new(api_key, signing_key: secret_key, public_key_id: public_key_id)
og_image = client.generate_url(
  RenderScreenshot::TakeOptions.url('https://example.com/og/hello-world?v=3').preset('og_card'),
  expires_at: Time.now + (21 * 24 * 3600)
)
<?php
// PHP
use RenderScreenshot\Client;
use RenderScreenshot\TakeOptions;

$client = new Client($apiKey, signingKey: $secretKey, publicKeyId: $publicKeyId);
$ogImage = $client->generateUrl(
    TakeOptions::url('https://example.com/og/hello-world?v=3')->preset('og_card'),
    new DateTime('+21 days')
);
// Go
client, _ := rs.New(apiKey, rs.WithSigningKey(secretKey), rs.WithPublicKeyID(publicKeyID))
ogImage, err := client.GenerateURL(
    rs.URL("https://example.com/og/hello-world?v=3").Preset("og_card"),
    time.Now().Add(21*24*time.Hour), "", "",
)
// Java
Client client = new Client(apiKey, null, null, signingKey, publicKeyId); // null = default base URL and timeout
String ogImage = client.generateUrl(
    TakeOptions.url("https://example.com/og/hello-world?v=3").preset("og_card"),
    Instant.now().plus(Duration.ofDays(21))
);

To sign cache_available=true as well, use the dependency-free signing functions on the signed URLs page, which accept any flat GET parameter.

Settings we recommend for cards

Setting Value Reason
preset og_card Correct size and format in one word
cache_available true Crawler repeats cost 0 credits
cache_ttl 2592000 Cards change only when you change them; 30 days is the maximum
Version in the template URL ?v=<updated_at> An edit gives a new cache key and a new image URL
Signed URL expiry 21 to 28 days, bucketed by week Stable URL for a week, never close to expiring when handed out
format leave PNG for flat designs Sharp text; switch to jpeg for photos

What it costs

One fresh render is one credit, a reused render is free, and a failed render is not charged. With cache_available=true and a 30-day TTL, each card is rendered at most about once a month plus once per edit, however often it is shared, because repeat crawler fetches hit the cache. So a blog needs roughly one credit per actively shared post per month. If that count is under 150, the free plan covers it.

A product with user-generated pages is different. At 10,000 new or changed cards a month, the Growth plan at $40 a month includes 12,000 credits, which works out to $4.00 per 1,000 cards at that volume. For smaller sites, Starter is $10 a month for 2,500 credits. All plans are on the pricing page.

When to pick something else

  • Your cards are simple flex layouts and you already host on Vercel: ImageResponse is built in and needs no account.
  • Non-developers must edit card designs visually without a deploy: a template editor fits that team better.
  • You need an image URL that never expires and you do not want to run storage: our CDN links last up to 30 days, so plan to copy cards into your own bucket.

Start

Create a free account, create a signing key pair under API Keys, and point one og:image at a signed og_card URL. The tutorial covers escaping, cache busting and platform limits, and the comparison of OG image generators shows where each tool fits.

Frequently asked questions

Which preset should I use for Open Graph images?
og_card. It sets a 1200x630 viewport, PNG output and a 24-hour cache. For a separate X image use twitter_card (1200x600, exactly 2:1) or twitter_card_large (1200x628).
Can I put the API URL directly in og:image?
Yes, as a signed URL. A GET request to /v1/screenshot returns the image bytes, so crawlers can fetch it like any image. The URL carries a public key ID and an HMAC signature, never your API key, and nobody can change its parameters.
Do crawler fetches cost a credit each time?
Only fresh renders cost a credit. Sign cache_available=true into the URL and repeat fetches of the same card inside the cache TTL return the earlier render for 0 credits. Failed renders are not charged.
What is X-Cache-URL for?
Every image response carries X-Cache-URL, a public CDN link to the stored PNG that needs no key or signature. If you render cards on your server, save that link and use it as og:image until X-Cache-Expires.
How long do signed URLs and cached cards last?
Both have a 30-day maximum. A signed URL must expire within 30 days, and cache.ttl accepts 3600 to 2592000 seconds. For a card that must outlive that, copy it to your own bucket with the storage option on a paid plan.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.