The Tech Stack Behind a JSON-to-Image API

Published October 15, 2026 by Balaji

Uncategorized

A JSON-to-image API is a job queue, a renderer, object storage, and signed URLs. This post walks the real paths: POST /api/v1/render, POST /api/v1/renders/batch (max 50), GET /api/v1/img/… — not a fictional /v1/images API.

Overview

Every image generation API looks the same from the outside: you POST JSON, you get a PNG back. The interesting part is what happens between the request hitting the server and the bytes landing in your response. That’s where the real engineering decisions live — and where most DIY implementations quietly fall apart.

This is the technical deep dive behind jsontoimg: why it renders images the way it does, what alternatives we evaluated, how the request pipeline is structured, and the specific trade-offs that shaped the architecture. If you’re building your own image pipeline (or just deciding whether to), this is the part nobody shows you.

The Core Problem: Rendering Is a Resource Problem

The moment you decide to generate images from data, you discover that the hard part isn’t the API — it’s the renderer. You have three honest options:

Approach Example tools Fidelity Cost to run
Headless browser Puppeteer, Playwright Pixel-perfect HTML/CSS High (Chromium is heavy)
Vector rendering resvg, node-canvas, skia-canvas Great for flat layouts Low to medium
Canvas/raster libs Sharp, Canvas API Great for photos/effects Low

The temptation is to reach for a headless browser, because it gives you “real” HTML and CSS — and it’s what GitHub itself uses to render Open Graph previews. But a headless browser is also a full browser: hundreds of megabytes of RAM per instance, CPU spikes, version drift, and the constant babysitting of zombie processes. At scale, that cost shows up on your infrastructure bill as clearly as on a line item.

The CustomJS blog summed up the hidden cost well: the browser library is easy — the queueing, browser updates, sandboxing, retries, and monitoring around it are what actually eat your engineering time.

Why jsontoimg Doesn’t Run a Headless Browser

The jsontoimg stack is built around deterministic template rendering, not HTML screenshots. That’s a deliberate choice, and it changes everything downstream.

When you send JSON to jsontoimg, the request doesn’t describe a web page — it describes a layer stack: a background color, an image, some styled text, an overlay. Each layer maps to a renderable primitive. Because the input is constrained, the renderer can be constrained too.

That gives us three properties a browser-based pipeline can’t match:

  1. Predictable memory — no tab, no DOM, no 60fps render loop. A render request uses a bounded, small amount of memory.
  2. Speed — vector rasterization of a flat composition takes milliseconds, not seconds. No page load, no networkidle2 wait, no font-loading race.
  3. Deterministic output — the same JSON always produces the same pixels. With a browser you’re one font-loading race away from a slightly different image on retry.

This is the same trade-off that services like Bannerbear, Placid, and APITemplate.io make, and it’s why template-based image APIs are so much cheaper per render than screenshot services.

The Rendering Pipeline

Here’s the actual request flow inside jsontoimg:

POST /api/v1/render (JSON payload)
        │
        ▼
┌─────────────────┐
│  API gateway     │  auth, validation, rate limiting
└────────┬────────┘
         ▼
┌─────────────────┐
│  Template engine │  merge JSON data into template layers
└────────┬────────┘
         ▼
┌─────────────────┐
│  Renderer        │  vector rasterization → PNG/JPG buffer
└────────┬────────┘
         ▼
┌─────────────────┐
│  Storage         │  object storage + CDN URL
└────────┬────────┘
         ▼
   image URL / webhook

Layer 1: Template Engine

Templates are stored as JSON schemas with typed layers — text, image, shape, group. The engine validates the incoming data against the schema, applies defaults, and resolves references (like layers.product_name.text or image URLs from your payload). This is a pure function: template + data → resolved composition. No side effects, no I/O, trivially testable.

Layer 2: The Renderer

The resolved composition is rasterized using a native vector renderer — not a browser. For text, that means explicit font handling: fonts are bundled and subsetted server-side, so “the title wraps differently on the 3rd render” is a problem we simply don’t have. For images, it means decoding input images (logos, backgrounds, product shots), scaling them to the layer bounds, and compositing with alpha blending.

Transparent PNG output is a first-class feature here — when the composition has no background layer, the alpha channel survives the pipeline. That’s how you can generate overlay graphics and logos, not just full-bleed cards.

