Skip to content

Templated Open Graph images

Send words to /v1/og and get a 1200×630 PNG back — three templates, twelve parameters, no HTML, CSS or fonts of your own.

By LeonidPublished 6 min read

Method & path
GET /v1/og
Auth
Bearer key
Quota
1 call
Cache
24 h
Rev
2026-08-31

Words in, a finished image out

GET /v1/og draws one of three studio templates at 1200×630 and answers with a PNG. What separates it from every other endpoint in this folder is what it refuses to take: no URL, no HTML, no CSS, no font file and no colour value. You send the words. The layout, the palette and the typefaces are ours.

That is the design and not a filter. If you want to decide the pixels yourself, the neighbouring sheets are the ones you want — screenshots for a page that already exists on the web, and HTML to image for a document you write. This endpoint trades that control for two guarantees they cannot make. Nothing you send is read as markup, as a style rule or as an address: every value reaches the page as a text node, so a title holding an image tag with an onerror attribute is drawn as those characters rather than executed. And the document we photograph carries default-src 'none', placed in the head before the body, so the browser rendering it cannot issue a single network request — not for a logo, not for a tracker, not for a font, which is why the three faces are inlined as data URLs.

Because there is no target to fetch and nothing of yours for us to visit, this is the one endpoint here that can safely be wired to a button anyone may press — which is why this sheet ends in a control that runs rather than a recording.

The twelve parameters

Every text field is trimmed before it is measured, has to hold at least one printable character after cleaning, and is rejected with a 400 whose message names the constraint it broke. The ceilings are character counts, not byte counts.

ParameterCeilingDrawn byRequired
templatetitle, stat or boardallyes
themedark or light, dark by defaultallno
title120all threeyes
subtitle160title, boardno
kicker40title, statno
statValue6statyes, on stat
statLabel60statyes, on stat
start12boardyes, on board
target12boardyes, on board
hint60boardno
footer40allno
brand40allno

The small ceilings are the type sizes talking. A stat figure is set at 160 px, so six characters fill the line; the two board cells are set at 96 px, so twelve is what fits between them. The headline is set at 84 px, drops to 68 px past 36 characters and to 56 px past 60, which is where 120 ends it.

The last two are the footer and are not template-specific: footer fills its left slot and brand its right, on all three templates. Leave either out and that slot is drawn empty — your paid image is never footed with our domain or our studio name.

The three templates

template=title is a headline with an optional second line under it and an optional small label above it in the accent colour — the one to reach for when the image stands in for an article.

template=stat is one figure, set large and in the accent, with a label under it and the title third and smaller. Both the figure and its label are required: a number with nothing naming it is not an image anyone can read. A kicker is allowed here; a subtitle is not.

template=board draws a start cell, an arrow, a question mark, a second arrow and a target cell on the accent, under a title and an optional subtitle. Both cells are required. The optional hint is the line under the row, and it takes the footer's left slot when you send one — a caller who passed a hint meant it to be visible there.

A field the template does not draw is refused

Send a stat figure to the title template and the request fails, against that field, with template=title does not draw statValue. A missing requirement reads the same way: template=stat requires statValue.

Dropping the extra field quietly would be the easier behaviour and the wrong one. A caller who sent a figure believes the figure is on the image; ignoring it ships them a picture they did not ask for, and they find out when it is already in someone else's timeline. A 400 costs one round trip and names which of the two mistakes it was: the wrong template, or the wrong field for the template.

What we take out of your text

Before anything is drawn, tabs, newlines and carriage returns become spaces, runs of whitespace collapse to one, and the string is trimmed. Line breaks turn into spaces rather than vanishing, so two words on two lines arrive as two words and not one.

Then the control characters go: C0, DEL and C1, the left-to-right and right-to-left marks, and the bidirectional embedding, override and isolate controls. None of these draw anything. One of them is a weapon. A right-to-left override reverses the display order of everything after it, so a string that reads as one domain in your editor renders as a different one on the image — the classic filename spoof, applied to a picture that will be shared on your behalf. Removing the whole family beats reasoning about which members are safe in one line of display text.

Which scripts are drawn in our own face

Every image is set in IBM Plex, and Latin, Cyrillic and Greek all render in it. The bundled faces carry 192 of the 256 assigned Cyrillic codepoints and 73 of the 135 Greek ones; what is missing is archaic and minority letters, so every living alphabet written in them is complete.

Text outside those scripts still appears, but in a system fallback that will not match the rest of the image. Emoji come back in full colour; CJK, Arabic, Hebrew, Thai and the rest of the Basic Multilingual Plane come back in a substitute face. A handful of rare blocks above U+FFFF, such as Linear B and CJK Extension B, have no font in the renderer at all and do come back as empty boxes.

One in-script exception is worth planning around. The figure fields — statValue, start and target — are set in IBM Plex Mono, which carries exactly one Greek letter, π. Greek in those three fields only is drawn by a fallback face, beside Plex everywhere else on the same image. If a consistent look matters more than the character does, keep those three to Latin or Cyrillic.

This is a GET, and the query string is logged

The parameters ride in the URL, and our access log keeps the whole request line, query string included, for its retention period. Do not put anything confidential in one. The text of a social card is public by nature, which is what makes a GET the right shape here, but it is worth saying plainly rather than leaving you to infer it.

For the same reason there is deliberately no ?api_key= fallback. The key travels in the authorization header, which keeps it out of that log, out of browser history and out of any referrer. Getting started covers issuing one, and acceptable use covers what may be drawn with it.

Cache, metering and quota

Identical parameters inside 24 hours are served from cache, and the response says which you got in x-cache: hit or miss. The header on the way out is private, max-age=86400 — the answer is yours to keep for a day, and no shared cache may keep it.

A cache hit is still metered. You are billed for the answer rather than for the work behind it, so a page that asks for the same card on every render spends quota every time; store the PNG on your side if that matters. The free plan is 100 calls a month at 60 requests a minute and one render in flight; the paid plans go to 2,000, 10,000 and 30,000 calls a month, with up to 240 requests a minute and two concurrent renders. The plans carries the current table.

Refusals cost nothing. A 401, a rate-limited 429 and a quota-exceeded 429 return before the handler runs and write no usage row, and neither does a 503 when every render slot is taken — that failure is ours. A 502 or a 504 is billed, because the render was attempted.

The strip at the foot of this sheet

The panel below is not a recording. It builds a request from the fields you fill in, sends it to the real API on a real key, and shows the bytes that came back with the status, the content type and how long it took. Put seven characters in the figure and you read the refusal in the API's own words. What appears in that frame is what the endpoint returns.

Sources

  1. The Open Graph protocol (ogp.me)
  2. UAX #9: Unicode Bidirectional Algorithm (www.unicode.org)
  3. MDN — the meta element (developer.mozilla.org)

Commissioning test

Live · runs against the real API

curl -s "https://api.softaleo.com/v1/og?template=stat&theme=dark&title=Softaleo+API&statValue=1%2C284&statLabel=renders+today" \
  -H "authorization: Bearer $KEY" \
  -o og.png

Six characters, and only the stat template draws it.

Nothing rendered yet. Run it and the PNG the API returns appears here, at 1200×630.

Operations

Every operation the API publishes, read from its OpenAPI document
MethodPathWhat it does
POST/v1/html-to-imageRender an HTML string to a PNG
GET/v1/ogRender a templated Open Graph image
POST/v1/pdfRender a URL or an HTML string to a PDF
GET/v1/pingLiveness ping
POST/v1/screenshotRender a URL to a PNG or JPEG

Written and maintained by Softaleo. Found a mistake? Use the feedback button on this page — corrections are fixed and dated above.