Docs

One endpoint. HTML and CSS in, an image out.

Generating one image per user or per event, not once at build time? Save a template with variables and sign a URL you paste straight into 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:

POST /v1/render200 image/png
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 headerrequired on every request
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.

FieldTypeDefaultNotes
htmlstringrequired≤ 200 KB. A fragment or a full document.
cssstringoptional≤ 100 KB. Injected as a <style> in <head>.
widthint1200100-2000
heightint630100-2000
scale1 or 21Device scale factor (2 for retina-density PNGs)
format"png" or "jpeg"png
quality1-10090jpeg only

Response

200: raw image bytes, Content-Type: image/png or image/jpeg, plus:

Errors

All errors are JSON: {"error":{"code":"...","message":"..."}}

StatusCodeMeaning
400invalid_requestBody failed schema validation.
400invalid_htmlSanitizer rejected the HTML or CSS. See below.
401invalid_keyMissing, malformed, or revoked key.
404not_foundtemplate does not exist on your account.
413payload_too_largeBody over 512 KB, or html/css over their limits.
429rate_limitedOver your plan's per-minute rate limit. See Retry-After.
429quota_exceededMonthly 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.
503busyRender pool is full. Retry-After: 2. Rare, retry once.
504render_timeoutRender 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:

POST /v1/playground200 image/png, half resolution
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
FieldTypeDefaultNotes
htmlstringrequiredSame limits and sanitizer as /v1/render.
cssstringoptional
variablesobjectoptionalSame {{name}} syntax as templates.
widthint1200100-1200. Over 1200 is 400, not clamped.
heightint630100-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:

GET /v1/me200 application/json
curl https://pixrend.com/v1/me -b "session=..."
response body200 application/json
{
  "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

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

astro prebuild scriptPOST /v1/render
// 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

next.js prebuild scriptPOST /v1/render
// 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

hugo prebuild scriptPOST /v1/render
#!/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 shortcodePOST /v1/render
// .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.