Layer 3: Storage and Delivery

Every generated image is persisted to object storage and served through a CDN with cache headers. That means:

  • Renders are idempotent — requesting the same image twice returns the stored copy, not a fresh render
  • Response times for repeat requests are CDN-fast
  • Batch jobs don’t hold webhook payloads hostage to slow clients

The browser overhead is not a matter of opinion — Puppeteer’s own containerisation guide walks through the shared-memory limits, sandbox flags, and font packages a headless Chrome image needs before it will render reliably, which is a fair summary of the operational surface you inherit. Skipping it leaves the pipeline with a much smaller job: composite layers, write the file, and cache it. Delivery then follows ordinary web caching rules, where Cache-Control decides how long an edge keeps a rendered image and web.dev’s CDN primer explains why serving it from the edge matters more than shaving milliseconds off the render. Choosing what to write is the other half, and MDN’s image file type guide is the reference we work from, and web.dev’s format guidance is why WebP rather than PNG is the sensible default for anything bound for a browser.

Sync vs Async: The Two-Path Design

A common mistake in image APIs is forcing everything through one mode. jsontoimg supports both:

  • Synchronous — for single images where you want the bytes in the response. Ideal for on-demand OG images and user-facing graphics.
  • Asynchronous with webhooks — for batches. POST an array of payloads, get a job ID, and receive a callback when the whole batch is done.

The batch path uses a worker queue. Each item is a self-contained render job, so the queue scales horizontally: add workers, consume faster. This is the architecture that makes “generate 10,000 product banners” a plausible afternoon task instead of a weekend project. (See our product banner generator use case for how that plays out in practice.)

What We Chose NOT to Build

Being an indie product means every layer you add is a layer you maintain. The stack deliberately stops at the edges:

  • No visual template editor — templates are JSON, versioned in your repo, reviewed in PRs. Designers can use Figma → export a spec; developers ship it. The getting started guide walks through the flow end to end.
  • No queuing monolith — the queue is a thin layer, not a platform. If you need orchestration, your existing pipeline (n8n, Make, your own workers) can call the API directly.
  • No browser emulation — if you genuinely need to screenshot a live web page, a screenshot API is the right tool. That’s not this product, and that’s fine.

The discipline is the point: every “no” keeps the core render path fast, cheap, and predictable.

The Cost Math That Validated the Approach

The resource difference between browser-based and vector rendering isn’t subtle. Rough numbers from operating this class of pipeline:

Metric Headless browser render jsontoimg-style render
Memory per render 100–250 MB (Chromium) < 50 MB
Cold render latency 1–5 s (page load + fonts) 50–300 ms
Retry variance font/timing races none (deterministic)

That’s the difference between “a service that occasionally costs you real money” and “a function you can call in a loop without thinking.” For teams generating images inside request paths — dynamic OG images, personalized emails, event tickets — that variance is what kills the UX. This is exactly why our Open Graph image and event ticket use cases are built on the synchronous path.

Key Takeaways

  • The renderer is the architecture. Everything else — API shape, pricing, reliability — follows from whether you render with a browser or with a vector rasterizer.
  • Constrained input enables constrained cost. JSON layer stacks render fast and predictably; HTML screenshots don’t.
  • Two-path design matters. Sync for single images, async + webhooks for batches, and a worker queue that scales horizontally.
  • Storage is part of the product. Idempotent renders + CDN delivery mean repeat requests are cheap and fast.
  • Saying no is a feature. No browser, no visual editor, no queue monolith — every omission keeps the core path lean.

FAQ

Why not run a headless browser for image generation?

A browser loads a full web engine per render, which means heavy memory, cold starts, and font surprises for every image. Compositing named layers directly removes that whole dependency and makes render time predictable.

What is the difference between the sync and async render paths?

The sync path renders and returns a single image in the request, which is what an Open Graph image generator or an interactive flow needs. The async path queues the job and reports back by webhook, which is what a batch of thousands needs.

Where are rendered images stored and served from?

Each render is written to object storage and served over a CDN, so the first unique request pays the render cost and every later request streams the stored file.

Related Resources

Zoom out: How JSON-to-Image Conversion Works maps the whole template, named-layer, and render-job architecture.