Skip to content

An OG image API for your own HTML templates

CurlShot is an OG image API. Write the share card once as HTML and CSS, send it with a window of 1200 by 630 pixels, and the answer is a PNG at exactly that size, the one social networks and chat apps expect. Fill in the title of each article or product and you get one Open Graph image per page, in your own design. With the cache on, the same card is served from storage instead of rendered again.

Request
curl -X POST "https://curlshot.com/api/v1/screenshot" \
  -H "X-Access-Key: YOUR_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<div style=\"display:flex;align-items:center;height:100vh;padding:80px;font:600 72px system-ui\">How we cut our build time in half</div>","viewport_width":1200,"viewport_height":630}' \
  --output og.png

How to generate OG images with the API in three steps

  1. 1

    Design the card as HTML and CSS, with a place for the title.

  2. 2

    Send it with viewport_width 1200 and viewport_height 630 for each page.

  3. 3

    Save the image with your other images and point the og:image tag of the page at it.

From an HTML template to a 1200 × 630 PNG

An OG image is the picture that shows when a link is shared. You design the card once in HTML and CSS, fill in the title of each page, and the API returns the image at exactly the size of the window you set.

Show the table (5 rows)
OptionSet it toWhy
htmlYour card template, with the title filled inOne design, one image per page.
viewport_width, viewport_height1200 and 630The image has the size of the window. The default, 1280 × 1024, is the wrong shape for a card.
formatpng (default), or jpeg for a card with a photoPNG keeps text sharp.
device_scale_factor2, if cards look soft on dense screensThe same card at 2400 × 1260.
cache, cache_keytrue, and a version label such as card-v1The same card is served from storage instead of rendered again.

Use it from Next.js metadata

Render the card when a post is published or edited, save the file with your other images, and point the metadata at it. Do not point og:image at the API itself: a POST request cannot be a plain link, and files kept by the API are removed after a while.

lib/og.ts: run when a post is published
export async function renderOgCard(title: string): Promise<Buffer> {
  const response = await fetch('https://curlshot.com/api/v1/screenshot', {
    method: 'POST',
    headers: { 'X-Access-Key': process.env.SCREENSHOT_ACCESS_KEY!, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      html: cardHtml(title), // your template; escape the title
      viewport_width: 1200,
      viewport_height: 630,
      cache: true,
      cache_key: 'card-v1',
    }),
  })
  if (!response.ok) throw new Error((await response.json()).error_message)
  return Buffer.from(await response.arrayBuffer()) // save as public/og/<slug>-<hash>.png
}
app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  const post = await getPost(params.slug)
  return {
    title: post.title,
    openGraph: { images: [{ url: post.ogImage, width: 1200, height: 630, alt: post.title }] },
    twitter: { card: 'summary_large_image' },
  }
}

Caching and updating cards

Two caches matter: the one in the API, and the copy each social network keeps of your image.

  • With cache=true, the same HTML returns the stored card for free. A changed title is different HTML, so it gets a new card without any work on your side.
  • Cached copies live 4 hours by default; cache_ttl sets up to 30 days.
  • Swapped a logo or a font that the HTML only links to? Change cache_key from card-v1 to card-v2 and every card renders fresh.
  • Social networks keep their own copy for a long time. Save each new card under a new file name, for example with a short hash of the title, so they fetch it again.
A share card with a large title on a dark background, 1200 by 630 pixels
An OG image rendered from an HTML template: 1200 × 630, as social networks show it.

What people use it for

  • A sharing card for every blog post, made when it is published
  • Product cards with the name, the price and a photo
  • Cards for user profiles, events and job posts
  • Images for X, LinkedIn, Facebook, Slack and iMessage previews

For developers: one request

The same thing from your own code. It is a plain HTTP request, so it works from any language. Create a free account to get an access key.

Questions people ask

What size should an Open Graph image be?

1200 by 630 pixels is the size that X, LinkedIn, Facebook and most chat apps show without cropping.

How do I make one image per page automatically?

Keep one HTML template and fill in the title of each page before you send it to CurlShot. Each request returns the card for that page.

Can I use a screenshot of the page itself as the image?

Yes. Send the address of the page instead of HTML, with the same window size, and the card is a screenshot of its first screen.

Is the image rendered again every time someone shares the link?

Not with the cache switched on. The card is rendered once and repeat requests are served from storage, without counting against your allowance.

Take as many screenshots as you need

A free account gives you 100 screenshots every month and keeps them in one place. No card needed.