Docs
One endpoint. HTML and CSS in, an image out.
og:image, no server code of your own to run on each view.Quickstart
Try any HTML free, with no account, in the playground. To render at full size, get a key from the dashboard and either buy a $12 pack (1,000 renders, never expire) or subscribe. A fresh account has no monthly quota of its own: the free tier is the playground, not the API. Then:
curl -X POST https://pixrend.com/v1/render \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"html": "<div style=\"display:grid;place-items:center;height:100vh;font:700 64px Inter\">Hello</div>"
}' \
-o card.png
Defaults produce a 1200×630 PNG. No width/height/format required for the common case.
Authentication
Every request needs an Authorization header:
Authorization: Bearer sk_live_<32 random bytes, base64url>
Only the key's SHA-256 hash is stored server-side. There is no way to recover a lost key. Revoke it and create a new one from the dashboard.
POST /v1/render
Body is JSON, limited to 512 KB total, and is either {html, css?} (below) or {template, variables?} to render a saved template. See rendering a template for the second shape; any of the fields below on the body override that template's stored values.
| Field | Type | Default | Notes |
|---|---|---|---|
html | string | required | ≤ 200 KB. A fragment or a full document. |
css | string | optional | ≤ 100 KB. Injected as a <style> in <head>. |
width | int | 1200 | 100-2000 |
height | int | 630 | 100-2000 |
scale | 1 or 2 | 1 | Device scale factor (2 for retina-density PNGs) |
format | "png" or "jpeg" | png | |
quality | 1-100 | 90 | jpeg only |
Response
200: raw image bytes, Content-Type: image/png or image/jpeg, plus:
X-Quota-Used,X-Quota-Limit: this billing period, before this request. When a credit was spent instead,X-Quota-UsedequalsX-Quota-Limitand the plan bucket is untouched.X-Credits-Remaining: your credit balance after this request.X-Cache:HITorMISS. A cache hit (same account, same document and options, within 24 hours) is not charged.X-Render-Ms: server-side render time,0on a cache hit.
Errors
All errors are JSON: {"error":{"code":"...","message":"..."}}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Body failed schema validation. |
| 400 | invalid_html | Sanitizer rejected the HTML or CSS. See below. |
| 401 | invalid_key | Missing, malformed, or revoked key. |
| 404 | not_found | template does not exist on your account. |
| 413 | payload_too_large | Body over 512 KB, or html/css over their limits. |
| 429 | rate_limited | Over your plan's per-minute rate limit. See Retry-After. |
| 429 | quota_exceeded | Monthly quota used up and no credits left. A subscribed account spends a credit before this fires, so it means both are exhausted. Quota resets on the 1st, UTC; buy a pack for more credits any time. |
| 503 | busy | Render pool is full. Retry-After: 2. Rare, retry once. |
| 504 | render_timeout | Render took over 10s, usually a font or asset that never loads. |
Quota (or a credit) is deducted before rendering; a 5xx response other than 504 refunds whichever bucket was spent automatically. A 504 is not refunded: a slow document is the caller's own.
POST /v1/playground
No API key. Renders any HTML at half resolution so the output is unusable in production but good enough to try the product. One request at a time globally; nothing is stored or charged. When the server has a public BASE_URL configured, it only accepts calls whose Origin matches that URL, so include it when testing from curl:
curl -X POST https://pixrend.com/v1/playground \
-H "Content-Type: application/json" \
-H "Origin: https://pixrend.com" \
-d '{
"html": "<h1 style=\"font:700 48px Inter\">Hello</h1>",
"width": 800,
"height": 400
}' -o preview.png
| Field | Type | Default | Notes |
|---|---|---|---|
html | string | required | Same limits and sanitizer as /v1/render. |
css | string | optional | |
variables | object | optional | Same {{name}} syntax as templates. |
width | int | 1200 | 100-1200. Over 1200 is 400, not clamped. |
height | int | 630 | 100-630 |
200 returns a PNG rendered at half the requested layout (an 800×400 request comes back 400×200) with header X-Playground-Scale: 0.5. Errors: 400 invalid_request or invalid_html; 403 forbidden_origin if called from a browser origin other than pixrend.com; 503 busy if another playground render is already in flight; 429 rate_limited past 30 renders a minute site-wide. The interactive version is at /playground.html.
Account and keys
These use your session cookie (signed in on the dashboard) or a Bearer key. GET /v1/me:
curl https://pixrend.com/v1/me -b "session=..."
{
"email": "you@example.com",
"plan": "starter",
"usage": { "period": "2026-09", "used": 40, "limit": 5000, "credits_used": 0 },
"credits": 1000,
"signing_secret": "<64 hex chars>",
"keys": [{ "id": 1, "prefix": "sk_live_ab12", "name": "prod", "created_at": "2026-09-19 03:19:22" }],
"templates": [{ "id": "tpl_...", "name": "welcome", "width": 1200, "height": 630, "format": "png", "scale": 1, "quality": 90 }]
}
POST /v1/keys {"name":"prod"} returns 201 {id, prefix, key}, the raw key shown once. DELETE /v1/keys/:id returns 204; requests already in flight with a revoked key still fail on their next call. POST /v1/signing-secret/rotate returns a new {signing_secret} and immediately breaks every already-published signed URL: see signing URLs.
Fonts, images, and what's blocked
- Fonts: Google Fonts, via
@import url(https://fonts.googleapis.com/css2?...)in yourcss, or a<link>to the same host. Emoji and Noto fonts are installed on the box, so emoji render without any extra setup. - Images: must be
data:URIs. There is no remote image fetching. Inline your logo or photo as base64 before sending it. - Everything else network-facing is blocked: no other
<script>, no other remote stylesheet, no analytics pixel, no web font from any host but the two above. This is deliberate. See why on the homepage.
Build-time examples
Generate images once, at build time, and serve them as static files. That is the common case for blogs and marketing sites. For an image that is different per user or per event (a SaaS share card, a welcome image, a ticket), use a signed URL instead: it renders on first view and needs no build step or server code of your own. There is a worked example for a per-user welcome card.
Astro
// scripts/generate-og.mjs: run from an npm "prebuild" script
import { writeFile } from "node:fs/promises";
import { getCollection } from "astro:content";
const posts = await getCollection("blog");
for (const post of posts) {
const res = await fetch("https://pixrend.com/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PIXREND_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html: `<div style="display:flex;align-items:center;height:100vh;
padding:80px;font:700 56px Inter;background:#111;color:#fff">
${post.data.title}</div>`,
}),
});
await writeFile(`public/og/${post.slug}.png`, Buffer.from(await res.arrayBuffer()));
}
Next.js
// scripts/generate-og.mjs: run before "next build"
import { writeFile, mkdir } from "node:fs/promises";
import { posts } from "../lib/posts.mjs";
await mkdir("public/og", { recursive: true });
for (const post of posts) {
const res = await fetch("https://pixrend.com/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PIXREND_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html: renderCardHtml(post) }), // your own template fn
});
await writeFile(`public/og/${post.slug}.png`, Buffer.from(await res.arrayBuffer()));
}
Hugo
#!/usr/bin/env node
// scripts/generate-og.mjs: run before "hugo", reads front matter directly
import { readFile, writeFile, readdir } from "node:fs/promises";
import matter from "gray-matter";
for (const file of await readdir("content/posts")) {
const { data, content } = matter(await readFile(`content/posts/${file}`, "utf8"));
const res = await fetch("https://pixrend.com/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PIXREND_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html: `<div style="display:flex;align-items:center;height:100vh;
padding:80px;font:700 56px Inter;background:#111;color:#fff">
${data.title}</div>`,
}),
});
await writeFile(`static/og/${data.slug}.png`, Buffer.from(await res.arrayBuffer()));
}
Eleventy
// .eleventy.js: an async shortcode, cached per build by Eleventy itself
const fs = require("node:fs/promises");
module.exports = function (eleventyConfig) {
eleventyConfig.addAsyncShortcode("ogImage", async (slug, title) => {
const out = `og/${slug}.png`;
const res = await fetch("https://pixrend.com/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PIXREND_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html: `<div style="display:flex;align-items:center;height:100vh;
padding:80px;font:700 56px Inter;background:#111;color:#fff">
${title}</div>`,
}),
});
await fs.writeFile(`_site/${out}`, Buffer.from(await res.arrayBuffer()));
return `/${out}`;
});
};
Support
Email support@pixrend.com. Include the error code from the response and roughly when the request was made.