jsontoimg templates are designed in the visual editor. The JSON you send is layer overrides (layers.headline.text), not a mustache document that describes every rectangle. This guide is the naming and overflow rules that survive production.
Overview
With a JSON-to-image API, the template is the product. Your API calls can be perfect — the right endpoint, the right auth, the right payload — but if the template is designed poorly, every image that comes out will look broken. Text overflows its box, images stretch instead of crop, and a price that looks fine at 3 characters turns into a disaster at 8.
This guide covers everything you need to design JSON image templates that render beautifully at any volume. We’ll walk through the anatomy of a template, each layer type, how dynamic text actually behaves, the naming conventions that keep your integrations stable, and a complete worked example: a product banner built end to end.
If you haven’t made your first render yet, start with the getting started tutorial — this guide assumes you know the basic flow: design a template, send JSON, get an image.
Why the Template Is the Contract
In a template-based image API, everything is separated into two parts:
- The template — the visual layout with placeholders. Designed once.
- The data — JSON that fills those placeholders. Sent on every render.
This separation is what makes the whole model work. A template-based image generation API “renders variable text, images, prices, and other structured data inside a predefined visual layout” — that’s the definition used across the industry (Abyssale’s roundup of template-based APIs puts it exactly that way). Bannerbear describes the same pattern as “design once, render many,” where every layer in the template becomes an object you can modify dynamically in your API request.
The design quality of that template is the single biggest factor in how good your generated images look. Data can’t fix bad layout — but a well-designed template can absorb almost any data you throw at it. So the real skill is designing templates that are *data-tolerant*: they still look right when the title is 20 characters, and still look right when it’s 120.
The Anatomy of a JSON Template
Every jsontoimg template is a JSON document with three parts: the canvas, the background, and an ordered layers array.
{
"width": 1200,
"height": 630,
"background": {
"type": "gradient",
"colors": ["#1a1a2e", "#16213e", "#0f3460"]
},
"layers": [
{
"type": "text",
"name": "headline",
"text": "Your headline goes here",
"font": { "size": 72, "weight": "bold", "color": "#ffffff" },
"position": { "x": 80, "y": 200 },
"width": 1040,
"align": "left"
}
]
}
Breaking it down:
- Canvas —
widthandheightin pixels. Design at the exact dimensions you’ll use in production. A 1200×630 social card, a 1080×1080 post, a 2000×800 banner. Don’t design big and scale down — text sizes and stroke weights are tuned per dimension. - Background — solid color, gradient, or an image. The background is the foundation; everything else renders on top of it.
- Layers — an ordered array. Later layers render on top of earlier ones, like a design tool’s layer panel. The order is part of the design, not an accident of your JSON.
Every layer needs a type and a name. The type tells the renderer what to draw; the name is the handle your API calls will use to target it.
Layer Types: Text, Image, and Shape
Text layers
The workhorse of dynamic image generation. Text layers control their own typography:
{
"type": "text",
"name": "price",
"text": "$49",
"font": {
"size": 48,
"weight": "bold",
"color": "#f59e0b"
},
"position": { "x": 80, "y": 420 },
"width": 400,
"align": "left",
"line_height": 1.2
}
Key properties: size, weight, color, align (left/center/right), line_height, and — most importantly — width. The width defines the wrapping box. Text that exceeds the box wraps to the next line; combined with line_height this determines how much vertical space the text block occupies.
Image layers
For logos, product photos, avatars, and background imagery:
{
"type": "image",
"name": "product_photo",
"image_url": "https://cdn.example.com/products/sneaker.png",
"position": { "x": 700, "y": 80 },
"width": 420,
"height": 470,
"fit": "cover",
"opacity": 1.0
}
The fit property matters more than people think:
cover— fills the box, cropping the overflow. Best for photos where the subject fills the frame.contain— fits the whole image inside the box, letterboxing with transparency. Best for logos and product shots on white backgrounds.fill— stretches to the box. Almost never what you want.
If you use cover, remember the box is a *crop window*. Center-weighted images survive cropping better than compositions with critical detail at the edges.
Shape layers
Badges, dividers, price tags, and accent blocks:
{
"type": "shape",
"name": "price_badge",
"shape": "rect",
"fill": "#f59e0b",
"radius": 12,
"position": { "x": 60, "y": 400 },
"width": 440,
"height": 80
}
Shapes give you the design elements that would otherwise be static: a highlight bar behind a headline, a rounded container behind a price, a divider line between sections. Layer them *behind* the text they support — order in the layers array is z-order.
Naming Layers: The API Contract
Here’s the rule that saves the most pain later: the name you give a layer is the key you’ll use in every API call, forever. Bannerbear’s own guidance is blunt about this — the names you give your layers are what you reference in your API requests, so get in the habit of naming clearly.
That means:
- Use stable, semantic names:
headline,subtitle,price,product_name,cta_text,logo. - Never use positional names:
text_1,layer_2,box_left. They describe location, not meaning, and the meaning is what your data pipeline cares about. - Use snake_case consistently so the names transfer cleanly into JSON keys and code variables.
- Treat names as immutable. Changing
product_nametotitlebreaks every integration that sendsproduct_name— silently. The render will fall back to the default text and nobody will notice until the images are wrong.
A good test: if you handed the layer list to a developer with no design context, would they know what data to send for each name? If not, rename it.
Dynamic Text Done Right
This is where most template quality is won or lost. Dynamic text is unpredictable by nature — you don’t control how long a product name or a blog title will be. A data-tolerant template handles that gracefully.
1. Give every text layer a sensible default
The text value in the template is the default. When an API call doesn’t override a layer, the default renders. Sensible defaults make the template testable in an editor (you see something reasonable) and give you graceful fallback when data is missing. text: "" is a valid value — sometimes you want an empty layer to collapse when there’s no data.
2. Design the box, not the text
Set an explicit width and let text wrap. Design the box with enough height for the *worst case* you expect, not the average. A headline box designed for a 40-character title will overflow (or clip) at 90 characters.
3. Use auto-fit for critical short text
For one-line text that must never wrap — prices, names, badge labels — enable auto-fit so the font shrinks to fit the box instead of overflowing:
{
"type": "text",
"name": "price",
"text": "$49",
"font": { "size": 64, "weight": "bold", "color": "#ffffff" },
"position": { "x": 80, "y": 410 },
"width": 400,
"height": 70,
"auto_fit": "shrink"
}
The renderer starts at size and shrinks until the text fits the box. This is the difference between a price that reads “$49” at any digit count and a price that spills over the badge.
4. Plan for overflow, don’t ignore it
For multi-line text (headlines, descriptions), decide what happens at the limit: allow wrapping with a fixed line count, or clip with an ellipsis. The render pipeline in template APIs typically applies “automated content adjustments” like auto-sizing and alignment before the final render (Robolly documents this exact pipeline: load template → apply modifications → auto-adjust content → render). Use those adjustments deliberately — set the max lines and let the engine handle the rest.
5. Watch the edges
Dynamic text doesn’t respect your padding. Keep text boxes away from the canvas edges — a 40px margin in the template becomes 0px when the text is bold and long. And when text sits on top of a photo background, add a scrim (a semi-transparent shape layer) or a gradient behind it so contrast never depends on what image arrives that day.
Two of those rules have standards behind them rather than taste. Overflow behaviour in CSS is governed by text-overflow, and the reason an ellipsis reads better than a clipped glyph is the same in a rendered image as it is on a page: a truncation the reader can see is information, while a silently cut word is a bug. Contrast is the other one — WCAG 2.1’s contrast minimum asks for 4.5:1 for body text and 3:1 for large text, and a template that fails it will fail it for every image it ever renders. Font loading deserves the same up-front thought; web.dev’s font best practices covers why a missing weight substitutes silently instead of erroring.
Defaults, Overrides, and Versioning
- Defaults live in the template. Every render starts from the template’s values.
- Overrides come from the API call. The
layersobject replaces values per named layer. - Never edit a template that’s live in production. Create a new version instead. When you change a layer’s
name, size, or position, every queued render and every integration referencing that template behaves differently — often invisibly.
Treat templates the way you’d treat a database schema: versioned, reviewed, and never mutated in place once integrations depend on it.
Worked Example: A Product Banner
Let’s put it together. A 1200×630 product banner with a product name, a price badge, a product photo, and a call to action — designed to survive real data.
Template:
{
"width": 1200,
"height": 630,
"background": {
"type": "image",
"image_url": "https://cdn.example.com/bg/dark-hero.jpg"
},
"layers": [
{
"type": "shape",
"name": "scrim",
"shape": "rect",
"fill": "rgba(10, 10, 20, 0.55)",
"position": { "x": 0, "y": 0 },
"width": 1200,
"height": 630
},
{
"type": "image",
"name": "product_photo",
"image_url": "https://cdn.example.com/products/default.png",
"position": { "x": 680, "y": 80 },
"width": 440,
"height": 470,
"fit": "cover"
},
{
"type": "text",
"name": "product_name",
"text": "Your product name",
"font": { "size": 56, "weight": "bold", "color": "#ffffff" },
"position": { "x": 80, "y": 190 },
"width": 520,
"line_height": 1.15
},
{
"type": "shape",
"name": "price_badge",
"shape": "rect",
"fill": "#f59e0b",
"radius": 14,
"position": { "x": 80, "y": 400 },
"width": 260,
"height": 76
},
{
"type": "text",
"name": "price",
"text": "$49",
"font": { "size": 44, "weight": "bold", "color": "#1a1a2e" },
"position": { "x": 100, "y": 412 },
"width": 220,
"height": 52,
"auto_fit": "shrink"
},
{
"type": "text",
"name": "cta_text",
"text": "Shop now",
"font": { "size": 30, "weight": "medium", "color": "#f59e0b" },
"position": { "x": 80, "y": 520 },
"width": 300
}
]
}
Notice what makes it data-tolerant:
- The
scrimshape guarantees text contrast over any background image. product_namehas a width and wraps; the badge is positioned well below the worst-case two-line name.priceusesauto_fit: "shrink"so “$9” and “$1,299.00” both fit the badge.- The product photo uses
coverwith a centered default — crops gracefully.
Render it:
curl -X POST "https://app.jsontoimg.com/api/v1/render?wait=60" \
-H "Authorization: Bearer $JSONTOIMG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "product-banner",
"format": "png",
"layers": {
"product_name": { "text": "Aurora Wireless Headphones" },
"product_photo": { "imageUrl": "https://cdn.example.com/products/aurora.png" },
"price": { "text": "$129.00" },
"cta_text": { "text": "Buy now" }
}
}'
Same template, different data — that’s the whole model. One template, thousands of SKUs. The product banner use case page shows this exact workflow scaled to an entire catalog.
Format is a template-level decision too, not an afterthought at request time. web.dev’s format guidance puts WebP and AVIF well ahead of JPG at matched quality for anything served to a browser, while MDN’s image file type guide is the reference for where each one is actually safe to use. The last pitfall is the one the API protects you from: an unrecognised layer key returns a 422 Unprocessable Content rather than rendering a half-filled image, so a typo surfaces in your logs instead of in a customer’s inbox.
Common Pitfalls (and How to Avoid Them)
| Pitfall | Symptom | Fix |
|---|---|---|
| Text overflows its box | Words clipped mid-glyph, overlapping layers | Set width, enable auto_fit for one-liners, design for worst case |
| No defaults on layers | Broken images when data is missing | Give every layer a sensible default value |
Wrong image fit |
Stretched logos, cropped product shots | cover for photos, contain for logos |
| Renaming live layers | Integrations silently render default text | Treat names as immutable; version templates instead |
| Text on busy backgrounds | Unreadable headlines | Add a scrim or gradient shape layer behind text |
| Variable-height text under fixed layout | Elements overlap when text wraps | Reserve vertical space; place lower layers below the worst-case text block |
Key Takeaways
- The template is the contract between your design and your data — design it to be data-tolerant, not just pretty.
- Layer names are the API surface: stable, semantic, snake_case, immutable.
- Dynamic text needs explicit boxes:
widthfor wrapping,auto_fitfor critical one-liners, defaults for graceful fallback. - Never edit a live template — version it. Integrations depend on its exact shape.
- Order matters: the layers array is z-order, and backgrounds/scrims belong first.
- A single well-designed template can produce thousands of distinct images — that’s the entire point.
FAQ
What makes a good layer name in an image template?
A stable, lowercase, descriptive name that describes the slot rather than the current content — product_title, not heading_2. The layer name is the API contract, so renaming it is a breaking change for every caller.
What happens if I send a layer name the template does not have?
The render is rejected with a 422 rather than silently dropping the value. A typo in a layer key fails loudly instead of shipping an image with a missing headline.
How do I stop long dynamic text from overflowing?
Design the box, not the string: give every text layer a sensible default, size the frame for the longest realistic value, and turn on auto-fit for short critical text so it scales down instead of spilling.
Can I redesign a template without breaking live renders?
Yes. Pin design_version in the request and your integration keeps rendering the version it was built against while you edit the draft.
Related Resources
- How to Auto-Generate Open Graph Images at Scale — template design applied to social cards
- How to Generate Product Banners for Thousands of SKUs — the worked example above, at catalog scale
- Building a Certificate Generator with a Template-to-Image API — multi-line dynamic text in practice
- jsontoimg Use Cases — the full range of template applications
Zoom out: How JSON-to-Image Conversion Works maps the whole template, named-layer, and render-job architecture.